import multipletau
import matplotlib.pyplot as plt
import numpy as np
import re
import os
import ntpath
import scipy as spy
from pathlib import Path
import fnmatch
from typing import List

from .extract_spad_data import extract_spad_data
from .distance2detelements import distance2detelements, spad_shift_vector_crosscorr
from .distance2detelements import spad_coord_from_det_numb as coord
from .get_fcs_info import get_file_info, get_metafile_from_file
from .meas_to_count import file_to_fcs_count, czi2h5
from .atimes2corr import atimes_2_corr
from .timetrace2corr import tt2corr
from .corr2csv import corr2csv

from ..tools.plot_colors import plot_colors
from ..tools.list_files import list_files
from ..tools.bindata import bindata_chunks
from ..tools.calcdist_from_coord import list_of_pixel_pairs_at_distance

"""
A correlations class object is returned whenever a correlations are calculated.
E.g. G, time_trace = fcs_load_and_corr_split(file,...)
The object contains fields such as G.central_average, containing a 2D array
with the autocorrelation for the central element (averaged over all chunks of data)
"""

class Correlations:
    def __init__(self):
        pass
    
    def get_av_corrs(self, corrs, av='_averageX'):
        # return all average correlations
        # e.g. G.get_av_corrs(['central', 'sum3', 'sum5']) returns
        # G.central_averageX, G.sum3_averageX, etc. in a single array
        g_shape = np.shape(getattr(self, corrs[0] + av))
        Garray = np.zeros((g_shape[0],len(corrs)))
        Gstd = np.zeros((g_shape[0],len(corrs))) + 1
        for i, corr in enumerate(corrs):
            Garray[:,i] = getattr(self, corr + av)[:,1]
            try:
                Gstd[:,i] = getattr(self, corr + av)[:,2]
            except:
                pass
        tau = getattr(self, corr + av)[:,0]
        Garray = np.squeeze(Garray)
        Gstd = np.squeeze(Gstd)
        return Garray, tau, Gstd
    
    def get_corrs(self, corrname):
        # return all correlations starting with corrname (except average)
        # e.g. G.sum3_chunk0, G.sum3_chunk1, etc.
        keylist = list(self.__dict__.keys())
        good_keys = []
        for ind, key in enumerate(keylist):
            if key.startswith(corrname + '_chunk'):
                good_keys.append(key)
        
        if len(good_keys) == 0:
            return None, None
        
        g_shape = np.shape(getattr(self, good_keys[0]))
        Garray = np.zeros((g_shape[0],len(good_keys)))
        for i in range(len(good_keys)):
            Garray[:,i] = getattr(self, corrname + '_chunk' + str(i))[:,1]
        tau = getattr(self, corrname + '_chunk' + str(i))[:,0]
        return Garray, tau

    @property
    def num_chunks(self):
        """
        return number of chunks
        """
        Gfields = list(self.__dict__.keys())
        t = [Gfields[i].split("_chunk")[0] for i in range(len(Gfields))]
        t = list(dict.fromkeys(t))
        try:
            t.remove("dwellTime")
        except:
            pass
        for field in t:
            avList = [i for i in Gfields if i.startswith(field + '_chunk')]
            if len(avList) > 0:
                return len(avList)
    
    @property
    def list_of_g_out(self):
        """
        return names of output correlations, e.g. ['central', 'sum7', 'sum19']
        """
        listOfCorr = list(self.__dict__.keys())
        listOfCorr2 = []
        
        for corr in listOfCorr:
            if 'average' not in corr and 'chunk' in corr and 'chunksOff' not in corr and 'chunks_off' not in corr:
                pos = corr.find('_chunk')
                if len(corr[0:pos]) > 0:
                    listOfCorr2.append(corr[0:pos])
        
        # remove duplicates
        listOfCorr2 = list(dict.fromkeys(listOfCorr2))
        return listOfCorr2
    
    @property
    def list_of_g_out_filters(self):
        """
        Returns:
          - unique base names, where a trailing 'F<number>' is ignored
          - the maximum trailing F tag as a string like 'F2' (or None if no F tags)
        """
        names = self.list_of_g_out
        
        base_pattern = re.compile(r"^(.*?)(?:F(\d+))?$")  # base + optional trailing F#
        bases: List[str] = []
        seen = set()
        max_f = -1
    
        for s in names:
            m = base_pattern.match(s)
            if not m:
                continue
            base, fnum = m.group(1), m.group(2)
    
            if base not in seen:
                seen.add(base)
                bases.append(base)
    
            if fnum is not None:
                max_f = max(max_f, int(fnum))
    
        max_f_tag = max_f if max_f >= 0 else None
        return bases, max_f_tag
    
    def average_chunks(self, listOfChunks):
        """
        identical to G = fcs_av_chunks(G, listOfChunks)
        """
        listOfCorr2 = self.list_of_g_out
        
        for corr in listOfCorr2:
            Gtemp = getattr(self, corr + '_chunk0') * 0
            GtempSquared = getattr(self, corr + '_chunk0') * 0
            for chunk in listOfChunks:
                Gtemp += getattr(self, corr + '_chunk' + str(chunk))
                GtempSquared += getattr(self, corr + '_chunk' + str(chunk))**2
            
            Gtemp /= len(listOfChunks)
            Gstd = np.sqrt(np.clip(GtempSquared / len(listOfChunks) - Gtemp**2, 0, None))
            
            Gtot = np.zeros((np.shape(Gtemp)[0], np.shape(Gtemp)[1] + 1))
            Gtot[:, 0:-1] = Gtemp # [time, average]
            Gtot[:, -1] = Gstd[:,1] # standard deviation
            
            setattr(self, corr + '_averageX', Gtot)

    
def fcs_load_and_corr_split(fname, list_of_g=['central', 'sum3', 'sum5'], accuracy=16, split=10, binsize=1, time_trace=False, metadata=None, root=0, list_of_g_out=None, averaging=None, algorithm='multipletau', print_info=True):
    """
    Load data from a file in chunks and calculate correlations.

    Parameters
    ----------
    fname : string
        File name.
    list_of_g : list, optional
        List of correlations to be calculated.
        The default is ['central', 'sum3', 'sum5'].
    accuracy : int, optional
        Accuracy of the autocorrelation function. The default is 16.
    split : float, optional
        Number of traces to split the data into
        E.g. split=10 will divide a 60 second stream in 10 six second
        traces and calculate G for each individual trace. The default is 10.
    binsize : int, optional
        Bin the data in bins of size binsize, only used for pch analysis
    time_trace : boolean, optional
        see output. The default is False.
    metadata : None or object, optional
        if None: metadata is extracted from .txt info file
        if metadata Object: meta is given as input parameter. The default is None.
    root : int, optional
        used for GUI only to pass progress. The default is 0.
    averaging : list of strings
        used to average cross correlations, e.g.
        averaging = ['14x12+15x3', '10x12+9x3', '12x14+3x15']
        averages the xcorr between ch14x12 and ch15x13 and saves them
        in a field with name from "list_of_g_out". THe length of averaging list
        and list_of_g_out must be the same
    list_of_g_out : list of strings, optional
        used for GUI only. The default is None.
    algorithm : string
        Which algorithm to use for calculating G:
            "multipletau": calculate G in time domain
            "wiener-khinchin": calculate G using fft

    Returns
    -------
    G : object
        Object with all autocorrelations
        E.g. G.central contains the array with the central detector
        element autocorrelation.
    data : np.array()
        if time_trace == False: last chunk of raw data
        if time_trace == True: full time trace binned: [Nx25] array.

    """
    
    if fname.endswith(".czi"):
        fname_h5 = fname[:-4] + '.h5'
        if not os.path.exists(fname_h5):
            print('converting czi file')
            fname = czi2h5(fname)
    
    if metadata is None:
        metafile = get_metafile_from_file(fname)
        metadata = get_file_info(metafile)
    
    try:
        dwellTime = 1e-6 * metadata.timeResolution # s
    except:
        dwellTime = 1e-6 * metadata.time_resolution # s
    duration = metadata.duration
    
    N = int(np.floor(duration / split)) # number of chunks
    
    if N == 0:
        return [None, None]
    
    G = Correlations()
    G.dwellTime = dwellTime
    chunkSize = int(np.floor(split / dwellTime))
    for chunk in range(N):
        # --------------------- CALCULATE CORRELATIONS SINGLE CHUNK ---------------------
        if root != 0:
            root.progress = chunk / N
        string2print = "| Loading chunk " + str(chunk+1) + "/" + str(N) + " |"
        stringL = len(string2print) - 2
        if print_info:
            print("+" + "-"*stringL + "+")
            print(string2print)
            print("+" + "-"*stringL + "+")
        data = file_to_fcs_count(fname, np.uint8, chunkSize, chunk*chunkSize)
        if time_trace == True:
            binSize_tt = int(chunkSize / 1000 * N) # time trace binned to 1000 data points
            bdata = bindata_chunks(data, binSize_tt)
            bdataL = len(bdata)
            num_ch = np.shape(bdata)[1]
            if chunk == 0:
                timetraceAll = np.zeros((1000, num_ch), dtype=int)
            timetraceAll[chunk*bdataL:(chunk+1)*bdataL, :] = bdata 
        
        indk = 0
        for ind, j in enumerate(list_of_g):
            if print_info:
                print('     --> ' + str(j) + ": ", end = '')
            # ------------------ CHUNK ------------------
            newList = [j]
            Gsplit = fcs2corr(data, 1e6*dwellTime, newList, accuracy, binsize, averaging=averaging, list_of_g_out=list_of_g_out, algorithm=algorithm)
            GsplitList = list(Gsplit.__dict__.keys())
            for k in GsplitList:
                if k.find('dwellTime') == -1:
                    attrname = k
                    if list_of_g_out is not None:
                        attrname = list_of_g_out[indk]
                        indk += 1
                    if not np.isnan(getattr(Gsplit, k)).any():
                        setattr(G, attrname + '_chunk' + str(chunk), getattr(Gsplit, k))
    
    # ---------- CALCULATE AVERAGE CORRELATION OF ALL CHUNKS ----------
    # Get list of "root" names, i.e. without "_chunk"
    Gfields = list(G.__dict__.keys())
    t = [Gfields[i].split("_chunk")[0] for i in range(len(Gfields))]
    t = list(dict.fromkeys(t))
    t.remove("dwellTime")
    # average over chunks
    for field in t:
        avList = [i for i in Gfields if i.startswith(field + '_chunk')]
        # check if all elements have same dimension
        Ntau = [len(getattr(G, i)) for i in avList]
        avList2 = [avList[i] for i in range(len(avList)) if Ntau[i] == Ntau[0]]
        
        Gtemp = getattr(G, avList2[0]) * 0
        GtempSquared = getattr(G, avList2[0])**2 * 0
        for chunk in avList2:
            Gtemp += getattr(G, chunk)
            GtempSquared += getattr(G, chunk)**2
        
        Gtemp /= len(avList2)
        Gstd = np.sqrt(np.clip(GtempSquared / len(avList2) - Gtemp**2, 0, None))
        
        Gtot = np.zeros((np.shape(Gtemp)[0], np.shape(Gtemp)[1] + 1))
        Gtot[:, 0:-1] = Gtemp # [time, average]
        Gtot[:, -1] = Gstd[:,1] # standard deviation
        
        setattr(G, str(field) + '_average', Gtot)   
    
    if time_trace == True:
        data = timetraceAll

    return G, data


def fcs2corr(data, dwell_time, list_of_g=['central', 'sum3', 'sum5', 'chessboard', 'ullr'], accuracy=50, binsize=1, algorithm='multipletau', averaging=None, list_of_g_out=None):
    """
    Convert SPAD-fcs data to correlation curves
    Function used for most of the correlations calculations.
    
    Parameters
    ----------
    data : np.array(Nt x Nc)
        Data variable, i.e. output from binfile2data.
        Nt: number of time points
        Nc: number of channels (typically 25 for a 5x5 detector)
    dwell_time : float
        Bin time [in µs].
    list_of_g : list, optional
        List of correlations to be calculated.
        The default is ['central', 'sum3', 'sum5', 'chessboard', 'ullr'].
    accuracy : int, optional
        Accuracy of the autocorrelation function. The default is 50.

    Returns
    -------
    G : object
        Object with all autocorrelations
        E.g. G.central contains the array with the central detector
        element autocorrelation.

    """
    
    # object from correlations class in which all correlation data is stored
    G = Correlations()
    
    # dwell time
    G.dwellTime = dwell_time
    
    # list_of_g_out index
    if list_of_g_out is not None:
        list_of_g_out_enum = enumerate(list_of_g_out)

    if len(np.shape(data)) == 1:
        # vector is given instead of matrix, single detector only
        print('Calculating autocorrelation ')
        if list_of_g_out is not None:
            attrname = next(list_of_g_out_enum)[1]
        else:
            attrname = 'det0'
        setattr(G, attrname, correlate(data, data, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
        return G

    num_ch = np.shape(data)[1]
    for i in list_of_g:
        if isinstance(i, int):
            # ----------------------------------------------------------------
            # autocorrelation of a detector element i
            # ----------------------------------------------------------------
            if list_of_g_out is not None:
                attrname = next(list_of_g_out_enum)[1]
            else:
                attrname = 'det' + str(i)
            print('Calculating autocorrelation of detector element ' + str(i))
            dataSingle = extract_spad_data(data, i)
            setattr(G, attrname, correlate(dataSingle, dataSingle, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))

        elif i[0] == 'x':
            # ----------------------------------------------------------------
            # cross correlation two detector elements: e.g. 'x0112' for det1 x det12
            # ----------------------------------------------------------------
            if list_of_g_out is not None:
                attrname = next(list_of_g_out_enum)[1]
            else:
                attrname = i
            det0 = int(i[1:3])
            det1 = int(i[3:5])
            dataSingle0 = extract_spad_data(data, det0)
            dataSingle1 = extract_spad_data(data, det1)
            print('Calculating cross-correlation detector elements ' + str(det0) + 'x' + str(det1))
            Gtemp = correlate(dataSingle0, dataSingle1, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm)
            setattr(G, attrname, Gtemp)

        elif i[0] == "C":
            # ----------------------------------------------------------------
            # crosscorrelation custom sum of channels
            # ----------------------------------------------------------------
            if list_of_g_out is not None:
                attrname = next(list_of_g_out_enum)[1]
            else:
                attrname = i
            xpos = np.max([i.find('X'), i.find('x')])
            if xpos > -1:
                print('Calculating crosscorrelation custom sum')
                dataSum1 = extract_spad_data(data, i[0:xpos])
                dataSum2 = extract_spad_data(data, 'C' + i[xpos+1:])
            else:
                print('Calculating autocorrelation custom sum')
                dataSum1 = extract_spad_data(data, i)
                dataSum2 = dataSum1
            setattr(G, attrname, correlate(dataSum1, dataSum2, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
        
        # elif i == "central":
        #     # ----------------------------------------------------------------
        #     # autocorrelation central detector element
        #     # ----------------------------------------------------------------
        #     if list_of_g_out is not None:
        #         attrname = next(list_of_g_out_enum)[1]
        #     else:
        #         attrname = i
        #     print('Calculating autocorrelation central detector element')
        #     dataCentral = extract_spad_data(data, "central")
        #     setattr(G, attrname, correlate(dataCentral, dataCentral, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))

        # elif i == "sum3":
        #     # ----------------------------------------------------------------
        #     # autocorrelation sum3x3
        #     # ----------------------------------------------------------------
        #     if list_of_g_out is not None:
        #         attrname = next(list_of_g_out_enum)[1]
        #     else:
        #         attrname = i
        #     print('Calculating autocorrelation sum3x3')
        #     dataSum3 = extract_spad_data(data, "sum3")
        #     setattr(G, attrname, correlate(dataSum3, dataSum3, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))

        # elif i == "sum5":
        #     # ----------------------------------------------------------------
        #     # autocorrelation sum5x5
        #     # ----------------------------------------------------------------
        #     if list_of_g_out is not None:
        #         attrname = next(list_of_g_out_enum)[1]
        #     else:
        #         attrname = i
        #     print('Calculating autocorrelation sum5x5')
        #     dataSum5 = extract_spad_data(data, "sum5")
        #     setattr(G, attrname, correlate(dataSum5, dataSum5, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
            
        # elif i == "allbuthot":
        #     # ----------------------------------------------------------------
        #     # autocorrelation sum5x5 except for the hot pixels
        #     # ----------------------------------------------------------------
        #     if list_of_g_out is not None:
        #         attrname = next(list_of_g_out_enum)[1]
        #     else:
        #         attrname = i
        #     print('Calculating autocorrelation allbuthot')
        #     dataAllbuthot = extract_spad_data(data, "allbuthot")
        #     setattr(G, attrname, correlate(dataAllbuthot, dataAllbuthot, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))

        # elif i == "chessboard":
        #     # ----------------------------------------------------------------
        #     # crosscorrelation chessboard
        #     # ----------------------------------------------------------------
        #     if list_of_g_out is not None:
        #         attrname = next(list_of_g_out_enum)[1]
        #     else:
        #         attrname = i
        #     print('Calculating crosscorrelation chessboard')
        #     dataChess0 = extract_spad_data(data, "chess0")
        #     dataChess1 = extract_spad_data(data, "chess1")
        #     setattr(G, attrname, correlate(dataChess0, dataChess1, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
            
        # elif i == "chess3":
        #     # ----------------------------------------------------------------
        #     # crosscorrelation small 3x3 chessboard
        #     # ----------------------------------------------------------------
        #     print('Calculating crosscorrelation small chessboard')
        #     if list_of_g_out is not None:
        #         attrname = next(list_of_g_out_enum)[1]
        #     else:
        #         attrname = i
        #     dataChess0 = extract_spad_data(data, "chess3a")
        #     dataChess1 = extract_spad_data(data, "chess3b")
        #     setattr(G, attrname, correlate(dataChess0, dataChess1, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))

        # elif i == "ullr":
        #     # ----------------------------------------------------------------
        #     # crosscorrelation upper left and lower right
        #     # ----------------------------------------------------------------
        #     if list_of_g_out is not None:
        #         attrname = next(list_of_g_out_enum)[1]
        #     else:
        #         attrname = i
        #     print('Calculating crosscorrelation upper left and lower right')
        #     dataUL = extract_spad_data(data, "upper_left")
        #     dataLR = extract_spad_data(data, "lower_right")
        #     setattr(G, attrname, correlate(dataUL, dataLR, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
        
        # elif i == "twofocus":
        #     # ----------------------------------------------------------------
        #     # crosscorrelations sum5left and sum5right, sum5top, and sum5bottom
        #     # ----------------------------------------------------------------
        #     dataL = extract_spad_data(data, "sum5left")
        #     dataR = extract_spad_data(data, "sum5right")
        #     dataT = extract_spad_data(data, "sum5top")
        #     dataB = extract_spad_data(data, "sum5bottom")
            
        #     if list_of_g_out is not None:
        #         attrnames = [next(list_of_g_out_enum)[1] for i in range(12)]
        #     else:
        #         attrnames = ["twofocusLR", "twofocusRL", "twofocusTL", "twofocusLT",
        #                     "twofocusLB", "twofocusBL", "twofocusRT", "twofocusTR",
        #                     "twofocusRB", "twofocusRB", "twofocusTB", "twofocusBT"]
            
        #     print('Calculating crosscorrelation two-focus left and right')
        #     setattr(G, attrnames[0], correlate(dataL, dataR, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
        #     setattr(G, attrnames[1], correlate(dataR, dataL, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
            
        #     print('Calculating crosscorrelation two-focus left and top')
        #     setattr(G, attrnames[2], correlate(dataT, dataL, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
        #     setattr(G, attrnames[3], correlate(dataL, dataT, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
            
        #     print('Calculating crosscorrelation two-focus left and bottom')
        #     setattr(G, attrnames[4], correlate(dataL, dataB, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
        #     setattr(G, attrnames[5], correlate(dataB, dataL, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
            
        #     print('Calculating crosscorrelation two-focus right and top')
        #     setattr(G, attrnames[6], correlate(dataR, dataT, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
        #     setattr(G, attrnames[7], correlate(dataT, dataR, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
            
        #     print('Calculating crosscorrelation two-focus right and bottom')
        #     setattr(G, attrnames[8], correlate(dataR, dataB, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
        #     setattr(G, attrnames[9], correlate(dataB, dataR, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
            
        #     print('Calculating crosscorrelation two-focus top and bottom')
        #     setattr(G, attrnames[10], correlate(dataT, dataB, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
        #     setattr(G, attrnames[11], correlate(dataB, dataT, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
            
            
        elif i == "crossCenter":
            # crosscorrelation center element with L, R, T, B
            dataCenter = extract_spad_data(data, 12)
            for j in range(25):
                print('Calculating crosscorrelation central element with ' + str(j))
                data2 = extract_spad_data(data, j)
                Gtemp = correlate(dataCenter, data2, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm)
                setattr(G, 'det12x' + str(j), Gtemp)
        
        elif i == 'singleElement':
            # autocorrelation single element
            # check first non empty channel
            j = -1
            ch1Found = False
            while not ch1Found:
                j += 1
                data1 = extract_spad_data(data, j)
                if np.sum(data1) > 10:
                    ch1Found = True
                    ch1 = j
            if ch1Found:
                print('Autocorrelation element ' + str(ch1))
                Gtemp = correlate(data1, data1, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm)
                G.auto = Gtemp
        
        elif i == "2MPD":
            # crosscorrelation two elements
            # check first non empty channel
            j = -1
            ch1Found = False
            ch2Found = False
            while not ch1Found:
                j += 1
                data1 = extract_spad_data(data, j)
                if np.sum(data1) > 10:
                    ch1Found = True
                    ch1 = j
            while not ch2Found and j < np.shape(data)[1] - 1:
                j += 1
                data2 = extract_spad_data(data, j)
                if np.sum(data2) > 10:
                    ch2Found = True
                    ch2 = j
            if ch1Found and not ch2Found:
                ch2 = ch1
                data2 = extract_spad_data(data, ch2)
                ch2Found = True
            if ch1Found and ch2Found:
                print('Cross correlation elements ' + str(ch1) + ' and ' + str(ch2))
                Gtemp = correlate(data1, data2, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm)
                G.cross12 = Gtemp
                print('Cross correlation elements ' + str(ch2) + ' and ' + str(ch1))
                Gtemp = correlate(data2, data1, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm)
                G.cross21 = Gtemp
                print('Autocorrelation element ' + str(ch1))
                Gtemp = correlate(data1, data1, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm)
                G.auto1 = Gtemp
                print('Autocorrelation element ' + str(ch2))
                Gtemp = correlate(data2, data2, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm)
                G.auto2 = Gtemp
                
        elif i == "crossAll":
            # crosscorrelation every element with every other element
            if algorithm == "sparse_matrices":
                print("Calculating all crosscorrelations with sparse matrices algorithm")
                [Gall, Gtimes] = fcs_sparse(data, dwell_time*1e-6, m=accuracy)
                if averaging is None:
                    for j in range(num_ch):
                        for k in range(num_ch):
                            Gtemp = np.column_stack((np.squeeze(Gtimes), Gall[:, j, k]))
                            setattr(G, 'det' + str(j) + 'x' + str(k), Gtemp)
                else:
                    # average over multiple cross-correlations
                    if averaging == 'default':
                        averaging = corrs2average_for_stics()
                        avs = [i[1] for i in averaging] # which correlations to average
                        els = [i[0] for i in averaging] # their new names
                    else:
                        avs = averaging
                        els = list_of_g_out
                    for el, av in enumerate(avs):
                        singleAv = [int(ch_nr) for ch_nr in re.findall(r'\d+', av)]
                        Nav = int(len(singleAv) / 2)
                        Gtemp = np.zeros((len(Gtimes), 2))
                        Gtemp[:,0] = np.squeeze(Gtimes)
                        for ind_av in range(Nav):
                            Gtemp[:,1] += Gall[:, singleAv[2*ind_av], singleAv[2*ind_av+1]]
                        Gtemp[:,1] /= Nav
                        setattr(G, els[el], Gtemp)
                        
            else:
                for j in range(num_ch):
                    data1 = extract_spad_data(data, j)
                    for k in range(num_ch):
                        data2 = extract_spad_data(data, k)
                        print('Calculating crosscorrelation det' + str(j) + ' and det' + str(k))
                        Gtemp = correlate(data1, data2, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm)
                        setattr(G, 'det' + str(j) + 'x' + str(k), Gtemp)
        
        elif i == "autoSpatial":
            # number of time points
            Nt = np.size(data, 0)
            # detector size (5 for SPAD)
            N = int(np.round(np.sqrt(np.size(data, 1)-1)))
            # G size
            M = 2 * N - 1
            deltats = range(0, 1, 1) # in units of dwell times
            G.autoSpatial = np.zeros((M, M, len(deltats)))
            # normalization
            print("Calculating average image")
            avIm = np.mean(data, 0)
            # avInt = np.mean(avIm[0:N*N]) - can't be used since every pixel
            # has a different PSF amplitude!!
            # for j in range(np.size(data, 0)):
                # data[j, :] = data[j, :] - avIm
            avIm = np.resize(avIm[0:N*N], (N, N))
            # calculate autocorrelation
            k = 0
            for deltat in deltats:
                print("Calculating spatial autocorr delta t = " + str(deltat * dwell_time) + " µs")
                for j in range(Nt-deltat):
                    im1 = np.resize(data[j, 0:N*N], (N, N))
                    im1 = np.ndarray.astype(im1, 'int64')
                    im2 = np.resize(data[j + deltat, 0:N*N], (N, N))
                    im2 = np.ndarray.astype(im2, 'int64')
                    # G.autoSpatial[:,:,k] = G.autoSpatial[:,:,k] + ssig.correlate2d(im1, im2)
                    # calculate correlation between im1 and im2
                    for shifty in np.arange(-4, 5):
                        for shiftx in np.arange(-4, 5):
                            # go through all detector elements
                            n = 0  # number of overlapping detector elements
                            Gtemp = 0
                            for detx in np.arange(np.max((0, shiftx)), np.min((5, 5+shiftx))):
                                for dety in np.arange(np.max((0, shifty)), np.min((5, 5+shifty))):
                                    GtempUnNorm = im1[dety, detx] * im2[dety-shifty, detx-shiftx]
                                    GtempNorm = GtempUnNorm - avIm[dety, detx] * avIm[dety-shifty, detx-shiftx]
                                    GtempNorm /= avIm[dety, detx] * avIm[dety-shifty, detx-shiftx]
                                    Gtemp += GtempNorm
                                    n += 1
                            Gtemp /= n
                            G.autoSpatial[shifty+4,shiftx+4,k] += Gtemp
                G.autoSpatial[:,:,k] /= (Nt-deltat)
                k = k + 1

        elif i == "av":
            # average of all 25 individual autocorrelation curves
            for j in range(25):
                # autocorrelation of a detector element j
                print('Calculating autocorrelation of detector element ' + str(j))
                dataSingle = extract_spad_data(data, j)
                Gtemp = correlate(dataSingle, dataSingle, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm)
                setattr(G, 'det' + str(j), Gtemp)
            Gav = Gtemp[:, 1]
            for j in range(24):
                Gav = np.add(Gav, getattr(G, 'det' + str(j))[:, 1])
            Gav = Gav / 25
            G.av = np.zeros([np.size(Gav, 0), 2])
            G.av[:, 0] = Gtemp[:, 0]
            G.av[:, 1] = Gav
        
        else:
            # ----------------------------------------------------------------
            # autocorrelation predefined keywords
            # ----------------------------------------------------------------
            if list_of_g_out is not None:
                attrname = next(list_of_g_out_enum)[1]
            else:
                attrname = i
            print('Calculating autocorrelation ' + str(i))
            data_kw = extract_spad_data(data, i)
            setattr(G, attrname, correlate(data_kw, data_kw, m=accuracy, binsize=binsize, deltat=dwell_time*1e-6, normalize=True, algorithm=algorithm))
            
    return G


def correlate(data1, data2, m=50, binsize=1, deltat=1, normalize=True, algorithm='multipletau'):
    """
    Calculate cross-correlation between two 1D arrays

    Parameters
    ----------
    data1 : np.array()
        First array with photon counts vs. time.
    data2 : np.array()
        Second array with photon counts vs. time.
    m : int, optional
        Accuracy for the correlation function. The default is 50.
    binsize : int, optional
        Bin the data in time with size binsize. Only used for pch analysis.
    deltat : int, optional
        Time between consecutive data points (dwell time). The default is 1.
    normalize : boolean, optional
        Normalize the correlation function, only needed for multipletau.
        The default is True.
    algorithm : string, optional
        Algorithm used to calculate the correlation. The default is 'multipletau'.
        Other options are 'wiener-khinchin' (fourier based) and 'tt2corr' for
        TCSPC data. In the last case, the input are arrays with arrival times

    Returns
    -------
    G : np.array()
        Correlation curve.

    """
    if algorithm == 'multipletau':
        G = multipletau.correlate(data1, data2, m=m, deltat=deltat, normalize=normalize)
    elif algorithm == 'wiener-khinchin':
        # time trace to correlation using fft
        G = tt2corr(data1, data2, m=m, macro_time=deltat)
    elif algorithm == 'pch':
        from ..pch.data2pch import calc_pch
        G = calc_pch(data1, binsize, maxcount=m)
    else:
        # algorithm is time-tag-to-correlation
        ind1 = np.where((data1 != 0))[0]
        ind2 = np.where((data2 != 0))[0]
        weights1 = data1[ind1]
        weights2 = data2[ind2]
        G = atimes_2_corr(ind1, ind2, accuracy=m, w0=weights1, w1=weights2, macroTime=deltat, taumax=ind1[-1] / 100)
    return G
        

def fcs2corrsplit(data, dwell_time, list_of_g=['central', 'sum3', 'sum5', 'chessboard', 'ullr'], accuracy=50, split=10):
    """
    Chunk SPAD-fcs trace into different parts and calculate correlation curves

    Parameters
    ----------
    data : np.array(Nt x Nc)
        Data variable, i.e. output from binfile2data.
        Nt: number of time points
        Nc: number of channels (typically 25 for a 5x5 detector)
    dwell_time : float
        Bin time [in µs].
    list_of_g : list, optional
        List of correlations to be calculated.
        The default is ['central', 'sum3', 'sum5', 'chessboard', 'ullr'].
    accuracy : int, optional
        Accuracy of the autocorrelation function. The default is 50.
    split : float, optional
        Number of traces to split the data into
        E.g. split=10 will divide a 60 second stream in 10 six second
        traces and calculate G for each individual trace. The default is 10.

    Returns
    -------
    G : object
        Object with all autocorrelations
        E.g. G.central contains the array with the central detector
        element autocorrelation..

    """
    
    if split == 1:
        G = fcs2corr(data, dwell_time, list_of_g, accuracy)
    else:
        G = Correlations()
        G.dwellTime = dwell_time
        N = int(np.size(data, 0))
        chunkSize = int(np.floor(N / split))
        for j in list_of_g:
            # --------------------- CALCULATE CORRELATION ---------------------
            print('Calculating correlation ' + str(j))
            i = 0
            for chunk in range(split):
                print('     Chunk ' + str(chunk+1) + ' --> ', end = '')
                # ------------------ CHUNK ------------------
                if data.ndim == 2:
                    dataSplit = data[i:i+chunkSize, :]
                else:
                    dataSplit = data[i:i+chunkSize]
                newList = [j]
                Gsplit = fcs2corr(dataSplit, dwell_time, newList, accuracy)
                GsplitList = list(Gsplit.__dict__.keys())
                for k in GsplitList:
                    if k.find('dwellTime') == -1:
                        setattr(G, k + '_chunk' + str(chunk), getattr(Gsplit, k))
                i += chunkSize
            # ---------- CALCULATE AVERAGE CORRELATION OF ALL CHUNKS ----------
            if j == '2MPD':
                avListBase = ['cross12', 'cross21', 'auto1', 'auto2']
                for avBase in avListBase:
                    avList = list(G.__dict__.keys())
                    avList = [i for i in avList if i.startswith(avBase + '_chunk')]
                    print('Calculating average correlation ' + avBase)
                    Gav = sum(getattr(G, i) for i in avList) / len(avList)
                    setattr(G, avBase + '_average', Gav)
                print('Calculating average cross correlation')
                G.cross_average = (G.cross12_average + G.cross21_average) / 2
            else:
                # Get list of "root" names, i.e. without "_chunk"
                Gfields = list(G.__dict__.keys())
                t = [Gfields[i].split("_chunk")[0] for i in range(len(Gfields))]
                t = list(dict.fromkeys(t))
                t.remove("dwellTime")
                # average over chunks
                for field in t:
                    print('Calculating average correlation ' + str(field))
                    avList = [i for i in Gfields if i.startswith(field + '_chunk')]
                    Gav = sum(getattr(G, i) for i in avList) / len(avList)
                    setattr(G, str(field) + '_average', Gav)
    return G


def fcs_sparse_matrices(fname, accuracy=16, split=10, time_trace=False, return_obj=False, averaging=None, root=0):
    """
    Calculate correlations using spare matrices properties.
    Author: Eleonora Perego

    Parameters
    ----------
    fname : string
        file name with fcs data.
    accuracy : int, optional
        Accuracy of the autocorrelation function. The default is 16.
    split : float, optional
        Number of seconds of each chunk to split the data into
        E.g. split=10 will divide a 60 second stream in 6 ten-second
        traces and calculate G for each  ividual trace. The default is 10.
    time_trace : boolean, optional
        see output. The default is False.
    return_obj : boolean, optional
        Return data as 4D np.array() or object. The default is False.
    root : int, optional
        Used for GUI only. The default is 0.
    averaging : list of list of two strings
        Used for averaging multiple cross-correlations (for each chunk)
        E.g. for having G.up_chunk0 = mean(det12x7_chunk0 and det11x6_chunk0)
            and G.down_chunk0 = mean(det12x17_chunk0 and det11x16_chunk0)
            etc.
        Use: averaging = [['up', '12x7+11x6'], ['down', '12x17+11x16']]

    Returns
    -------
    G : object
        np.array or object with autocorrelations.
    data : np.array()
        if time_trace == False: last chunk of raw data
        if time_trace == True: full time trace binned: [Nx25] array.

    """
    
    
    metafile = get_metafile_from_file(fname)
    info = get_file_info(metafile)
    
    dwellTime = 1e-6 * info.timeResolution
    duration = info.duration
    
    N = int(np.floor(duration / split)) # number of chunks

    G = Correlations()
    G.dwellTime = dwellTime
    chunkSize = int(np.floor( split/ dwellTime))
    Gcorrs = []
    
    for chunk in range(N):
        # --------------------- CALCULATE CORRELATIONS SINGLE CHUNK ---------------------
        print("+-----------------------")
        print("| Loading chunk " + str(chunk))
        print("+-----------------------")
        
        if root != 0:
            root.progress = chunk / N
        
        data = file_to_fcs_count(fname, np.uint8, chunkSize, chunk*chunkSize)
        
        if time_trace == True:
            binSize = int(chunkSize / 1000 * N) # time trace binned to 1000 data points
            bdata = bindata_chunks(data, binSize)
            bdataL = len(bdata)
            num_ch = np.shape(bdata)[1]
            if chunk == 0:
                timetraceAll = np.zeros((1000, num_ch), dtype=int)
            timetraceAll[chunk*bdataL:(chunk+1)*bdataL, :] = bdata 
        
        Gsplit,Gtimes = fcs_sparse(data, dwellTime, accuracy)
        Gcorrs.append(Gsplit)
    G.lagtimes = Gtimes
    G.Gsplit   = Gcorrs
    
    if time_trace:
        data = timetraceAll
    
    if return_obj:
        # convert 4D numpy array (chunk, y, corr1, corr2) to corr object
        Garr = np.asarray(G.Gsplit)
        Gshape = np.shape(Garr)
        Nchunk = Gshape[0]
        Ntau = Gshape[1]
        Nx = Gshape[2]
        Ny = Gshape[3]
        Gout = Correlations()
        if averaging is None:
            for c in range(Nchunk):
                for x in range(Nx):
                    for y in range(Ny):
                        Gtemp = np.zeros((Ntau, 2))
                        Gtemp[:,0] = np.squeeze(G.lagtimes)
                        Gtemp[:,1] = Garr[c, :, x, y]
                        setattr(Gout, "det" + str(x) + "x" + str(y) + "_chunk" + str(c), Gtemp)
        else:
            # average over multiple cross-correlations
            avs = [i[1] for i in averaging] # which correlations to average
            els = [i[0] for i in averaging] # their new names
            for c in range(Nchunk):
                for el, av in enumerate(avs):
                    singleAv = [int(i) for i in re.findall(r'\d+', av)]
                    Nav = int(len(singleAv) / 2)
                    Gtemp = np.zeros((len(G.lagtimes), 2))
                    for i in range(Nav):
                        Gtemp[:,0] += np.squeeze(G.lagtimes)
                        Gtemp[:,1] += Garr[c, :, singleAv[2*i], singleAv[2*i+1]]
                    Gtemp /= Nav
                    setattr(Gout, els[el] + "_chunk" + str(c), Gtemp)
                
        Gout = fcs_av_chunks(Gout, list(range(Nchunk)))
        Gout.dwellTime = dwellTime
        G = Gout
    
    return G, data


def fcs_sparse(data, dwell_time, m=50, normalize = True):
    # object from correlations class in which all correlation data is stored
    G = Correlations()
    
    # dwell time
    G.dwellTime = dwell_time
    
    # Check parameters
    if m // 2 != m / 2:
        mold = m
        m = np.int_((m // 2 + 1) * 2)
    else:
        m = np.int_(m)

    N = N0 = data.shape[0]
    Nchan = data.shape[1]
    k = np.int_(np.floor(np.log2(N / m)))
    lenG = m + k * m // 2 + 1
    Gtimes = np.zeros((lenG, 1), dtype = "float32")
    G = np.zeros((lenG, Nchan, Nchan), dtype="float32")
    normstat = np.zeros(lenG, dtype="float32")
    normnump = np.zeros(lenG, dtype="float32")
    
    spdata = spy.sparse.csr_matrix(data.transpose())
    spdata = spdata.astype("float32")
    
    traceavg = spdata.mean(axis = 1)
    traceavg[traceavg==0]=1
    
    # Calculate autocorrelation function for first m+1 bins
    for n in range(0, m + 1):
        Gtimes[n] = dwell_time * n
        res = spdata[:,:N-n].dot(spdata[:,n:].transpose())
        G[n] = res.toarray()
        normstat[n] = N - n
        normnump[n] = N
    
    if N % 2 == 1:
        N -= 1
    # compress every second element
    spdata = (spdata[:,:N:2] + spdata[:,1:N:2]) / 2
    spdata = spdata.toarray() # spdata is now full
    
    spdata = spdata-traceavg
    
    N //= 2
    
    # Start iteration for each m/2 values
    for step in range(1, k + 1):
        # Get the next m/2 values via correlation of the trace
        for n in range(1, m // 2 + 1):
            npmd2 = n + m // 2
            idx = m + n + (step - 1) * m // 2
            if spdata[:,:N - npmd2].shape[1] == 0:
                # This is a shortcut that stops the iteration once the
                # length of the trace is too small to compute a corre-
                # lation. The actual length of the correlation function
                # does not only depend on k - We also must be able to
                # perform the sum with respect to k for all elements.
                # For small N, the sum over zero elements would be
                # computed here.
                #
                # One could make this for-loop go up to maxval, where
                #   maxval1 = int(m/2)
                #   maxval2 = int(N-m/2-1)
                #   maxval = min(maxval1, maxval2)
                # However, we then would also need to find out which
                # element in G is the last element...
                G = G[:idx - 1]
                normstat = normstat[:idx - 1]
                normnump = normnump[:idx - 1]
                # Note that this break only breaks out of the current
                # for loop. However, we are already in the last loop
                # of the step-for-loop. That is because we calculated
                # k in advance.
                break
            else:
                Gtimes[idx] = dwell_time * npmd2 * 2**step
                # This is the computationally intensive step
                G[idx] = spdata[:,:N-npmd2].dot(spdata[:,npmd2:].transpose())
                normstat[idx] = N - npmd2
                normnump[idx] = N
        # Check if len(trace) is even:
        if N % 2 == 1:
            N -= 1
        # compress every second element
        spdata = (spdata[:,:N:2] + spdata[:,1:N:2]) / 2  
        N //= 2
   
    if normalize:
        # G /= normstat.reshape(lenG,1,1)
        # G /= (traceavg*traceavg.transpose())
        # G -= 1
        lenG = len(G)
        G /= normstat.reshape(lenG,1,1)
        G /= (traceavg*traceavg.transpose())
        G[1:m+1] -= 1

    lenG = np.min((len(G), len(Gtimes)))
    G = G[0:lenG,:,:]
    Gtimes = Gtimes[0:lenG]

    return G, Gtimes


def fcs_spatialcorrav(G, N=5, returnType='3Darray'):
    spatialCorr = np.zeros([2*N-1, 2*N-1, len(G.det0x0_average)])
    for shifty in np.arange(-(N-1), N):
        for shiftx in np.arange(-(N-1), N):
            avList = spad_shift_vector_crosscorr([shifty, shiftx], N)
            avList = [s + '_average' for s in avList]
            Gav = sum(getattr(G, i) for i in avList) / len(avList)
            if returnType == '3Darray':
                spatialCorr[shifty+N-1, shiftx+N-1, :] = Gav[:,1]
            else:
                setattr(G, 'shifty_' + str(shifty) + '_shiftx_' + str(shiftx), Gav)
    if returnType == '3Darray':
        G.spatialCorr = spatialCorr
    return G


def correlate_parallel(a, m=16, deltat=1, normalize=False, copy=True, dtype=None,
                  compress="average", ret_sum=False):
   
    if not isinstance(normalize, bool):
        raise ValueError("`normalize` must be boolean!")
    if not isinstance(copy, bool):
        raise ValueError("`copy` must be boolean!")
    if not isinstance(ret_sum, bool):
        raise ValueError("`ret_sum` must be boolean!")
    if normalize and ret_sum:
        raise ValueError("'normalize' and 'ret_sum' must not both be True!")
    compress_values = ["average", "first", "second"]
    if compress not in compress_values:
        raise ValueError("Invalid value for `compress`! Possible values "
                         "are '{}'.".format(','.join(compress_values)))

    if dtype is None:
        dtype = np.dtype(a[0].__class__)
    else:
        dtype = np.dtype(dtype)

    ZERO_CUTOFF = 1e-15

    # If copy is false and dtype is the same as the input array,
    # then this line does not have an effect:
    trace = np.array(a, dtype=dtype, copy=copy)
    trace = trace.transpose()

    # Check parameters
    if m // 2 != m / 2:
        mold = m
        m = np.int_((m // 2 + 1) * 2)
    else:
        m = np.int_(m)

    N = N0 = trace.shape[0]

    # Find out the length of the correlation function.
    # The integer k defines how many times we can average over
    # two neighboring array elements in order to obtain an array of
    # length just larger than m.
    k = np.int_(np.floor(np.log2(N / m)))

    # In the base2 multiple-tau scheme, the length of the correlation
    # array is (only taking into account values that are computed from
    # traces that are just larger than m):
    lenG = m + k * (m // 2) + 1

    G = np.zeros((lenG, 2), dtype=dtype)

    normstat = np.zeros(lenG, dtype=dtype)
    normnump = np.zeros(lenG, dtype=dtype)

    traceavg = np.average(trace, axis = 1)

    # We use the fluctuation of the signal around the mean
    if normalize:
        trace -= traceavg.reshape(len(traceavg),1)

    # Otherwise the following for-loop will fail:
    if N < 2 * m:
        raise ValueError("`len(a)` must be >= `2m`!")

    # Calculate autocorrelation function for first m+1 bins
    # Discrete convolution of m elements
    for n in range(0, m + 1):
        G[n, 0] = deltat * n
        # This is the computationally intensive step
        G[n, 1] = np.sum(trace[:N - n] * trace[n:])
        normstat[n] = N - n
        normnump[n] = N
    # Now that we calculated the first m elements of G, let us
    # go on with the next m/2 elements.
    # Check if len(trace) is even:
    if N % 2 == 1:
        N -= 1
    # compress every second element
    if compress == compress_values[0]:
        trace = (trace[:N:2] + trace[1:N:2]) / 2
    elif compress == compress_values[1]:
        trace = trace[:N:2]
    elif compress == compress_values[2]:
        trace = trace[1:N:2]
    N //= 2
    # Start iteration for each m/2 values
    for step in range(1, k + 1):
        # Get the next m/2 values via correlation of the trace
        for n in range(1, m // 2 + 1):
            npmd2 = n + m // 2
            idx = m + n + (step - 1) * m // 2
            if len(trace[:N - npmd2]) == 0:
                # This is a shortcut that stops the iteration once the
                # length of the trace is too small to compute a corre-
                # lation. The actual length of the correlation function
                # does not only depend on k - We also must be able to
                # perform the sum with respect to k for all elements.
                # For small N, the sum over zero elements would be
                # computed here.
                #
                # One could make this for-loop go up to maxval, where
                #   maxval1 = int(m/2)
                #   maxval2 = int(N-m/2-1)
                #   maxval = min(maxval1, maxval2)
                # However, we then would also need to find out which
                # element in G is the last element...
                G = G[:idx - 1]
                normstat = normstat[:idx - 1]
                normnump = normnump[:idx - 1]
                # Note that this break only breaks out of the current
                # for loop. However, we are already in the last loop
                # of the step-for-loop. That is because we calculated
                # k in advance.
                break
            else:
                G[idx, 0] = deltat * npmd2 * 2**step
                # This is the computationally intensive step
                G[idx, 1] = np.sum(trace[:N - npmd2] *
                                   trace[npmd2:])
                normstat[idx] = N - npmd2
                normnump[idx] = N
        # Check if len(trace) is even:
        if N % 2 == 1:
            N -= 1
        # compress every second element
        if compress == compress_values[0]:
            trace = (trace[:N:2] + trace[1:N:2]) / 2
        elif compress == compress_values[1]:
            trace = trace[:N:2]
        elif compress == compress_values[2]:
            trace = trace[1:N:2]

        N //= 2

    if normalize:
        G[:, 1] /= traceavg**2 * normstat
    elif not ret_sum:
        G[:, 1] *= N0 / normnump

    if ret_sum:
        return G, normstat
    else:
        return G


def fcs_av_chunks(G, listOfChunks):
    """
    Average for each correlation mode the chunks given in listOfChunks.
    Used to calculate the average correlation for the good chunks only.

    Parameters
    ----------
    G : object
        Correlations object that contains all correlations.
    listOfChunks : list
        List with chunk numbers used for the calculation of the average
        e.g. [1, 3, 4, 7].

    Returns
    -------
    G : object
        Same object as input but with the additional fields
        G.det10_F0_averageX
        G.sum3_F0_averageX
        G.sum5_F0_averageX
        G.det10_F1_averageX
        ...
        where the averages are calculated over the listed chunks only.

    """
    
    listOfCorr = list(G.__dict__.keys())
    listOfCorr2 = []
    
    for corr in listOfCorr:
        if 'average' not in corr and 'chunk' in corr and 'chunksOff' not in corr and 'chunks_off' not in corr:
            pos = corr.find('chunk')
            if len(corr[0:pos]) > 0:
                listOfCorr2.append(corr[0:pos])
    
    # remove duplicates
    listOfCorr2 = list(dict.fromkeys(listOfCorr2))
    
    for corr in listOfCorr2:
        Gtemp = getattr(G, corr + 'chunk0') * 0
        GtempSquared = getattr(G, corr + 'chunk0') * 0
        for chunk in listOfChunks:
            Gtemp += getattr(G, corr + 'chunk' + str(chunk))
            GtempSquared += getattr(G, corr + 'chunk' + str(chunk))**2
        
        Gtemp /= len(listOfChunks)
        Gstd = np.sqrt(np.clip(GtempSquared / len(listOfChunks) - Gtemp**2, 0, None))
        
        Gtot = np.zeros((np.shape(Gtemp)[0], np.shape(Gtemp)[1] + 1))
        Gtot[:, 0:-1] = Gtemp # [time, average]
        Gtot[:, -1] = Gstd[:,1] # standard deviation
        
        setattr(G, corr + 'averageX', Gtot)
    
    return G


def fcs_crosscenter_av(G, returnField='_averageX', returnObj = True):
    """
    Average pair-correlations between central pixel and other pixels that are
    located at the same distance from the center

    Parameters
    ----------
    G : TYPE
        Correlations object that (at least) contains all
        cross-correlations between central pixel and all other pixels:
        G.det12x12_average, G.det12x13_average, etc..
    returnField : string, optional
        name of the field containing the average. The default is '_averageX'.
    returnObj : boolean, optional
        return np.array() with average correlation or object. The default is True.

    Returns
    -------
    G : object or np.array()
        Same object as input but with the additional field
        G.crossCenterAv, which contains array of 6 columns, containing
        averaged cross-correlations between central pixel and pixels
        located at a distance of
            | 0 | 1 | sqrt(2) | 2 | sqrt(5) | sqrt(8) |.

    """
    
    try:
        tau = G.det12x12_average[:,0]
    except:
        tau = G.det12x12_chunk0[:,0]
    G.crossCenterAv = np.zeros((len(tau), 6))
    
    # average autocorrelation center element
    try:
        G.crossCenterAv[:,0] = getattr(G, 'det12x12' + returnField)[:,1]
    except:
        G.crossCenterAv[:,0] = getattr(G, 'det12x12' + '_average')[:,1]
    
    # average pair-correlations 4 elements located at distance 1 from center
    try:
        G.crossCenterAv[:,1] = np.mean(np.transpose(np.array([getattr(G, 'det12x' + str(det) + returnField)[:,1] for det in [7, 11, 13, 17]])), 1)
    except:
        G.crossCenterAv[:,1] = np.mean(np.transpose(np.array([getattr(G, 'det12x' + str(det) + '_average')[:,1] for det in [7, 11, 13, 17]])), 1)
    
    # average pair-correlations 4 elements located at distance sqrt(2) from center
    try:
        G.crossCenterAv[:,2] = np.mean(np.transpose(np.array([getattr(G, 'det12x' + str(det) + returnField)[:,1] for det in [6, 8, 16, 18]])), 1)
    except:
        G.crossCenterAv[:,2] = np.mean(np.transpose(np.array([getattr(G, 'det12x' + str(det) + '_average')[:,1] for det in [6, 8, 16, 18]])), 1)
    
    # average pair-correlation 4 elements located at distance 2 from center
    try:
        G.crossCenterAv[:,3] = np.mean(np.transpose(np.array([getattr(G, 'det12x' + str(det) + returnField)[:,1] for det in [2, 10, 14, 22]])), 1)
    except:
        G.crossCenterAv[:,3] = np.mean(np.transpose(np.array([getattr(G, 'det12x' + str(det) + '_average')[:,1] for det in [2, 10, 14, 22]])), 1)
    
    # average pair-correlation 8 elements located at distance sqrt(5) from center
    try:
        G.crossCenterAv[:,4] = np.mean(np.transpose(np.array([getattr(G, 'det12x' + str(det) + returnField)[:,1] for det in [1, 3, 5, 9, 15, 19, 21, 23]])), 1)
    except:
        G.crossCenterAv[:,4] = np.mean(np.transpose(np.array([getattr(G, 'det12x' + str(det) + '_average')[:,1] for det in [1, 3, 5, 9, 15, 19, 21, 23]])), 1)
    
    # average pair-correlation 4 elements located at distance sqrt(8) from center
    try:
        G.crossCenterAv[:,5] = np.mean(np.transpose(np.array([getattr(G, 'det12x' + str(det) + returnField)[:,1] for det in [0, 4, 20, 24]])), 1)
    except:
        G.crossCenterAv[:,5] = np.mean(np.transpose(np.array([getattr(G, 'det12x' + str(det) + '_average')[:,1] for det in [0, 4, 20, 24]])), 1)
    
    if returnObj:
        return G
    else:
        return G.crossCenterAv


def fcs_bin_to_csv_all(folderName=[], Glist=['central', 'sum3', 'sum5', 'chessboard', 'ullr'], split=10):
    # PARSE INPUT
    if folderName == []:
        folderName = os.getcwd()
    folderName = folderName.replace("\\", "/")
    folderName = Path(folderName)
    
    # CHECK BIN FILES
    allFiles = list_files(folderName, 'bin')
    
    # GO THROUGH EACH FILE
    for file in allFiles:
        fileName = ntpath.basename(file)
        print("File found: " + fileName)
        [G, data] = fcs_load_and_corr_split(file, Glist, 50, split)
        corr2csv(G, file[0:-4], [0, 0], 0)


def plot_fcs_correlations(G, plotList='all', limits=[0, -1], vector=[], pColors='auto', yscale='lin'):
    """
    Plot correlation curves

    Parameters
    ----------
    G : object
        Object with all autocorrelations
        Possible attributes:
            det*,
            central, sum3, sum5, allbuthot, chessboard, ullr, av
            autoSpatial,
            det12x*
            dwellTime (is not plotted).
    plotList : list or string, optional
        correlations to plot. The default is 'all'.
    limits : list with two elements, optional
        tau boundaries to plot. The default is [0, -1].
    vector : list, optional
        List of vectors to plot for crosscorr. The default is [].
    pColors : string, optional
        Plot colors. The default is 'auto'.
    yscale : string, optional
        linear or log y scale. The default is 'lin'.

    Returns
    -------
    figure.

    """

    spatialCorrList = ['autoSpatial']

    start = limits[0]
    stop = limits[1]

    # plotList contains all attributes of G that have to be plotted
    if plotList == 'all':
        plotList = list(G.__dict__.keys())
        
    # remove dwellTime from plotList
    if 'dwellTime' in plotList:
        plotList.remove('dwellTime')

    if 'av' in plotList:
        # remove all single detector element correlations
        plotListRemove = fnmatch.filter(plotList, 'det?')
        for elem in plotListRemove:
            plotList.remove(elem)
        plotListRemove = fnmatch.filter(plotList, 'det??')
        for elem in plotListRemove:
            plotList.remove(elem)
    
    if np.size(fnmatch.filter(plotList, 'det12x??')) > 10:
        # replace all individual cross-correlations by single crossCenter element
        plotListRemove = fnmatch.filter(plotList, 'det12x?')
        for elem in plotListRemove:
            plotList.remove(elem)
        plotListRemove = fnmatch.filter(plotList, 'det12x??')
        for elem in plotListRemove:
            plotList.remove(elem)
        plotList.append('crossCenter')
    
    if fnmatch.filter(plotList, '*_'):
        plotListStart = plotList[0]
        # plot chunks of data and average
        plotList = list(G.__dict__.keys())
        plotList.remove('dwellTime')
        plotList = [i for i in plotList if i.startswith(plotListStart)]
        

    # -------------------- Check for temporal correlations --------------------
    plotTempCorr = False
    for i in range(np.size(plotList)):
        if plotList[i] not in spatialCorrList:
            plotTempCorr = True
            break

    if plotTempCorr:
        leg = []  # figure legend
        h = plt.figure()
        plt.rcParams.update({'font.size': 15})
        maxy = 0
        miny = 0
        minx = 25e-9
        maxx = 10
        pColIndex = 0
        for i in plotList:

            #if i not in list(G.__dict__.keys()):
              #  break

            if i in ["central", "central_average", "sum3", "sum3_average", "sum5", "sum5_average", "allbuthot", "allbuthot_average", "chessboard", "chessboard_average", "chess3", "chess3_average", "ullr", "ullr_average", "av", "cross12_average", "cross21_average", "cross_average", "auto1_average", "auto2_average"]:
                # plot autocorrelation
                Gtemp = getattr(G, i)
                plt.plot(Gtemp[start:stop, 0], Gtemp[start:stop, 1], color=plot_colors(i), linewidth=1.3)
                maxy = np.max([maxy, np.max(Gtemp[start+1:stop, 1])])
                miny = np.min([miny, np.min(Gtemp[start+1:stop, 1])])
                minx = Gtemp[start, 0]
                maxx = Gtemp[stop, 0]
                leg.append(i)

            elif i == 'crossCenter':
                for j in range(25):
                    Gsingle = getattr(G, 'det12x' + str(j))
                    # plotColor = color_from_map(distance2detelements(12, j), 0, np.sqrt(8))
                    plt.plot(Gsingle[start:stop, 0], Gsingle[start:stop, 1], color=plot_colors(i))
                    maxy = np.max([maxy, np.max(Gsingle[start+1:stop, 1])])
                    miny = np.min([miny, np.min(Gtemp[start+1:stop, 1])])
                    leg.append(i + str(j))
            
            elif i == 'crossCenterAv':
                tau = G.det12x12_average[:,0]
                for j in range(6):
                    plt.plot(tau[start:stop], G.crossCenterAv[start:stop, j], color=plot_colors(j))
                miny = np.min(G.crossCenterAv[start+10:stop,:])
                maxy = np.max(G.crossCenterAv[start+1:stop,:])
                leg = ['$\Delta r = 0$', '$\Delta r = 1$', '$\Delta r = \sqrt{2}$', '$\Delta r = 2$', '$\Delta r = \sqrt{5}$', '$\Delta r = 2\sqrt{2}$']

            elif i != 'autoSpatial' and i != 'stics' and i != 'crossAll' and i != 'crossVector':
                # plot autocorr single detector element
                if pColors == 'auto':
                    plt.plot(getattr(G, i)[start:stop, 0], getattr(G, i)[start:stop, 1])
                else:
                    plt.plot(getattr(G, i)[start:stop, 0], getattr(G, i)[start:stop, 1], color=plot_colors(pColors[pColIndex]))
                    pColIndex += 1
                maxy = np.max([maxy, np.max(getattr(G, i)[start+1:stop, 1])])
                miny = np.min([miny, np.min(getattr(G, i)[start+1:stop, 1])])
                minx = getattr(G, i)[start, 0]
                maxx = getattr(G, i)[stop, 0]
                if '_average' in i:
                    iLeg = i[0:-8]
                else:
                    iLeg = i
                leg.append(iLeg)

        # figure lay-out
        plt.xscale('log')
        plt.xlabel('Temporal shift [s]')
        plt.ylabel('G')
        if yscale == 'log':
            plt.yscale('log')
        else:
            plt.yscale('linear')
        axes = plt.gca()
        axes.set_xlim([minx, maxx])
        axes.set_ylim([miny, maxy])
        if np.size(leg) > 0 and np.size(leg) < 10 and 'crossCenter' not in plotList:
            axes.legend(leg)
        plt.tight_layout()
        
        if 'crossCenter' in plotList:
            plot_cross_center_scheme()
            
    # -------------------- Check for spatial correlations --------------------
    if 'autoSpatial' in plotList:
        Gtemp = G.autoSpatial
        Gmax = np.max(Gtemp)
        xmax = (np.size(Gtemp, 0)) / 2
        extent = [-xmax, xmax, -xmax, xmax]
        for j in range(np.size(Gtemp, 2)):
            h = plt.figure()
            plt.imshow(Gtemp[:, :, j], extent=extent, vmin=0, vmax=Gmax)
            plt.title('delta_t = ' + str(G.dwellTime * j) + ' µs')
    
    if 'crossAll' in plotList:
        Gtemp = G.spatialCorr
        tau = G.det0x0_average[:,0]
        for vector in [[4, 4], [3, 4], [3, 3], [2, 4], [2, 3], [2, 2]]:
            plt.plot(tau, Gtemp[vector[0], vector[1], :])
        plt.legend(['[0, 0]', '[1, 0]', '[1, 1]', '[0, 2]', '[2, 1]', '[2, 2]'])
        plt.xscale('log')
        plt.xlabel('Temporal shift [s]')
        plt.ylabel('G')
        axes.set_ylim([miny, np.max(Gtemp[:,:,2:])])
#        Gtemp = G.spatialCorr
#        Gmax = np.sort(Gtemp.flatten())[-2] # second highest number
#        extent = [-4, 5, -4, 5]
#        for j in range(np.size(Gtemp, 2)):
#            h = plt.figure()
#            plt.imshow(Gtemp[:, :, j], extent=extent, vmin=0)
#            plt.title('delta_t = ' + str(G.dwellTime * j) + ' µs')
    
    if 'crossVector' in plotList:
        Gtemp = G.spatialCorr
        tau = G.det0x0_chunk0[:,0]
        for i in range(len(vector)):
            vectorI = vector[i]
            plt.plot(tau, Gtemp[4+vectorI[0], 4+vectorI[1], :], label='[' + str(vectorI[0]) + ', ' + str(vectorI[1]) + ']')
        plt.xscale('log')
        plt.xlabel('Temporal shift [s]')
        plt.ylabel('G')
        plt.legend()
        axes.set_ylim([miny, np.max(Gtemp[:,:,2:])])
    
    if 'stics' in plotList:
        Gtemp = getattr(G, 'det12x12')
        Gplot = np.zeros([9, 9])
        N = 10
        indArray = np.concatenate(([0], np.round(np.logspace(0, np.log10(len(Gtemp) - 1), N)).astype('int')))
        for i in range(N):
            # go through all lag times
            ind = np.round(indArray[i])
            print(ind)
            for yshift in np.arange(-4, 5):
                for xshift in np.arange(-4, 5):
                    # go through each shift vector
                    detDiff = -yshift * 5 - xshift
                    Gv = 0
                    nG = 0
                    for det1 in range(25):
                        [y, x] = coord(det1)
                        if x-xshift < 0 or x-xshift>4 or y-yshift < 0 or y-yshift > 4:
                            # don't do anything
                            pass
                        else:
                            det2 = det1 + detDiff
                            print('det1 = ' + str(det1) + ' and det2 = ' + str(det2))
                            if det2 >= 0 and det2 <= 24:
                                Gv += getattr(G, 'det' + str(det1) + 'x' + str(det2))[int(ind), 1]
                                nG += 1
                    Gplot[yshift+4, xshift+4] = Gv / nG
            if i == 0:
                plotMax = np.max(Gplot)
            xmax = 9 / 2
            extent = [-xmax, xmax, -xmax, xmax]
            h = plt.figure()
            plt.imshow(Gplot, extent=extent, vmin=0, vmax=plotMax)
            plt.title('delta_t = ' + str(int(G.dwellTime * ind)) + ' µs')
            
    return h


def plot_g_surf(G):
    N = np.size(G, 0)
    N = (N - 1) / 2
    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')
    x = y = np.arange(-N, N + 1)
    X, Y = np.meshgrid(x, y)
    ax.plot_surface(X, Y, G)
    return fig


def plot_cross_center_scheme():
    detEl = range(25)
    distances = np.zeros(25)
    for i in detEl:
        distances[i] = distance2detelements(12, i)
    distances = np.resize(distances, (5, 5))
    plt.figure()
    plt.imshow(distances, 'viridis')
    plt.title('Color scheme cross-correlations')


def corrs2average_for_stics(det_size=5, pixelsOff=[]):
    """
    Return a list of cross-correlations with identical shifts between their
    channels

    Parameters
    ----------
    det_size : int, optional
        Number of pixels in each dimension of the array detector.
        The default is 5.
    pixelsOff : list, optional
        List of pixels that are excluded for this analysis because they are
        too far away from the optical axis, e.g [0,1,3,4,5,9,15,19,20,21,23,24]
        The default is [].

    Returns
    -------
    average : list
        List of pairs of strings describing the correlations to average and their
        new name, e.g.
        [['up', '12x0+12x1+12x2'], ['down', '0x12+1x12+2x12']]
    """
    listOfX = []
    listOfY = []
    names = []
    for vert in range(int(2*det_size-1)):
        for hor in range(int(2*det_size-1)):
            shifty = vert-det_size+1
            shiftx = hor-det_size+1
            listOfY.append(shifty)
            listOfX.append(shiftx)
            names.append('V' + str(shifty) + '_H' + str(shiftx))
    avList = [list_of_pixel_pairs_at_distance([listOfY[i], listOfX[i]], pixelsOff=pixelsOff) for i in range(len(listOfY))]
    avListStr = []
    names_final = []
    for idx, avSingleDist in enumerate(avList):
        if len(avSingleDist) > 0:
            avstr = ''
            for j in avSingleDist:
                avstr += str(j[0]) + 'x' + str(j[1]) + '+'
            avListStr.append(avstr[0:-1])
            names_final.append(names[idx])
        
    average = [[names_final[i], avListStr[i]] for i in range(len(names))]
    
    return average