import framel
import numpy as np
import ctypes
import sys
if sys.version_info[0] > 2 and sys.version_info[1] > 6:
    from collections.abc import Sequence
else:
    from collections import Sequence

MAX_VECT_DIMS = 5


def frgetvect(filename, channel, start=-1, span=-1, verbose=False):
    """ Reads a vector from a GWF file to a numpy array.

    Parameters
    ----------
    filename : `str`
        Source of data either frame file or FFL.

    channel : `str`
        Channel name to read from the source.

    start : `float`, optional
        GPS start time of the data, a value <=0 reads it from the frame.

    span : `float`, optional
        GPS span of the data, a value <=0 reads it from the frame.

    verbose : `bool`, optional
        Provide additional details of what is being done.

    Returns
    -------

    vect : `numpy.ndarray`
        The requested channel data as a numpy array.
    start : `float`
        GPS start time of the data.
    startX : `numpy.ndarray`
        x-axes start values as a tuple of floats (for time-series, this is
          generally an offset from the GPS start time).
    dx : `numpy.ndarray`
        x-axes spacings as a numpy array of floats.
    unitX : `tuple`
        Units of x-axes as a tuple of strings.
    unitY : `str`
        Unit of y-axis as a string
    """

    # Set the verbosity of the framel library
    framel.FrLibSetLvl(verbose)

    # Open up the frame file
    iFile = framel.FrFileINew(filename)

    # Print what we are doing
    if verbose:
        print(
            f"Opening {filename} for channel {channel} (start={start:.2f}, "
            f"span={span:.2f})."
        )

    # Check if the file was open
    if not iFile:
        print(framel.FrErrorGetHistory())

    if verbose:
        print(f"Opening {filename}")

    # Check if the start needs to be read from the frame
    if start == -1:
        start = framel.FrFileITStart(iFile)

    # Check if the span needs to be read from the frame
    if span == -1:
        span = framel.FrFileITEnd(iFile) - start

    # Get the channel from the frame
    vect = framel.FrFileIGetVect(iFile, channel, start, span)

    # Dump the frame to stdout
    if verbose:
        framel.FrVectDump(vect, framel.stdout, verbose)

    if not vect:
        # Try to open it as StaticData
        # Here I'd like to do
        #   sd = FrStatDataReadT(iFile, channel, start);
        # but FrStatDataReadT does *not* return samples after
        # "start". Doh. Instead, I have to do this:
        frstart = framel.FrFileITStart(iFile)
        sd = framel.FrStatDataReadT(iFile, channel, frstart)

        # Dump the channel stats
        if verbose:
            framel.FrStatDataDump(sd, framel.stdout, verbose)

        # Check the stat reading was okay
        if not sd:
            print(f"In file {filename}, vector not found {channel}")
            framel.FrFileIEnd(iFile)

        # Check if the next stat is a null vector
        if sd.contents.next:
            print(
                f"In file {filename}, staticData channel {channel} has "
                "next!=NULL. Freaking out"
            )
            framel.FrFileIEnd(iFile)
            return None

        # Extract the stat data
        if sd.contents.data:
            vect = sd.contents.data
        else:
            print(
                f"In file {filename}, staticData channel {channel} has no "
                "vector. Freaking out."
            )
            framel.FrFileIEnd(iFile)
            return None

        # Check if the stat data has multiple dimensions
        if vect.contents.nDim != 1:
            print(
                f"In file {filename}, staticData channel {channel} has "
                "multiple dimensions. Freaking out."
            )
            framel.FrFileIEnd(iFile)
            return None

        # Recompute limits and pointers, so "vect" contains only the
        # subset of data we requested
        if (vect.contents.nData > span / vect.contents.dx[0]):
            vect.contents.nx[0] = int(span / vect.contents.dx[0])
            vect.contents.nData = vect.contents.nx[0]

        # Check if we need to reset the shift of the data
        if (frstart < start):
            shift = int((start - frstart) / vect.contents.dx[0])
            if (vect.contents.type == framel.FR_VECT_2S):
                void_p = ctypes.cast(
                    vect.contents.dataS, ctypes.c_voidp
                ).value + shift*ctypes.sizeof(vect.contents.dataS._type_)
                vect.contents.dataS = ctypes.cast(
                    void_p, ctypes.POINTER(ctypes.c_short)
                )
            elif (vect.contents.type == framel.FR_VECT_4S):
                void_p = ctypes.cast(
                    vect.contents.dataI, ctypes.c_voidp
                ).value + shift*ctypes.sizeof(vect.contents.dataI._type_)
                vect.contents.dataI = ctypes.cast(
                    void_p, ctypes.POINTER(ctypes.c_int)
                )
            elif (vect.contents.type == framel.FR_VECT_8S):
                void_p = ctypes.cast(
                    vect.contents.dataL, ctypes.c_voidp
                ).value + shift*ctypes.sizeof(vect.contents.dataL._type_)
                vect.contents.dataL = ctypes.cast(
                    void_p, ctypes.POINTER(framel.FRLONG)
                )
            elif (vect.contents.type == framel.FR_VECT_1U):
                void_p = ctypes.cast(
                    vect.contents.dataU, ctypes.c_voidp
                ).value + shift*ctypes.sizeof(vect.contents.dataU._type_)
                vect.contents.dataU = ctypes.cast(
                    void_p, ctypes.POINTER(framel.c_ubyte)
                )
            elif (vect.contents.type == framel.FR_VECT_2U):
                void_p = ctypes.cast(
                    vect.contents.dataUS, ctypes.c_voidp
                ).value + shift*ctypes.sizeof(vect.contents.dataUS._type_)
                vect.contents.dataUS = ctypes.cast(
                    void_p, ctypes.POINTER(framel.c_ushort)
                )
            elif (vect.contents.type == framel.FR_VECT_4U):
                void_p = ctypes.cast(
                    vect.contents.dataUI, ctypes.c_voidp
                ).value + shift*ctypes.sizeof(vect.contents.dataUI._type_)
                vect.contents.dataUI = ctypes.cast(
                    void_p, ctypes.POINTER(framel.c_uint)
                )
            elif (vect.contents.type == framel.FR_VECT_8U):
                void_p = ctypes.cast(
                    vect.contents.dataUL, ctypes.c_voidp
                ).value + shift*ctypes.sizeof(vect.contents.dataUL._type_)
                vect.contents.dataUL = ctypes.cast(
                    void_p, ctypes.POINTER(framel.FRULONG)
                )
            elif (vect.contents.type == framel.FR_VECT_4R):
                void_p = ctypes.cast(
                   vect.contents.dataF, ctypes.c_voidp
                ).value + shift*ctypes.sizeof(vect.contents.dataF._type_)

                vect.contents.dataF = ctypes.cast(
                 void_p, ctypes.POINTER(ctypes.c_float)
                )
            elif (vect.contents.type == framel.FR_VECT_8R):
                void_p = ctypes.cast(
                    vect.contents.dataD, ctypes.c_voidp
                ).value + shift*ctypes.sizeof(vect.contents.dataD._type_)
                vect.contents.dataD = ctypes.cast(
                    void_p, ctypes.POINTER(ctypes.c_double)
                )
            # Note the 2* shift for complex types
            elif (vect.contents.type == framel.FR_VECT_8C):
                void_p = ctypes.cast(
                    vect.contents.dataF, ctypes.c_voidp
                ).value + shift*ctypes.sizeof(vect.contents.dataF._type_)
                vect.contents.dataF = ctypes.cast(
                    void_p, ctypes.POINTER(framel.c_float)
                )
            elif (vect.contents.type == framel.FR_VECT_16C):
                void_p = ctypes.cast(
                    vect.contents.dataD, ctypes.c_voidp
                ).value + shift*ctypes.sizeof(vect.contents.dataD._type_)
                vect.contents.dataD = ctypes.cast(
                    void_p, ctypes.POINTER(framel.c_double)
                )
            # If none of these types, it will fail later

    if verbose:
        print(f"Extracted channel {channel} successfully!")

    # Extract the number of data and dimensions
    nDim = vect.contents.nDim

    # Get the chane for each vector dimension
    shape = np.zeros(nDim, dtype=np.int64)
    for i in range(nDim):
        shape[i] = vect.contents.nx[i]

    # Both FrVect and Numpy store data in C array order (vs Fortran)
    if (vect.contents.type == framel.FR_VECT_2S):
        outVect = np.empty(shape, dtype=np.int16)
        ctypes.memmove(
            outVect.ctypes.data, vect.contents.dataS, outVect.nbytes
        )
    elif (vect.contents.type == framel.FR_VECT_4S):
        outVect = np.empty(shape, dtype=np.int32)
        ctypes.memmove(
            outVect.ctypes.data, vect.contents.dataI, outVect.nbytes
            )
    elif (vect.contents.type == framel.FR_VECT_8S):
        outVect = np.empty(shape, dtype=np.int64)
        ctypes.memmove(
            outVect.ctypes.data, vect.contents.dataL, outVect.nbytes
        )
    elif (vect.contents.type == framel.FR_VECT_1U):
        outVect = np.empty(shape, dtype=np.uint8)
        ctypes.memmove(
            outVect.ctypes.data, vect.contents.dataU, outVect.nbytes
        )
    elif (vect.contents.type == framel.FR_VECT_2U):
        outVect = np.empty(shape, dtype=np.uint16)
        ctypes.memmove(
            outVect.ctypes.data, vect.contents.dataUS, outVect.nbytes
        )
    elif (vect.contents.type == framel.FR_VECT_4U):
        outVect = np.empty(shape, dtype=np.uint32)
        ctypes.memmove(
            outVect.ctypes.data, vect.contents.dataUI, outVect.nbytes
        )
    elif (vect.contents.type == framel.FR_VECT_8U):
        outVect = np.empty(shape, dtype=np.uint64)
        ctypes.memmove(
            outVect.ctypes.data, vect.contents.dataUL, outVect.nbytes
        )
    elif (vect.contents.type == framel.FR_VECT_4R):
        outVect = np.empty(shape, dtype=np.float32)
        ctypes.memmove(
            outVect.ctypes.data, vect.contents.dataF, outVect.nbytes
        )
    elif (vect.contents.type == framel.FR_VECT_8R):
        outVect = np.empty(shape, dtype=np.float64)
        ctypes.memmove(
            outVect.ctypes.data, vect.contents.dataD, outVect.nbytes
        )
    elif (vect.contents.type == framel.FR_VECT_8C):
        outVect = np.empty(shape, dtype=np.complex64)
        ctypes.memmove(
            outVect.ctypes.data, vect.contents.dataF, outVect.nbytes
        )
    elif (vect.contents.type == framel.FR_VECT_16C):
        outVect = np.empty(shape, dtype=np.complex128)
        ctypes.memmove(
            outVect.ctypes.data, vect.contents.dataD, outVect.nbytes
        )
    else:
        print(f"Unrecognized vect.contents.type (= {vect.contents.type})")
        framel.FrVectFree(vect)
        framel.FrFileIEnd(iFile)
        return None

    # ------------- other outputs ------------------------
    # output2 = gps start time
    gpsStart = np.float64(vect.contents.GTime)

    # output3 = x-axes start values as a tuple of PyFloats
    # output4 = x-axes spacings as a tuple of PyFloats
    # output5 = x-axes units as a tuple of strings
    startX = np.zeros(nDim)
    dx = np.zeros(nDim)
    unitX = ["" for i in range(nDim)]
    for i in range(nDim):
        startX[i] = vect.contents.startX[i]
        dx[i] = vect.contents.dx[i]
        # Loop the unitX until it reaches a NULL character
        j = 0
        while vect.contents.unitX[i][j] != b"\x00":
            j += 1
        unitX[i] = vect.contents.unitX[i][0:j].decode("utf-8")

    # output6 = unitY as a string
    if vect.contents.unitY:
        unitY = str(vect.contents.unitY)
    else:
        unitY = ""

    # ------------- clean up -----------------------------
    framel.FrVectFree(vect)
    framel.FrFileIEnd(iFile)

    return outVect, gpsStart, startX, dx, unitX, unitY


def frgetvect1d(filename, channel, start=-1, span=-1, verbose=False):
    """Reads a one-dimensional vector from a Fr file to a numpy array.  Will
    raise an exception if invoked on multi-dimensional data.

    Parameters
    ----------
    filename : `str`
        Source of data either frame file or FFL.

    channel : `str`
        Channel name to read from the source.

    start : `float`, optional
        GPS start time of the data, a value <=0 reads it from the frame.

    span : `float`, optional
        GPS span of the data, a value <=0 reads it from the frame.

    verbose : `bool`, optional
        Provide additional details of what is being done.

    Returns
    -------

    vect : `numpy.ndarray`
        The requested channel data as a numpy array.
    start : `float`
        GPS start time of the data.
    startX : `float`
        x-axes start value (for time-series, this is generally an offset from
         the GPS start time).
    dx : `float`
        x-axes spacings as a numpy array of floats.
    unitX : `str`
        Units of x-axes as a tuple of strings.
    unitY : `str`
        Unit of y-axis as a string
    """

    # Call the frgetvect function
    out = frgetvect(filename, channel, start, span, verbose)
    if out is None:
        return None

    # Extract the tuples into their principal value
    outVect = out[0]
    gpsStart = out[1]
    startX = out[2][0]
    dx = out[3][0]
    unitX = out[4][0]
    unitY = out[5]

    return outVect, gpsStart, startX, dx, unitX, unitY


def frputvect(filename, channellist, history="", verbose=False):
    """Write numpy arrays to a frame file.

    Parameters
    ----------
    filename  : `str`
        Name of GWF file to write.
    channellist : `list`
        list of dictionaries with the fields below:
            1) name - channel name - string
            2) data - list of one-dimensional vectors to write
            3) start - lower limit of the x-axis in GPS time or Hz
            4) dx - spacing of x-axis in seconds or Hz
            5) x_unit - unit of x-axis as a string (default = '')
            6) y_unit - unit of y-axis as a string (default = '')
            7) kind - 'PROC', 'ADC', or 'SIM' (default = 'PROC')
            8) type - type of data (default = 1):
                   0 - Unknown/undefined
                   1 - Time series
                   2 - Frequency series
                   3 - Other 1-D series
                   4 - Time-frequency
                   5 - Wavelets
                   6 - Multi-dimensional
            9) subType - sub-type of frequency series (default = 0):
                   0 - Unknown/undefined
                   1 - DFT
                   2 - Amplitude spectral density
                   3 - Power spectral density
                   4 - Cross spectral density
                   5 - Coherence
                   6 - Transfer function
    history : `str`, optional
        history string
    verbose : `bool`
        Provide additional details of what is being done.

    Returns
    ----
    None
    """

    # Set the verbosity of the framel library
    framel.FrLibSetLvl(verbose)

    # create frames, create vectors, and fill them.

    # Channel-list must be any type of sequence
    if not isinstance(channellist, Sequence) and len(channellist) == 0:
        print("channellist is empty")
        return None

    # Get channel name from first dictionary
    framedict = channellist[0]
    if not isinstance(framedict, dict):
        print("First entry of channellist is not a dictionary")
        return None
    channel = framedict["name"]

    # Create the frame
    if verbose:
        print(f"Creating frame {channel}...")
    frame = framel.FrameNew(channel)
    if not frame:
        print(f"FrameNew failed ({framel.FrErrorGetHistory()})")
        return None

    # Loop over the channellist
    if verbose:
        print("Now iterating...")
    for ichanneldict in channellist:

        if verbose:
            print("In loop...")
        # Extract quantities from dict -- all borrowed references
        channel = ichanneldict["name"]
        start = ichanneldict["start"]
        dx = ichanneldict["dx"]
        array = ichanneldict["data"]

        # Check if data is an array
        if not isinstance(array, np.ndarray):
            print("data is not an array")
            framel.FrameFree(frame)
            return None

        # Get the data properties
        nData = array.size
        nBits = array.itemsize
        arrayType = array.dtype.name

        # kind, x_unit, y_unit, type, and subType have default values
        if "kind" in ichanneldict:
            kind = ichanneldict["kind"]
        else:
            kind = "PROC"
        if "x_unit" in ichanneldict:
            x_unit = ichanneldict["x_unit"]
        else:
            x_unit = ""
        if "y_unit" in ichanneldict:
            y_unit = ichanneldict["y_unit"]
        else:
            y_unit = ""
        if "type" in ichanneldict:
            itype = ichanneldict["type"]
        else:
            itype = 1
        if "subType" in ichanneldict:
            subType = ichanneldict["subType"]
        else:
            subType = 0
        if verbose:
            print(
                f"type = {itype}, subType = {subType}, start = {start}, dx = "
                f"{dx}"
            )

        # Set the sample rate as 1/dx
        sampleRate = 1./dx

        if verbose:
            print("Now copying data to vector...")
        # Create empty vector (-typecode ==> empty) with metadata,
        # then copy data to vector
        if (arrayType == "int16"):
            vect = framel.FrVectNew1D(
                channel, -framel.FR_VECT_2S, nData, dx, x_unit, y_unit
            )
            ctypes.memmove(
                vect.contents.dataS, array.ctypes.data, array.nbytes
            )
        elif (arrayType == "int32"):
            vect = framel.FrVectNew1D(
                channel, -framel.FR_VECT_4S, nData, dx, x_unit, y_unit
            )
            ctypes.memmove(
                vect.contents.dataI, array.ctypes.data, array.nbytes
            )
        elif (arrayType == "int64"):
            vect = framel.FrVectNew1D(
                channel, -framel.FR_VECT_8S, nData, dx, x_unit, y_unit
            )
            ctypes.memmove(
                vect.contents.dataL, array.ctypes.data, array.nbytes
            )
        elif (arrayType == "uint8"):
            vect = framel.FrVectNew1D(
                channel, -framel.FR_VECT_1U, nData, dx, x_unit, y_unit
            )
            ctypes.memmove(
                vect.contents.dataU, array.ctypes.data, array.nbytes
            )
        elif (arrayType == "uint16"):
            vect = framel.FrVectNew1D(
                channel, -framel.FR_VECT_2U, nData, dx, x_unit, y_unit
            )
            ctypes.memmove(
                vect.contents.dataUS, array.ctypes.data, array.nbytes
            )
        elif (arrayType == "uint32"):
            vect = framel.FrVectNew1D(
                channel, -framel.FR_VECT_4U, nData, dx, x_unit, y_unit
            )
            ctypes.memmove(
                vect.contents.dataUI, array.ctypes.data, array.nbytes
            )
        elif (arrayType == "uint64"):
            vect = framel.FrVectNew1D(
                channel, -framel.FR_VECT_8U, nData, dx, x_unit, y_unit
            )
            ctypes.memmove(
                vect.contents.dataUL, array.ctypes.data, array.nbytes
            )
        elif (arrayType == "float32"):
            vect = framel.FrVectNew1D(
                channel, -framel.FR_VECT_4R, nData, dx, x_unit, y_unit
            )
            ctypes.memmove(
                vect.contents.dataF, array.ctypes.data, array.nbytes
            )
        elif (arrayType == "float64"):
            vect = framel.FrVectNew1D(
                channel, -framel.FR_VECT_8R, nData, dx, x_unit, y_unit
            )
            ctypes.memmove(
                vect.contents.dataD, array.ctypes.data, array.nbytes
            )
        # FrVects don't have complex pointers.  Numpy stores complex
        # numbers in the same way, but we have to trick it into giving
        # us a (real) float pointer. */
        elif (arrayType == "complex64"):
            vect = framel.FrVectNew1D(
                channel, -framel.FR_VECT_8C, nData, dx, x_unit, y_unit
            )
            arrayview = array.view(np.float32)
            ctypes.memmove(
                vect.contents.dataF, arrayview.ctypes.data, array.nbytes
            )
        elif (arrayType == "complex128"):
            vect = framel.FrVectNew1D(
                channel, -framel.FR_VECT_16C, nData, dx, x_unit, y_unit
            )
            arrayview = array.view(np.float64)
            ctypes.memmove(
                vect.contents.dataD, arrayview.ctypes.data, array.nbytes
            )
        else:
            print("{arrayType} does not have a corresponsing FrVect type")

        # Dump the frame we are writing
        if verbose:
            print("Done copying...")
            framel.FrameDump(frame, framel.stdout, 6)

        # Add to data and attach vector to the data
        if kind == "PROC":
            proc = framel.FrProcDataNew(frame, channel, sampleRate, 1, nBits)
            framel.FrVectFree(proc.contents.data)
            proc.contents.data = vect
            proc.contents.type = itype
            proc.contents.subType = subType
            frame.contents.GTimeS = np.uint32(start)
            frame.contents.GTimeN = np.uint32(
                (start-(frame.contents.GTimeS))*1e9
            )
            if (itype == 1):  # time series
                proc.contents.tRange = nData*dx
                frame.contents.dt = nData*dx
            elif (itype == 2):  # frequency series
                proc.contents.fRange = nData*dx
        elif kind == "ADC":
            adc = framel.FrAdcDataNew(
                frame, channel, sampleRate, 1, nBits
            )
            framel.FrVectFree(adc.contents.data)
            adc.contents.data = vect
            frame.contents.dt = nData*dx
            frame.contents.GTimeS = np.uint32(start)
            frame.contents.GTimeN = np.uint32(
                (start-(frame.contents.GTimeS))*1e9
            )
        elif kind == "SIM":
            sim = framel.FrSimDataNew(frame, channel, sampleRate, 1, nBits)
            framel.FrVectFree(sim.contents.data)
            sim.contents.data = vect
            frame.contents.dt = nData*dx
            frame.contents.GTimeS = np.uint32(start)
            frame.contents.GTimeN = np.uint32(
                (start-(frame.contents.GTimeS))*1e9
            )
        else:
            print(f"Unknown kind: {kind} for {channel}")

        if verbose:
            print("Attached vect to frame.")

    # ------------- Write file -----------------------------
    oFile = framel.FrFileONewH(filename, 1, history)  # 1 ==> gzip contents

    # Check the frame could be opened
    if not oFile:
        print(framel.FrErrorGetHistory())
        framel.FrFileOEnd(oFile)
        return None

    # Write the frame to the file
    if (framel.FrameWrite(frame, oFile) != framel.FR_OK):
        print(framel.FrErrorGetHistory())
        framel.FrFileOEnd(oFile)
        return None

    # The framel.FrFile owns data and vector memory.
    # Do not free them separately.
    framel.FrFileOEnd(oFile)
    framel.FrameFree(frame)


def extract_event_dict(event):
    """
    Utility function to extract an FrEvent's parameters into a Python
    dictionary
    """

    # Each FrEvent will be stored in a dict
    event_dict = dict()

    # guarantee these parameters exist
    event_dict["name"] = str(event.contents.name)
    event_dict["comment"] = str(event.contents.comment or "")
    event_dict["inputs"] = str(event.contents.inputs or "")
    event_dict["GTimeS"] = np.uint64(event.contents.GTimeS)
    event_dict["GTimeN"] = np.uint64(event.contents.GTimeN)
    event_dict["timeBefore"] = float(event.contents.timeBefore)
    event_dict["timeAfter"] = float(event.contents.timeAfter)
    event_dict["eventStatus"] = np.uint64(event.contents.eventStatus)
    event_dict["amplitude"] = float(event.contents.amplitude)
    event_dict["probability"] = float(event.contents.probability)
    event_dict["statistics"] = str(event.contents.statistics or "")

    # additional parameters
    for i in range(event.contents.nParam):
        event_dict[str(event.contents.parameterNames[i])] = \
            float(event.contents.parameters[i])

    return event_dict


def frgetevent(filename, verbose):
    """Extract the FrEvents from a given frame file

    Parameters
    ----------
    filename : `str`
        Source of data either frame file or FFL.
    verbose : `bool`
        Provide additional details of what is being done.

    Returns
    -------
    event_list : `list` of `dicts`
        list of dicts, with each dict representing an FrEvent's fields
        and values.
    """
    framel.FrLibSetLvl(verbose)

    # -------------- set up file for reading --------------

    iFile = framel.FrFileINew(filename)
    if iFile is None:
        print("%s" % framel.FrErrorGetHistory())
        return None

    # require at least one frame in the file
    frame = framel.FrameRead(iFile)
    if frame is None:
        print("%s" % framel.FrErrorGetHistory())
        framel.FrFileIEnd(iFile)
        return None

    # ------ iterate, putting each event into output list ------
    event_list = []

    # Loop over all the events and add them to the list
    while frame:
        event = frame.contents.event
        while event:
            event_dict = extract_event_dict(event)
            event_list.append(event_dict)
            event = event.contents.next
        framel.FrameFree(frame)
        frame = framel.FrameRead(iFile)

    # -------------- clean up and return --------------
    framel.FrFileIEnd(iFile)
    return event_list
