# -*- coding: utf-8 -*-
# imageio is distributed under the terms of the (new) BSD License.

""" SPE file reader
"""

import os
import logging

import numpy as np

from .. import formats
from ..core import Format


logger = logging.getLogger(__name__)


class Spec:
    """SPE file specification data

    Tuples of (offset, datatype, count), where offset is the offset in the SPE
    file and datatype is the datatype as used in `numpy.fromfile`()

    `data_start` is the offset of actual image data.

    `dtypes` translates SPE datatypes (0...4) to numpy ones, e. g. dtypes[0]
    is dtype("<f") (which is np.float32).

    `controllers` maps the `type` metadata to a human readable name

    `readout_modes` maps the `readoutMode` metadata to something human readable
    although this may not be accurate since there is next to no documentation
    to be found.
    """

    basic = {
        "datatype": (108, "<h"),  # dtypes
        "xdim": (42, "<H"),
        "ydim": (656, "<H"),
        "xml_footer_offset": (678, "<Q"),
        "NumFrames": (1446, "<i"),
        "file_header_ver": (1992, "<f"),
    }

    metadata = {
        # ROI information
        "NumROI": (1510, "<h"),
        "ROIs": (
            1512,
            np.dtype(
                [
                    ("startx", "<H"),
                    ("endx", "<H"),
                    ("groupx", "<H"),
                    ("starty", "<H"),
                    ("endy", "<H"),
                    ("groupy", "<H"),
                ]
            ),
            10,
        ),
        # chip-related sizes
        "xDimDet": (6, "<H"),
        "yDimDet": (18, "<H"),
        "VChipXdim": (14, "<h"),
        "VChipYdim": (16, "<h"),
        # other stuff
        "controller_version": (0, "<h"),
        "logic_output": (2, "<h"),
        "amp_high_cap_low_noise": (4, "<H"),  # enum?
        "mode": (8, "<h"),  # enum?
        "exposure_sec": (10, "<f"),
        "date": (20, "<10S"),
        "detector_temp": (36, "<f"),
        "detector_type": (40, "<h"),
        "st_diode": (44, "<h"),
        "delay_time": (46, "<f"),
        # shutter_control: normal, disabled open, disabled closed
        # But which one is which?
        "shutter_control": (50, "<H"),
        "absorb_live": (52, "<h"),
        "absorb_mode": (54, "<H"),
        "can_do_virtual_chip": (56, "<h"),
        "threshold_min_live": (58, "<h"),
        "threshold_min_val": (60, "<f"),
        "threshold_max_live": (64, "<h"),
        "threshold_max_val": (66, "<f"),
        "time_local": (172, "<7S"),
        "time_utc": (179, "<7S"),
        "adc_offset": (188, "<H"),
        "adc_rate": (190, "<H"),
        "adc_type": (192, "<H"),
        "adc_resolution": (194, "<H"),
        "adc_bit_adjust": (196, "<H"),
        "gain": (198, "<H"),
        "comments": (200, "<80S", 5),
        "geometric": (600, "<H"),  # flags
        "sw_version": (688, "<16S"),
        "spare_4": (742, "<436S"),
        "XPrePixels": (98, "<h"),
        "XPostPixels": (100, "<h"),
        "YPrePixels": (102, "<h"),
        "YPostPixels": (104, "<h"),
        "readout_time": (672, "<f"),
        "xml_footer_offset": (678, "<Q"),
        "type": (704, "<h"),  # controllers
        "clockspeed_us": (1428, "<f"),
        "readout_mode": (1480, "<H"),  # readout_modes
        "window_size": (1482, "<H"),
        "file_header_ver": (1992, "<f"),
    }

    data_start = 4100

    dtypes = {
        0: np.dtype(np.float32),
        1: np.dtype(np.int32),
        2: np.dtype(np.int16),
        3: np.dtype(np.uint16),
        8: np.dtype(np.uint32),
    }

    controllers = [
        "new120 (Type II)",
        "old120 (Type I)",
        "ST130",
        "ST121",
        "ST138",
        "DC131 (PentaMax)",
        "ST133 (MicroMax/Roper)",
        "ST135 (GPIB)",
        "VTCCD",
        "ST116 (GPIB)",
        "OMA3 (GPIB)",
        "OMA4",
    ]

    # This was gathered from random places on the internet and own experiments
    # with the camera. May not be accurate.
    readout_modes = ["full frame", "frame transfer", "kinetics"]

    # Do not decode the following metadata keys into strings, but leave them
    # as byte arrays
    no_decode = ["spare_4"]


class SpeFormat(Format):
    """ Some CCD camera software produces images in the Princeton Instruments
    SPE file format. This plugin supports reading such files.

    Parameters for reading
    ----------------------
    char_encoding : str
        Character encoding used to decode strings in the metadata. Defaults
        to "latin1".
    check_filesize : bool
        The number of frames in the file is stored in the file header. However,
        this number may be wrong for certain software. If this is `True`
        (default), derive the number of frames also from the file size and
        raise a warning if the two values do not match.

    Metadata for reading
    --------------------
    ROIs : list of dict
        Regions of interest used for recording images. Each dict has the
        "top_left" key containing x and y coordinates of the top left corner,
        the "bottom_right" key with x and y coordinates of the bottom right
        corner, and the "bin" key with number of binned pixels in x and y
        directions.
    comments : list of str
        The SPE format allows for 5 comment strings of 80 characters each.
    controller_version : int
        Hardware version
    logic_output : int
        Definition of output BNC
    amp_hi_cap_low_noise : int
        Amp switching mode
    mode : int
        Timing mode
    exp_sec : float
        Alternative exposure in seconds
    date : str
        Date string
    detector_temp : float
        Detector temperature
    detector_type : int
        CCD / diode array type
    st_diode : int
        Trigger diode
    delay_time : float
        Used with async mode
    shutter_control : int
        Normal, disabled open, or disabled closed
    absorb_live : bool
        on / off
    absorb_mode : int
        Reference strip or file
    can_do_virtual_chip : bool
        True or False whether chip can do virtual chip
    threshold_min_live : bool
        on / off
    threshold_min_val : float
        Threshold minimum value
    threshold_max_live : bool
        on / off
    threshold_max_val : float
        Threshold maximum value
    time_local : str
        Experiment local time
    time_utc : str
        Experiment UTC time
    adc_offset : int
        ADC offset
    adc_rate : int
        ADC rate
    adc_type : int
        ADC type
    adc_resolution : int
        ADC resolution
    adc_bit_adjust : int
        ADC bit adjust
    gain : int
        gain
    sw_version : str
        Version of software which created this file
    spare_4 : bytes
        Reserved space
    readout_time : float
        Experiment readout time
    type : str
        Controller type
    clockspeed_us : float
        Vertical clock speed in microseconds
    readout_mode : {"full frame", "frame transfer", "kinetics", ""}
        Readout mode. Empty string means that this was not set by the
        Software.
    window_size : int
        Window size for Kinetics mode
    file_header_ver : float
        File header version
    chip_size : [int, int]
        x and y dimensions of the camera chip
    virt_chip_size : [int, int]
        Virtual chip x and y dimensions
    pre_pixels : [int, int]
        Pre pixels in x and y dimensions
    post_pixels : [int, int],
        Post pixels in x and y dimensions
    geometric : list of {"rotate", "reverse", "flip"}
        Geometric operations
    """

    def _can_read(self, request):
        return (
            request.mode[1] in self.modes + "?" and request.extension in self.extensions
        )

    def _can_write(self, request):
        return False

    class Reader(Format.Reader):
        def _open(self, char_encoding="latin1", check_filesize=True):
            self._file = self.request.get_file()
            self._char_encoding = char_encoding

            info = self._parse_header(Spec.basic)
            self._file_header_ver = info["file_header_ver"]
            self._dtype = Spec.dtypes[info["datatype"]]
            self._shape = (info["ydim"], info["xdim"])
            self._len = info["NumFrames"]

            if check_filesize:
                # Some software writes incorrect `NumFrames` metadata.
                # To determine the number of frames, check the size of the data
                # segment -- until the end of the file for SPE<3, until the
                # xml footer for SPE>=3.
                data_end = (
                    info["xml_footer_offset"]
                    if info["file_header_ver"] >= 3
                    else os.path.getsize(self.request.get_local_filename())
                )
                l = data_end - Spec.data_start
                l //= self._shape[0] * self._shape[1] * self._dtype.itemsize
                if l != self._len:
                    logger.warning(
                        "The file header of %s claims there are %s frames, "
                        "but there are actually %s frames.",
                        self.request.filename,
                        self._len,
                        l,
                    )
                    self._len = min(l, self._len)

            self._meta = None

        def _get_meta_data(self, index):
            if self._meta is None:
                if self._file_header_ver < 3:
                    self._init_meta_data_pre_v3()
                else:
                    self._init_meta_data_post_v3()
            return self._meta

        def _close(self):
            # The file should be closed by `self.request`
            pass

        def _init_meta_data_pre_v3(self):
            self._meta = self._parse_header(Spec.metadata)

            nr = self._meta.pop("NumROI", None)
            nr = 1 if nr < 1 else nr
            self._meta["ROIs"] = roi_array_to_dict(self._meta["ROIs"][:nr])

            # chip sizes
            self._meta["chip_size"] = [
                self._meta.pop("xDimDet", None),
                self._meta.pop("yDimDet", None),
            ]
            self._meta["virt_chip_size"] = [
                self._meta.pop("VChipXdim", None),
                self._meta.pop("VChipYdim", None),
            ]
            self._meta["pre_pixels"] = [
                self._meta.pop("XPrePixels", None),
                self._meta.pop("YPrePixels", None),
            ]
            self._meta["post_pixels"] = [
                self._meta.pop("XPostPixels", None),
                self._meta.pop("YPostPixels", None),
            ]

            # comments
            self._meta["comments"] = [str(c) for c in self._meta["comments"]]

            # geometric operations
            g = []
            f = self._meta.pop("geometric", 0)
            if f & 1:
                g.append("rotate")
            if f & 2:
                g.append("reverse")
            if f & 4:
                g.append("flip")
            self._meta["geometric"] = g

            # Make some additional information more human-readable
            t = self._meta["type"]
            if 1 <= t <= len(Spec.controllers):
                self._meta["type"] = Spec.controllers[t - 1]
            else:
                self._meta["type"] = ""
            m = self._meta["readout_mode"]
            if 1 <= m <= len(Spec.readout_modes):
                self._meta["readout_mode"] = Spec.readout_modes[m - 1]
            else:
                self._meta["readout_mode"] = ""

            # bools
            for k in (
                "absorb_live",
                "can_do_virtual_chip",
                "threshold_min_live",
                "threshold_max_live",
            ):
                self._meta[k] = bool(self._meta[k])

            # frame shape
            self._meta["frame_shape"] = self._shape

        def _parse_header(self, spec):
            ret = {}
            # Decode each string from the numpy array read by np.fromfile
            decode = np.vectorize(lambda x: x.decode(self._char_encoding))

            for name, sp in spec.items():
                self._file.seek(sp[0])
                cnt = 1 if len(sp) < 3 else sp[2]
                v = np.fromfile(self._file, dtype=sp[1], count=cnt)
                if v.dtype.kind == "S" and name not in Spec.no_decode:
                    # Silently ignore string decoding failures
                    try:
                        v = decode(v)
                    except Exception:
                        logger.warning(
                            'Failed to decode "{}" metadata '
                            "string. Check `char_encoding` "
                            "parameter.".format(name)
                        )

                try:
                    # For convenience, if the array contains only one single
                    # entry, return this entry itself.
                    v = v.item()
                except ValueError:
                    v = np.squeeze(v)
                ret[name] = v
            return ret

        def _init_meta_data_post_v3(self):
            info = self._parse_header(Spec.basic)
            self._file.seek(info["xml_footer_offset"])
            xml = self._file.read()
            self._meta = {"__xml": xml}

        def _get_length(self):
            if self.request.mode[1] in "vV":
                return 1
            else:
                return self._len

        def _get_data(self, index):
            if index < 0:
                raise IndexError("Image index %i < 0" % index)
            if index >= self._len:
                raise IndexError("Image index %i > %i" % (index, self._len))

            if self.request.mode[1] in "vV":
                if index != 0:
                    raise IndexError("Index has to be 0 in v and V modes")
                self._file.seek(Spec.data_start)
                data = np.fromfile(
                    self._file,
                    dtype=self._dtype,
                    count=self._shape[0] * self._shape[1] * self._len,
                )
                data = data.reshape((self._len,) + self._shape)
            else:
                self._file.seek(
                    Spec.data_start
                    + index * self._shape[0] * self._shape[1] * self._dtype.itemsize
                )
                data = np.fromfile(
                    self._file, dtype=self._dtype, count=self._shape[0] * self._shape[1]
                )
                data = data.reshape(self._shape)
            return data, self._get_meta_data(index)


def roi_array_to_dict(a):
    """Convert the `ROIs` structured arrays to :py:class:`dict`

    Parameters
    ----------
    a : numpy.ndarray
        Structured array containing ROI data

    Returns
    -------
    list of dict
        One dict per ROI. Keys are "top_left", "bottom_right", and "bin",
        values are tuples whose first element is the x axis value and the
        second element is the y axis value.
    """
    l = []
    a = a[["startx", "starty", "endx", "endy", "groupx", "groupy"]]
    for sx, sy, ex, ey, gx, gy in a:
        d = {
            "top_left": [int(sx), int(sy)],
            "bottom_right": [int(ex), int(ey)],
            "bin": [int(gx), int(gy)],
        }
        l.append(d)
    return l


fmt = SpeFormat("spe", "SPE file format", ".spe", "iIvV")
formats.add_format(fmt, overwrite=True)
