#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net )

This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),

This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""

import logging; logger = logging.getLogger(__name__); logger.info("import")

#Python Standard Library
import os
import os.path
import json
import atexit
from sys import exit as sysexit

#Third Party Modules
from calfbox import cbox

#Our Template Modules
from .history import History
from .duration import DB, DL, D1, D2, D4, D8, D16, D32, D64, D128, MAXIMUM_TICK_DURATION

#User Data
from engine.config import *
from engine.main import Data

class Session(object):

    def __init__(self):
        self.sessionPrefix = "" #set in nsm_openOrNewCallback. If we want to save and load resources they need to be in the session dir. We never load from outside, the scheme is always "import first, load local file"
        self.absoluteJsonFilePath = "" #set in nsm_openOrNewCallback. Makes it possible to save a file manually. Even in script mode or debug mode.
        self.guiWasSavedAsNSMVisible = False #set by nsm_openOrNewCallback, but without a save file we start with an initially hidden GUI
        self.nsmClient = None #We get it from api.startEngine which gets it from the GUI. nsmClient.reactToMessage is added to the global event loop there.
        self.history = History() #Undo and Redo. Works through the api but is saved in the session. Not saved in the save file.
        self.guiSharedDataToSave = {} #the gui can write its values here directly to get them saved and restored on startup. We opt not to use the Qt config save to keep everything in one file.
        self.recordingEnabled = False #MidiInput callbacks can use this to prevent/allow data creation. Handled via api callback. Saved.
        self.eventLoop = None # added in api.startEngine
        self.data = None #nsm_openOrNewCallback
        self.standaloneMode = None #fake NSM single server for LaborejoSoftwareSuite programs or not. Set in nsm_openOrNewCallback

    def addSessionPrefix(self, jsonDataAsString:str):
        """During load the current session prefix gets added. Turning pseudo-relative paths into
        absolute paths.

        This is meant for imported resources, e.g. a soundfont, which need to be handled as abolute
        paths while the program is running (e.g. for fluidsynth). It leaves our engine functions
        cleaner because they do not know if they are under session management or not.

        It assumes that the session dir does not change during runtime, which is a reasonable
        assumption."""
        assert type(jsonDataAsString) is str, type(jsonDataAsString)
        assert self.sessionPrefix, self.sessionPrefix
        return jsonDataAsString.replace("<sessionDirectory>", self.sessionPrefix)

    def removeSessionPrefix(self, jsonDataAsString:str):
        """During saving the current session prefix gets removed from all absolute paths leaving
        pseudo-relative paths "<sessionDirectory>" behind.

        This is meant for imported resources, e.g. a soundfont, which need to be handled as abolute
        paths while the program is running (e.g. for fluidsynth). It leaves our engine functions
        cleaner because they do not know if they are under session management or not.

        It assumes that the session dir does not change during runtime, which is a reasonable
        assumption."""
        assert type(jsonDataAsString) is str, type(jsonDataAsString)
        return jsonDataAsString.replace(self.sessionPrefix, "<sessionDirectory>")

    def nsm_openOrNewCallback(self, ourPath, sessionName, ourClientNameUnderNSM):
        logger.info("New/Open session")
        cbox.init_engine("")

        #Most set config must be called before start audio.  Set audio outputs seems to be an exception.
        cbox.Config.set("io", "client_name", ourClientNameUnderNSM)
        cbox.Config.set("io", "enable_common_midi_input", 0) #remove the default "catch all" midi input
        cbox.Config.set("io", "outputs", METADATA["cboxOutputs"])

        #A workaround to have the program adopt to JACK transport position. It already tries but there is no song yet, so it is song-duration=0 and setting transport is not possible beyond song duration.
        #We fake a song length which will shortly be set to the real size.
        cbox.Document.get_song().set_loop(MAXIMUM_TICK_DURATION, MAXIMUM_TICK_DURATION)
        cbox.Document.get_song().update_playback()

        #Now we can start audio with a fake song length
        cbox.start_audio()
        atexit.register(self.stopSession) #this will handle all python exceptions, but not segfaults of C modules.

        cbox.do_cmd("/master/set_ppqn_factor", None, [D4]) #quarter note has how many ticks? needs to be in a list.

        self.sessionPrefix = ourPath #if we want to save and load resources they need to be in the session dir. We never load from outside, the scheme is always "import first, load local file"
        self.absoluteJsonFilePath = os.path.join(ourPath,  "save." + METADATA["shortName"] + ".json")

        self.standaloneMode = sessionName == "NOT-A-SESSION"

        try:
            self.data = self.openFromJson(self.absoluteJsonFilePath)
        except FileNotFoundError:
            self.data = None #This makes debugging output nicer. If we init Data() here all errors will be presented as follow-up error "while handling exception FileNotFoundError".
        except (NotADirectoryError, PermissionError) as e:
            self.data = None
            logger.error("Will not load or save because: " + e.__repr__())
        if not self.data:
            self.data = Data(parentSession = self)
        logger.info("New/Open session complete")

    def openFromJson(self, absoluteJsonFilePath):
        logger.info("Loading file start")
        with open(absoluteJsonFilePath, "r", encoding="utf-8") as f:
            try:
                text = self.addSessionPrefix(f.read())
                result = json.loads(text)
            except Exception as error:
                logger.error(error)

        if result and "version" in result and "origin" in result and result["origin"] == METADATA["url"]:
            if METADATA["version"] >= result["version"]: #Achtung. This only works because Python can compare Strings this way!
                self.guiWasSavedAsNSMVisible = result["guiWasSavedAsNSMVisible"]
                if "recordingEnabled" in result: #introduced in april 2020
                    self.recordingEnabled = result["recordingEnabled"]
                self.guiSharedDataToSave = result["guiSharedDataToSave"]
                assert type(self.guiSharedDataToSave) is dict, self.guiSharedDataToSave
                logger.info("Loading file complete")
                return Data.instanceFromSerializedData(parentSession=self, serializedData=result)
            else:
                logger.error(f"""{absoluteJsonFilePath} was saved with {result["version"]} but we need {METADATA["version"]}""")
                sysexit()
        else:
            logger.error(f"""Error. {absoluteJsonFilePath} not loaded. Not a sane {METADATA["name"]} file in json format""")
            sysexit()


    def nsm_saveCallback(self, ourPath, sessionName, ourClientNameUnderNSM):
        #not neccessary. NSMClient does that for us. self.nsmClient.announceSaveStatus(True)
        try:
            if not os.path.exists(ourPath):
                os.makedirs(ourPath)
        except Exception as e:
            logger.error("Will not load or save because: " + e.__repr__())

        result = self.data.serialize()
        result["origin"] = METADATA["url"]
        result["version"] = METADATA["version"]
        result["recordingEnabled"] = self.recordingEnabled
        result["guiWasSavedAsNSMVisible"] = self.nsmClient.isVisible
        result["guiSharedDataToSave"] = self.guiSharedDataToSave
        #result["savedOn"] = datetime.now().isoformat() #this is inconvenient for git commits. Even a save without no changes will trigger a git diff.
        jsonData = json.dumps(result, indent=2)
        jsonData = self.removeSessionPrefix(jsonData)

        #if result and jsonData: #make a test here to make sure that the data is not wiped out or corrupted
        try:
            with open(self.absoluteJsonFilePath, "w", encoding="utf-8") as f:
                f.write(jsonData)
        except Exception as e:
            logger.error("Will not load or save because: " + e.__repr__())

        logger.info("Saving file complete")
        return self.absoluteJsonFilePath

    def stopSession(self):
        """This got registered with atexit in the nsm new or open callback above.
        will handle all python exceptions, but not segfaults of C modules. """
        logger.info("Starting Quit through @atexit, session.stopSession")
        self.eventLoop.stop()
        logger.info("@atexit: Event loop stopped")
        #Don't do that. We are just a client.
        #cbox.Transport.stop()
        #logger.info("@atexit: Calfbox Transport stopped ")
        cbox.stop_audio()
        logger.info("@atexit: Calfbox Audio stopped ")
        cbox.shutdown_engine()
        logger.info("@atexit: Calfbox Engine shutdown ")
