# -*- coding: UTF-8 -*-
# ==============================================================================

# ------------------------------------------------------------------------------
# Project: pysiril ( Python SiriL )
#
# This is a library to interface python to Siril.
# ------------------------------------------------------------------------------
#    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"
# ------------------------------------------------------------------------------
from __future__ import absolute_import
__all__=["Siril"]
import sys
import os
import subprocess
import time
import queue
import re

path_exec=os.path.dirname(  __file__ )

if not path_exec :
    path_exec='.'
sys.path.append(path_exec + os.sep + 'lib')

import changelog
import LogMsg
import PipeReader
import PipeWriter
import ThreadSiril
import tools

# ==============================================================================
if sys.platform.startswith('win32'):
    try:
        import win32pipe
        import win32file
        MODULE_LOADED=True
    except:
        MODULE_LOADED=False
else:
    from fcntl import fcntl, F_GETFL, F_SETFL
    from os import O_NONBLOCK, read
    MODULE_LOADED=True

# ------------------------------------------------------------------------------
# Constante
COMPATIBILITY_SIRIL= ("1.2.0.0", "1.2.0.0")

if sys.platform.startswith('win32'):
    SIRIL_CMD_IN  = r'\\.\pipe\siril_command.in'
    SIRIL_CMD_OUT = r'\\.\pipe\siril_command.out'
else:
    SIRIL_CMD_IN  = r'/tmp/siril_command.in'
    SIRIL_CMD_OUT = r'/tmp/siril_command.out'

# ------------------------------------------------------------------------------
class Siril:
    """ Siril is the class that interfaces Python to SiriL via a named pipe

        Parameters:
            - siril_exe : set the executable path of siril

                    By default, it is set to "None". the Siril class will
                    search for the executable in the usual places depending
                    on the platform.

                    Windows: C:/Program Files/SiriL/bin/siril.exe
                          or C:/Program Files (x86)/SiriL/bin/siril.exe
                    darwin: /Applications/Siril.app/Contents/Resources/bin/Siril
                    linux: /usr/bin/siril or /usr/local/bin/siril

            - bStable :  set to False if Siril version is a Developpment version

                    By default, it is set to "True".

            - delai_start : set the delay for Siril to start.

                    By default, it is set to "7 seconds".

            - requires : set the minimum version of Siril required, e.g "0.99.8.1"

                    By default, it is set to "".

        """
    def __init__(self, siril_exe=None, bStable=True, delai_start=7,requires='' ):

        self.tr          = LogMsg.trace()
        self.delai_start = delai_start
        self.bDev        = 0
        self.bReady      = False
        self.bOpened     = False

        self.thReader    = None
        self.PipeWriter  = None
        self.version     = ""
        self.requires    = requires
        self.bStandby    = True

        self.bInfo= True
        #self.tr.Configure(bLog=True,bInfo=self.bInfo,bWarning=True,bError=True)

        if not bStable :
            self.bDev = 1

        if sys.version_info[0] != 3 :
            self.tr.displayraw("\n")
            self.tr.error( "* " + _("Python version") +" :" + str(sys.version_info) )
            self.tr.error( "* " + _("Error, python version should be") + " >= 3 \n" )
            self.tr.Stop()
            raise Exception()

        if MODULE_LOADED == False :
            self.tr.WriteRaw("\n")
            self.tr.error( _("Error import win32pipe & win32file")
                        + "\n\n" + _("The module pywin32 should be downloaded :")
                        + "\n    python.exe -m pip install pypiwin32 "
                        + "\n " + _("or")
                        + "\n    python.exe -m pip install pywin32"
                        + "\n")
            self.tr.Stop()
            raise Exception()

        if siril_exe == None :
            if sys.platform.startswith('win32'):
                siril_exe="C:/Program Files/SiriL/bin/siril-cli.exe"
                if not os.path.exists( siril_exe ) :
                    siril_exe="C:/Program Files (x86)/SiriL/bin/siril-cli.exe"
            if sys.platform.startswith('darwin') :
                siril_exe="/Applications/Siril.app/Contents/Resources/bin/Siril-cli"
                if not os.path.exists( siril_exe ) :
                    siril_exe="/Applications/SiriL.app/Contents/MacOS/siril-cli"    # SiriL Dev
            if sys.platform.startswith('linux') :
               siril_exe="/usr/bin/siril"
               if not os.path.exists( siril_exe ) :
                    siril_exe="/usr/local/bin/siril-cli"
            if siril_exe == None :
                self.tr.error(_("pysiril needs the path  of executable siril") + siril_exe )
                self.tr.Stop()
                raise Exception()

            self.tr.warning(_("pysiril uses by default :") + siril_exe )


        if self.__check_version__(siril_exe) == False :
            self.tr.Stop()
            try:
                raise Exception()
            finally:
                sys.exit()
        self.__check_needstandby__()
        self.siril_exe     = siril_exe
        self.thSiril       = ThreadSiril.ThreadSiril(self.tr,siril_exe)
        self.qStatus       = queue.Queue()
        self.thReader      = PipeReader.ThreadReader(self.tr,self.qStatus,SIRIL_CMD_OUT, self.__check_alive__)
        self.PipeWriter    = PipeWriter.PipeWriter(self.tr,self.qStatus,SIRIL_CMD_IN, self.__check_alive__)
        self.tr.info(_("Initialisation")+" pySiril V" + changelog.NO_VERSION +" : OK")
        self.bReady    = True

    # --------------------------------------------------------------------------
    def Display(self, bLog , bInfo ):
        ''' configure the display of traces

        Parameters:

            - bLog  : set or unset the log trace (False/True)
            - bInfo : set or unset the info trace (False/True)

        Return: None
        '''
        self.tr.Configure(bLog=bInfo,bInfo=bInfo,bWarning=True,bError=True)
    # --------------------------------------------------------------------------
    def MuteSiril(self, bValue=True ):
        ''' configure the display of named pipes

        Parameters:

            - bValue  : set or unset the display of named pipes (False/True)

                    By default, it is set to True (Mute).

        Return: None
        '''
        self.tr.MuteSiril(bValue)
    # --------------------------------------------------------------------------
    def Open(self):
        """ Run SiriL, and next connect to named pipes

        Return: True=Success / False=Aborted
        """
        if not self.bReady :
            self.tr.warning(_("pysiril is not ready"))
            return False
        if self.bOpened :
            self.tr.warning(_("pysiril is already opened"))
            return False

        self.tr.info(_("First step: Starting") +" 'Siril -p' ....")
        self.thSiril.start()
        time.sleep(0.5)
        cnt =  self.delai_start
        self.tr.info(_("waiting: "))
        while cnt :
            time.sleep(1)
            if self.bInfo :
                print(str(cnt)+"s ", end='', flush=True )
            cnt = cnt - 1
            if self.thSiril.IsRunning() == True: break
            if self.thSiril.IsRunning() == False :
                self.tr.info(self.thSiril.GetOutput())
                self.tr.error( ".... " + _("Aborted") + ": 'Siril -p'")
                self.first  =  True
                return False

        if self.bInfo :
            print("")

        self.tr.info(_("Second step: Starting") + " pipe reader ....")
        self.thReader.start()
        if not self.thReader.IsRunning() :
            return False

        self.tr.info(_("Third step: Starting") +" pipe writer ....")
        if not self.PipeWriter.Start() :
            return False

        self.tr.info("Open()")
        self.bOpened = True
        if len(self.requires) > 0:
            self.Execute('requires ' + self.requires)
        else:
            self.Execute('requires ' + self.version.split()[1])
        return True

    # --------------------------------------------------------------------------
    def Close(self):
        ''' Close properly the instance of class before destroy

        Return: None
        '''
        if not self.bReady :
            self.tr.warning(_("pysiril is not ready or closed"))
            self.tr.Stop()
            return
        if not self.bOpened :
            self.tr.warning(_("pysiril is already closed"))
            self.tr.Stop()
            return
        self.thReader.Stop()
        self.PipeWriter.Stop()
        self.bOpened = False
        self.bReady = False
        self.tr.DisplayMsgQueue()
        self.tr.info("Close()")
        self.__del_threads__()

    # --------------------------------------------------------------------------
    def Execute(self, commandes , bEndTest = True ):
        ''' Execute of Siril commands

        Parameters:

            - commandes : string of Siril commands separate by '\n'

        Return:

            - True = Success / False = Aborted

        '''
        if not self.bReady :
            self.tr.warning(_("pysiril is not ready"))
            return False
        for cmd in commandes.split("\n") :
            cmd = re.sub( r'^[ \t]*',r'',cmd)
            cmd = cmd.rstrip() + "\n"
            self.thReader.ClrCR()
            try:
                if not self.PipeWriter.Put(cmd) :
                    return False
            except Exception as e :
                self.tr.error("*** Execute() :" +str(e) +"\n" )
                self.Close()
                return False
        if bEndTest :
            self.__check_endcmd__()
        return True

    def GetData(self):
        ''' Get the data returned by the named pipes

        Return:

            - list of strings

        '''
        return self.thReader.GetCR()
    # --------------------------------------------------------------------------
    def Script(self, scriptfile ):
        ''' Execute of Siril Script File (*.ssf)

        Parameters:

            - scriptfile : name of Siril Script File '\n'

        Return:

            - True = Success / False = Aborted

        '''
        if not os.path.exists( scriptfile ) :
            self.tr.error( scriptfile + " " + _("don't exist") + '!!!')
            return False

        with open(scriptfile) as f:
            lines = f.readlines()

        for line in lines :
            if self.Execute(line) is False :
                return False
        return True


    # --------------------------------------------------------------------------
    def __check_version__(self,siril_exe):
        ''' [internal method] check siril version '''
        cr_ko = False
        cr_ok = True

        # verification de la compatibilite de Siril avec pySiril
        if not os.path.exists( siril_exe ) :
            self.tr.error( _("siril don't exist") + " => " + siril_exe + '?')
            return cr_ko

        try:
            if len(self.requires)==0:
                rVers  =  eval( "[" + COMPATIBILITY_SIRIL[self.bDev].replace('.',',') + "]" )
            else:
                rVers = [int(i) for i in self.requires.split('.')]
                i=len(rVers)
                if len(rVers)<4:
                    while i<4:
                        rVers.append(0)
                        i+=1

            command = [ siril_exe ,'--version' ]
            try:
                if sys.platform.startswith('win32'):
                    SW_HIDE = 0
                    info              = subprocess.STARTUPINFO()
                    info.dwFlags     |= subprocess.STARTF_USESHOWWINDOW
                    info.wShowWindow  = SW_HIDE
                    pipe_subprocess   = subprocess.Popen(command, startupinfo=info, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL, cwd=None )
                else:
                    pipe_subprocess   = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=None,bufsize=-1,universal_newlines=False )
            except Exception as e:
                self.tr.error("*** pipe_subprocess.Popen() :" + str( e ) + '\n')
                return

            try:
                version, errs = pipe_subprocess.communicate(timeout=15)
            except TimeoutExpired:
                self.tr.error("***" + _("Timeout expired : Problem to run 'Siril --version' "))
                pipe_subprocess.kill()
                return cr_ko

            version = (version.decode('utf8')).split('\n')
            found = False
            for vv in version :
                if re.search(r'siril [0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*',vv) :
                    found = True
                    version = vv.strip()
                    break
            if found == False :
                self.tr.error("*** Uncompatible version format :'" + str( version ) + "'\n")
                return

            version_split = version.split(' ')
            sirilname     = version_split[0]
            number        = version_split[1]

            self.tr.info(_("VERSION")+ " "+version+' : ')
            (nMajor,nMinor,nRelease) = number.split('.')[0:3]
            nRelease = nRelease.split('-')
            if type(nRelease) == list :
                nRelease=nRelease[0]

            compatible = False
            if int(nMajor) > rVers[0] :
                compatible = True
            else:
                if int(nMajor) == rVers[0] :
                    if int(nMinor) > rVers[1]  :
                        compatible = True
                    else:
                        if int(nMinor) == rVers[1]  :
                            if int(nRelease) >= rVers[2]:
                                compatible = True
            self.version = version
            if compatible :
                self.tr.info(_("Siril is compatible with pysiril"))
            else:
                self.tr.error(_("Siril is not compatible with pySiril") )
                self.tr.error(_("pySiril requires : Siril ") + str(rVers[0]) +'.'+ str(rVers[1]) +'.'+ str(rVers[2]) +'.'+ str(rVers[3]) )
                return cr_ko
        except Exception as e :
            self.tr.error("***" + _("Error: Siril don't work") + " => " + siril_exe + '?')
            self.tr.error("*** Siril::__check_version__()" + str( e ) + '\n')
            return cr_ko

        return cr_ok


    # --------------------------------------------------------------------------
    def __del_threads__(self):
        ''' [internal method] delete all threads '''
        if self.thReader is not None :
            self.thReader.Stop()
        if self.PipeWriter is not None :
            self.PipeWriter.Stop()
        self.tr.Stop()

    def __check_alive__(self):
        ''' [internal method] checks if all threads are alive '''
        isAlive=True
        if self.thReader is not None :
            isAlive = isAlive and self.thReader.IsRunning()
        if self.PipeWriter is not None :
            isAlive = isAlive and self.PipeWriter.IsRunning()
        if self.thSiril is not None :
            isAlive = isAlive and self.thSiril.IsRunning()
        return isAlive

    def __check_needstandby__(self):
        ''' [internal method] checks if needs 1s standby after each command - not required if >= 0.99.8 '''
        number = self.version.split(' ')[1].split('-')[0]
        (nMajor,nMinor,nRelease) = number.split('.')[0:3]
        needstandby = True
        if int(nMajor) > 0 :
            needstandby = False
        else:
            if int(nMajor) == 0 :
                if int(nMinor) > 99  :
                    needstandby = False
                else:
                    if int(nMinor) == 99  :
                        if int(nRelease) >= 8:
                            needstandby = False
        self.bStandby=needstandby


    def __check_endcmd__(self):
        ''' [internal method] checks if the command is finished '''
        if self.bStandby:
            avant = self.GetData()
            time.sleep(1) # attente car le resultat arrive apres [status: success]
            apres = self.GetData()
            if len(avant ) != len(apres) :
                self.tr.warning("__check_endcmd__() *** before timout:", avant )
                self.tr.warning("__check_endcmd__() *** After timout:", apres)

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

if __name__ == '__main__':
    print("Test:begin")
    app=Siril()
    app.Open()
    app.Execute("cd ..")
    app.Close()
    del app
    print("Test:end")

