#!/usr/bin/env python3

import json as JSON
import os as OS

class JSONDataBase:
    __version__ = 1
    __author__  = "Manuel Baumi <koenigm771@gmail.com>"

    ### Errors
    class UnknownHeader(Exception): pass
    class UnknownEnviromentVariableValue(Exception): pass
    class AlreadyExisting(Exception): pass

    ### Dunder
    def __init__(self, FileName):
        self.FilePath = f"{self._GetCurrentPath()}/{FileName}.json"

        try:
            with open(self.FilePath, "r") as f:
                if len(f.read()) < 10:
                    self._InitFile()
        except FileNotFoundError:
            self._InitFile()

    def __getitem__(self, Key):
        match self.GetEnviroment("BaseObjectIndexingMode"):
            case False:
                raise self.UnknownEnviromentVariableValue("Enviroment (\"BaseObjectIndexingMode\") not set up!")
            case "ReturnDataBaseName":
                return self._GetAllDataBaseNames()[Key]
            case "ReturnDataBaseValues":
                return self._GetJSONByPath(f"DataBase/{self._GetAllDataBaseNames()[Key]}")
            case _ as Mode:
                raise self.UnknownEnviromentVariableValue(Mode)

    ### Enviroment Logic
    def SetEnviroment(self, Key, Value):
        self._SetJSONByPath(f"Enviroment/{Key}", Value)
    def GetEnviroment(self, Key):
        return self._GetJSONByPath(f"Enviroment/{Key}")
    def LoadEnviroment(self):
        return self._GetJSONByPath("Enviroment")

    ### Header Logic
    def AppendHeader(self, DataBase, Header, Update=True):
        Value = self._GetJSONByPath(f"Enviroment/Header\'{DataBase}")
        if Header not in Value:
            Value.append(Header)
        self._SetJSONByPath(f"Enviroment/Header\'{DataBase}", Value)

        if Update:
            self._UpdateDataBase(DataBase)
    def DeleteHeader(self, DataBase, Header, Update=True):
        Value = self._GetJSONByPath(f"Enviroment/Header\'{DataBase}")
        Value.pop(Value.index(Header))
        self._SetJSONByPath(f"Enviroment/Header\'{DataBase}", Value)

        if Update:
            self._UpdateDataBase(DataBase)

    def DeleteMultipleHeaders(self, DataBase, *Headers):
        for Header in Headers:
            self.DeleteHeader(DataBase, Header, Update=False)

        self._UpdateDataBase(DataBase)
    def SetHeaders(self, DataBase, *Headers):
        for Header in Headers:
            self.AppendHeader(DataBase, Header, Update=False)

        self._UpdateDataBase(DataBase)
    def GetHeaders(self, DataBase):
        return self._GetJSONByPath(f"Enviroment/Header\'{DataBase}")
    
    def _CreateHeaders(self, DataBase):
        self._SetJSONByPath(f"Enviroment/Header\'{DataBase}", [])
    def _UpdateDataBase(self, DataBase):
        HeadersSet = set(self.GetHeaders(DataBase))
        Values = self._GetJSONByPath(f"DataBase/{DataBase}")

        for ID, Value in Values.items():
            ValuesSet = set(Value)

            Missings = HeadersSet - ValuesSet
            Deprecateds = ValuesSet - HeadersSet

            for Missing in Missings:
                Value[Missing] = None
            for Deprecated in Deprecateds:
                del Value[Deprecated]
        
        self._SetJSONByPath(f"DataBase/{DataBase}", Values)
    def _DeleteHeaders(self, DataBase):
        self._SetJSONByPath(f"Enviroment/Header\'{DataBase}", [])
    
    ### Other Useful Functions
    @staticmethod
    def ConcatinateSubValue(*Values):
        return "|".join(Values)
    
    @staticmethod
    def ConcatinateSubValueByOtherConcatination(Values, OtherSplit):
        return "|".join([Value for Value in Values.split(OtherSplit)])
    
    ### DataBase Management Logic
    # Gets Next ID for Database
    def _AssignID(self, DataBase):
        if len(Keys := list(self._GetJSONByPath(f"DataBase/{DataBase}").keys())) != 0:
            return max(map(int, Keys)) + 1
        return 0

    def CreateDataBase(self, Name, *Headers):
        if Name in self._GetAllDataBaseNames():
            raise self.AlreadyExisting(f"DataBase {Name} already Exists!")

        Value = self._GetJSONByPath(f"DataBase")
        Value[Name] = {}
        self._SetJSONByPath("DataBase", Value)

        self._CreateHeaders(Name)
        self.SetHeaders(Name, *Headers)

    def PurgeDataBase(self, DataBase):
        self._SetJSONByPath(f"DataBase/{DataBase}", {})
        self._DeleteHeaders(DataBase)

    def DeleteDataBase(self, DataBase):
        Value = self._GetJSONByPath("DataBase")
        del Value[DataBase]
        self._SetJSONByPath("DataBase", Value)
        self._DeleteHeaders(DataBase)

    def AppendValue(self, DataBase, **HeaderValues):
        ID = self._AssignID(DataBase)
        Headers = self.GetHeaders(DataBase)

        Value = {}

        TheDataBase = self._GetJSONByPath(f"DataBase/{DataBase}")

        for Header, SubValue in HeaderValues.items():
            if Header not in Headers:
                raise self.UnknownHeader(f"{DataBase}->{Header}?")
            Value[Header] = SubValue

        for Header in Headers:
            if Header not in Value:
                Value[Header] = None
        
        TheDataBase[ID] = Value

        self._SetJSONByPath(f"DataBase/{DataBase}", TheDataBase)

    def DeleteValue(self, DataBase, ID):
        Value = self._GetJSONByPath(f"DataBase/{DataBase}")
        del Value[ID]
        self._SetJSONByPath(f"DataBase/{DataBase}", Value)

    def AlterValue(self, DataBase, ID, **HeaderSubValue):
        Value = self._GetJSONByPath(f"DataBase/{DataBase}/{ID}")
        Headers = self.GetHeaders(DataBase)

        for Header, SubValue in HeaderSubValue.items():
            if Header not in Headers:
                raise self.UnknownHeader(f"{DataBase}->{Header}?")
            Value[Header] = SubValue

        self._SetJSONByPath(f"DataBase/{DataBase}/{ID}", Value)

    def GetCompleteViews(self, DataBase):
        Values = self._GetJSONByPath(f"DataBase/{DataBase}")
        TheValues = []
        for ID, Value in Values.items():
            TheValues.append(self._CreateView(ID, Value))

        return TheValues
    
    def GetPartialViews(self, DataBase, *Headers):
        Values = self._GetJSONByPath(f"DataBase/{DataBase}")
        TheValues = []

        for ID, Value in Values.items():
            PartialValue = {}
            for Header in Headers:
                PartialValue[Header] = Value[Header]
            TheValues.append(self._CreateView(ID, PartialValue))

        return TheValues

    ### Copying and Sorting Views Logic
    def CopyAllDistinctOfOneHeader(self, Views, Header):
        TheViews = set()

        for View in Views:
            TheViews.add(View["Value"][Header])

        return list(TheViews)
    
    @staticmethod
    def CopyViewsByAmount(Views, Amount):
        return Views[:Amount]

    @staticmethod
    def CopyViewsByCondition_AND_NAND(Views, **HeaderSubValues):
        """
        Returns Tuple:
        [0] -> Views where all given HeaderSubValues are True.
        [1] -> Views where not all given HeaderSubValues are True.
        """
        
        TheViews = [], []
        MatchesTotalNeeded = len(HeaderSubValues)

        for View in Views:
            Matches = 0
            for Header, SubValue in HeaderSubValues.items():
                if View["Value"][Header] == SubValue:
                    Matches += 1
            if Matches == MatchesTotalNeeded:
                TheViews[0].append(View)
            else:
                TheViews[1].append(View)

        return TheViews
    
    @staticmethod
    def CopyViewsByCondition_OR_NOT(Views, **HeaderSubValues):
        """
        Returns Tuple:
        [0] -> Views where one or more given HeaderSubValues are True.
        [1] -> Views where no given HeaderSubValues are True.
        """

        TheViews = [], []

        for View in Views:
            Matches = 0
            for Header, SubValue in HeaderSubValues.items():
                if View["Value"][Header] == SubValue:
                    Matches += 1
            if Matches >= 1:
                TheViews[0].append(View)
            else:
                TheViews[1].append(View)

        return TheViews
    
    @staticmethod
    def CopyViewsByCondition_KMatch(Views, Truth=1, **HeaderSubValues):
        """
        Returns Tuple:
        [0] -> Views where a given amount (Truth) of given HeaderSubValues are True.
               E.g. for Truth=2 teh View must have two HeaderSubValues True.
        [1] -> Views where not a given amount (Truth) of given HeaderSubValues are True.
        """

        TheViews = [], []

        for View in Views:
            Matches = 0
            for Header, SubValue in HeaderSubValues.items():
                if View["Value"][Header] == SubValue:
                    Matches += 1
            if Matches == Truth:
                TheViews[0].append(View)
            else:
                TheViews[1].append(View)

        return TheViews
    
    @staticmethod
    def OrderViewsByAlphabeticalHeader(Views, Header, Reverse=False):
        return sorted(Views, key=lambda View: View["Value"][Header], reverse=Reverse)
    
    @staticmethod
    def OrderViewsByID(Views, Reverse=False):
        return sorted(Views, key=lambda View: int(View["ID"]), reverse=Reverse)

    def _GetAllDataBaseNames(self):
        return [Name for Name in self._GetJSONByPath("DataBase")]

    @staticmethod
    def _CreateView(ID, Value):
        return {"ID": ID, "Value": Value}
    
    @staticmethod
    def _FormatJSON(Value, Indent = "\t"):
        return JSON.dumps(Value, indent=Indent)

    def _GetJSON(self):
        with open(self.FilePath, "rt") as f:
            return JSON.load(f)
        
    def _GetJSONByPath(self, Path):
        JSONFetch = self._GetJSON()

        for SubPath in Path.split("/"):
            JSONFetch = JSONFetch[SubPath]

        return JSONFetch
    
    def _SetJSON(self, Value):
        with open(self.FilePath, "wt") as f:
            JSON.dump(Value, f, indent="\t")

        self.RedundancyCopy(Value)

    def _SetJSONByPath(self, Path, Value):
        JSONFetch = self._GetJSON()

        JSONFetchForOverwrite = JSONFetch

        for SubPath in Path.split("/")[:-1]:
            JSONFetchForOverwrite = JSONFetchForOverwrite[SubPath]

        JSONFetchForOverwrite[Path.split("/")[-1]] = Value

        self._SetJSON(JSONFetch)

    @staticmethod
    def _CreateFile(SystemPath):
        with open(f"{SystemPath}", "x") as f: pass

    @staticmethod
    def _DeleteFile(SystemPath):
        OS.remove(SystemPath)

    def _InitFile(self):
        try:
            self._CreateFile(self.FilePath)
        except FileExistsError: pass
        finally:
            self._SetJSON({
                "Enviroment": {
                    "BaseObjectIndexingMode": False,
                    "RedundancyFilePath": False,
                    "SeperateLogFilePath": False,
                    "Logs": []
                },
                "DataBase": {}
            })

    @staticmethod
    def _GetCurrentPath():
        return "/".join(__file__.split("/")[:-1])
    
    def RedundancyCopy(self, Value):
        if (RedundancyFilePath := self.GetEnviroment("RedundancyFilePath")):
            try:
                self._CreateFile(RedundancyFilePath)
            except FileExistsError: pass
            finally:
                with open(RedundancyFilePath, "wt") as f:
                    JSON.dump(Value, f, indent="\t")

if __name__ == "__main__":
    J = JSONDataBase("db")

    print(J._FormatJSON(J.CopyViewsByAmount(J.GetCompleteViews("Books"), 2)))
    J.AppendHeader("Books", "Cover")
    print(J._FormatJSON(J.CopyViewsByAmount(J.GetCompleteViews("Books"), 2)))
    J.DeleteHeader("Books", "Cover")
    print(J._FormatJSON(J.CopyViewsByAmount(J.GetCompleteViews("Books"), 2)))