# -*- coding: UTF-8 -*-
'''
================================================================================
Project: SiriL-ic ( SiriL Image Companion )

This script structures the SiriL work folder into a subfolder, copies the
astronomical images into the subfolders, and builds the associated SiriL
script. It can also group scripts.
================================================================================
   Author:  M27trognondepomme <pebe92 (at) gmail.com>

This program is provided without any guarantee.

The license is  LGPL-v3
For details, see GNU General Public License, version 3 or later.
                       "https://www.gnu.org/licenses/gpl.html"
================================================================================
'''
import glob
import os
import copy

from sirilic.lib import tools
from sirilic.lib.constantes import IMAGE, OFFSET, DARK, FLAT, DFLAT
from sirilic.lib.constantes import NB_IODF, IODF_SORTED, TYPENAME
from sirilic.lib.constantes import NoLayer

# ==============================================================================
NORM_NO = "no"
NORM_ADD = "add"
NORM_MUL = "mul"
NORM_ADDSCALE = "addscale"
NORM_MULSCALE = "mulscale"

STACK_SUM = "sum"
STACK_MEAN = "mean"
STACK_MEDIAN = "median"
STACK_MAX = "max"
STACK_MIN = "min"

REJ_NONE = "No rejection"
REJ_PERCENTILE = "Percentile"
REJ_SIGMA = "Sigma"
REJ_MAD = "MAD"
REJ_MEDIAN = "Median"
REJ_WINSORIZED = "Winsorized"
REJ_LINEARFIT = "Linear-Fit"
REJ_GENERALIZED = "Generalized"

WEIGHING_NONE = "None"
WEIGHING_NB_STARS = "Nb of stars"
WEIGHING_FWHM = "WFWHM"
WEIGHING_NOISE = "Noise"
WEIGHING_NB_IMAGES = "Nb of images"

# keywords for stringify()
KwKeyStr = "/"

# ==============================================================================


class CDatabase():
    def __init__(self):
        self.Clr()

    def Clr(self):
        self.db = {}
        self.prop = {'multisession': True, 'stack_intermed': False,
                     'CFAequa': True, 'FastNorm': False,
                     'SameProperties': True}

    def GetProp(self):
        return self.prop

    def Set(self, strprop, newvalue):
        if self.prop[strprop] != newvalue:
            self.prop[strprop] = newvalue
            return True
        return False

    def SetProp(self, multisession, stack_intermed, CFAequa, FastNorm, same_properties=False):
        changed = (self.Set('multisession', multisession) or
                   self.Set('stack_intermed', stack_intermed) or
                   self.Set('CFAequa', CFAequa) or
                   self.Set('FastNorm', FastNorm) or
                   self.Set('SameProperties', same_properties))
        return changed

    def KeyStr(self, ObjectName, LayerName, SessionName):
        return ObjectName + KwKeyStr + LayerName + KwKeyStr + SessionName

    def Split(self, keystr):
        return keystr.split(KwKeyStr)

    def Add(self, keystr):
        items = keystr.split(KwKeyStr)
        self.db[keystr] = C_DbImage(items[0], items[1], items[2])

    def GetDb(self):
        return self.db

    def GetItem(self, keystr):
        try:
            return self.db[keystr]
        except Exception:   # pylint: disable=broad-except
            return None

    def GetItemDb(self, keystr):
        try:
            return self.db[keystr].db
        except Exception:   # pylint: disable=broad-except
            return None

    def GetKeyStrSorted(self):
        ObjLayersession = []
        for keystr in self.db:
            item = self.db[keystr]
            ObjLayersession.append(
                (item.db['Object'], item.db['LayerName'], item.db['Session']))
        ObjLayersession_tries = sorted(
            ObjLayersession, key=lambda xx: xx[0] + "%d" % NoLayer[xx[1]] + xx[2], reverse=False)
        KeyStrSorted = [self.KeyStr(item[0], item[1], item[2])
                        for item in ObjLayersession_tries]
        return KeyStrSorted

    def DelItem(self, keystr):
        self.db.pop(keystr, None)

    def GetItems(self):
        out = []
        for keystr in self.db:
            item = self.db[keystr]
            etat = self.Item_IsInitialised(keystr)
            out.append(
                (item.db['Object'], item.db['LayerName'], item.db['Session'], etat))
        return out

    def GetFiles(self, keystr):
        return self.db[keystr].GetFiles()

    def SetFiles(self, keystr, files):
        return self.db[keystr].SetFiles(files)

    def Item_IsInitialised(self, keystr):
        return self.db[keystr].IsInitialised()

    def Stringify(self):
        chaines = "{\n"
        for keystr in self.db:
            item = self.db[keystr]
            chaines += item.Stringify()
        chaines += "}"
        chaines = chaines.replace(",\n}", "\n}")
        chaines = chaines.replace(",\n]", "\n]")
        chaines = "[" + str(self.prop) + ",\n" + chaines + "]"
        return chaines

    def String2data(self, chaines):
        try:
            self.Clr()
            [prop, db_in] = tools.String2data(chaines)
            for keystr, value in db_in.items():
                self.Add(keystr)
                db = self.GetItemDb(keystr)
                for ii in range(NB_IODF):
                    jj = IODF_SORTED[ii]
                    data = db['Images'][jj].GetData()
                    for key in data.keys():
                        # ne charge que les cles de l'objet
                        if not (key in value[ii]):
                            continue  # la cle n'existe pas, on passe a la suite
                        if type(data[key]) is dict:
                            if self.CopyDict(data[key], value[ii][key]):
                                return True
                            continue
                        data[key] = value[ii][key]
            for key in self.prop.keys():
                # ne charge que les cles existantes dans self.prop()
                if not (key in prop):
                    continue  # la cle n'existe pas, on passe a la suite
                self.prop[key] = prop[key]

        except Exception as e:   # pylint: disable=broad-except
            print("*** CDatabase::String2data() error loading " + str(e))
            return True
        return False

    def CopyDict(self, a, b, cnt=0):
        for key in a.keys():
            if not (key in b.keys()):
                continue  # la cle n'existe pas, on passe a la suite
            if type(a[key]) is dict:
                if cnt < 5:
                    if self.CopyDict(a[key], b[key], cnt + 1):
                        return True
                    continue
                else:
                    print(
                        "*** CDatabase::CopyDict error : too much level = " + str(cnt))
                    return True
            a[key] = b[key]
        return False

    def IsMultiSession(self, keystr):
        count = 0
        obj_r, layer_r = keystr.split(KwKeyStr)[0:2]
        for kstr in self.db.keys():
            obj, layer = kstr.split(KwKeyStr)[0:2]
            if (obj == obj_r) and (layer == layer_r):
                count += 1
        return count > 1

    def IsSameProperties(self):
        return self.prop['SameProperties']

    def GetArborescence(self):
        arbre = {}
        for keystr in sorted(self.db.keys()):
            xx, yy, zz = self.Split(keystr)
            if xx in arbre.keys():
                if yy in arbre[xx]:
                    arbre[xx][yy][zz] = [keystr]
                else:
                    arbre[xx][yy] = {zz: [keystr]}
            else:
                arbre[xx] = {yy: {zz: [keystr]}}
        return arbre

    def Recopie(self, keystr_dest, keystr_src):
        self.db[keystr_dest].db['Images'] = self.db[keystr_src].Recopie_dB()
# ==============================================================================


class C_DbImage:
    def __init__(self, ObjectName, LayerName, SessionName):
        self.db = {'Object': ObjectName,
                   'LayerName': LayerName,
                   'Session': SessionName,
                   'Images': [C_Offset(), C_Dark(), C_DFlat(), C_Flat(), C_Image()]
                   }

    def KeyStr(self):
        return self.db['Object'] + KwKeyStr + self.db['LayerName'] + KwKeyStr + self.db['Session']

    def GetFiles(self):
        files = [None] * NB_IODF
        for ii in IODF_SORTED:
            files[ii] = self.db['Images'][ii].GetFiles()
        return files

    def GetDbIodf(self, index):
        return self.db['Images'][index].GetData()

    def GetIodf(self, index):
        return self.db['Images'][index]

    def SetFiles(self, files):
        flag = False
        for ii in IODF_SORTED:
            flag |= self.db['Images'][ii].SetFiles(files[ii])
        return flag

    def IsInitialised(self):
        etat = ""
        for ii in IODF_SORTED:
            value = self.db['Images'][ii].IsInitialised()
            if value == "":
                continue
            etat = value if not etat else etat + "," + \
                self.db['Images'][ii].IsInitialised()
        if not etat:
            etat = "uninitialized"
        etat = etat.replace("lights", "images")
        return etat

    def Stringify(self, ident=4):
        chaine = "'" + self.KeyStr() + "': [\n"
        for ii in IODF_SORTED:
            chaine += self.db['Images'][ii].Stringify(ident) + ',\n'
        chaine = chaine + '],\n'
        return chaine

    def Recopie_dB(self):
        images = [C_Offset(), C_Dark(), C_DFlat(), C_Flat(), C_Image()]
        for ii in range(NB_IODF):
            images[ii].SetData(self.db['Images'][ii].Recopie_data())
        return images
# ==============================================================================


class C_IODF():
    def __init__(self, type_img):
        self.data = {"typename": TYPENAME[type_img],
                     "type": type_img,
                     "files": [],
                     "pp": {"copylib": False, "suboffset": False},
                     "stack": {
            "type": STACK_MEAN,
            "reject": {"type": REJ_WINSORIZED, "high": 3, "low": 3},
            "weighing": WEIGHING_NONE,
            "norm": NORM_NO,
            "RGBequa": False}
        }

    def IsInitialised(self):
        if self.data["files"]:
            return self.data["typename"].lower()
        return ""

    def GetData(self):
        return self.data

    def isModified(self, data):
        return self.data["typename"] != data["typename"]

    def SetData(self, data=None):
        if data is None:
            data = {}
        flag = self.data != data
        self.data = copy.deepcopy(data)
        return flag

    def Stringify(self, ident=4):
        return tools.Stringify(self.data, ident)

    def GetFiles(self):
        return self.data["files"]

    def GetFilesExpanded(self):
        if not self.data["files"]:
            return []
        liste_expanded = []
        for xx in self.data["files"]:
            yy = glob.glob(xx)
            if len(yy) == 0:
                yy = (xx,)
            for zz in yy:
                liste_expanded.append(zz)
        liste_expanded = set(liste_expanded)
        return list(liste_expanded)

    def SetFiles(self, files):
        flag = self.data["files"] != files
        self.data["files"] = files
        return flag

    def IsLib(self):
        if self.IsSynthetic():
            return True
        liste = self.GetFilesExpanded()
        if not liste:
            if not self.data["files"]:
                return False
            if self.IsCopyLib():
                liste = self.data["files"]
        if len(liste) == 1:
            ext = os.path.splitext(liste[0])[1].lower()
            if ext != ".fit" and ext != ".fts" and ext != ".fits":
                return False
            nb_image = tools.fit(liste[0]).GetImageNumber()
            return nb_image == 1
        return False

    def IsSynthetic(self):
        return False

    def IsMultiImage(self):
        liste = self.GetFilesExpanded()
        if not liste:
            if not self.data["files"]:
                return False
            if self.IsCopyLib():
                liste = self.data["files"]
        if len(liste) == 1:
            ext = os.path.splitext(liste[0])[1].lower()
            if ext == ".ser":
                return True
            if ext != ".fit" and ext != ".fts" and ext != ".fits":
                return False
            nb_image = tools.fit(liste[0]).GetImageNumber()
            return nb_image > 1
        return False

    def IsCopyLib(self):
        if self.data["type"] != IMAGE:
            return self.data["pp"]["copylib"]
        return False

    def Recopie_data(self):
        return copy.deepcopy(self.data)

# ==============================================================================


class C_Offset(C_IODF):
    def __init__(self):
        C_IODF.__init__(self, OFFSET)
        self.data["stack"]["norm"] = NORM_NO
        self.data["pp"].pop("suboffset")

    def IsSynthetic(self):
        if not self.data["files"]:
            return False
        if len(self.data["files"]) > 1:
            return False
        return (self.data["files"][0][0] == '=')

    def GetValueSynthetic(self):
        if self.IsSynthetic():
            return self.data["files"][0]
        return "=0"

# ==============================================================================


class C_Dark(C_IODF):
    def __init__(self):
        C_IODF.__init__(self, DARK)
        self.data["stack"]["norm"] = NORM_NO

# ==============================================================================


class C_Flat(C_IODF):
    def __init__(self):
        C_IODF.__init__(self, FLAT)
        self.data["stack"]["norm"] = NORM_MUL

# ==============================================================================


class C_DFlat(C_IODF):
    def __init__(self):
        C_IODF.__init__(self, DFLAT)
        self.data["stack"]["Norm"] = NORM_NO

# ==============================================================================


class C_Image(C_IODF):
    def __init__(self):
        C_IODF.__init__(self, IMAGE)

        self.data["fix_fujix"] = False
        self.data["subsky"] = {"degree": 0, "rbf": False,
                               "samples": 20, "tolerance": 1, "smooth": 0.5}

        self.data["stack"]["norm"] = NORM_ADDSCALE
        self.data["stack"]["DarkOpt"] = False
        self.data["stack"]["filter"] = {"fwhm": "", "wfwhm": "", "round": "",
                                        "quality": "", "nbstars": "", "bkg": ""}

        self.data["cosmetic"] = {"enable": False,
                                 "hot": 3, "cold": 3, "bpm": ""}
        self.data["findstar"] = {"enable": False, "sigma": 1.0, "roundness": 0.5,
                                 "radius": 3.0, "focal": 0.0, "pixelsize": 0.0,
                                 "convergence": 1.0, "solver": "gaussian",
                                 "minbeta": 1.0, "relax": False,
                                 "minA": 0.0, "maxA": 1.0}

        self.data["register"] = {"layerstar": "layer 0", "transfo": "",
                                 "interpo": "", "norot": False, "drizzle": False,
                                 "minpairs": "", "maxstars": "", "2pass": False, "noout": False,
                                 "noclamp": False, "selected": False, "nostarslist": False}

        self.data["seqreg"] = {"enable": False, "autoframing": 'cog', "interpo": "", "drizzle": False,
                               "layerstar": "layer 0", "noclamp": False}

        self.data["seqreg"]["filter"] = {"fwhm": "", "wfwhm": "", "round": "",
                                         "quality": "", "nbstars": "", "bkg": ""}
        self.data["pp"].pop("copylib")

        self.data["platesolve"] = {"mode": 0, "focal": 0, "pixelsize": 0.0,
                                   "center": "", "noflip": False, "force": False,
                                   "downscale": False, "localasnet": False,
                                   "limitmag": "", "catalog": "auto"}

    def IsLib(self):
        return False
