import sys, socket, subprocess, time, os, json
from enum import Enum
from configparser import ConfigParser


class ErrorCodes(Enum):
  NO_ERROR = 0
  ERROR_NO_CONFIG_FOUND = 1
  ERROR_INCOMPLETE_CONFIG = 2
  ERROR_BINDING_SOCKET = 3
  ERROR_CONNECTING_SOCKET = 4
  ERROR_MISSING_VA_INI = 5
  ERROR_INVALID_REPRODUCTION_ID = 6
  ERROR_INCOMPLETE_VA_INI = 7
  ERROR_UNDEFINED_LAUNCHER_STATE = 8

class ReproductionInput(Enum):
  NOT_SPECIFIED = 0
  BINAURAL = 1
  AMBISONICS = 2
  CUSTOM = 3

# Class representing the VA-Launcher config (.json) file
class LauncherConfig:
  def __init__(conf, sConfigFile):
    try:
      with open( sConfigFile ) as json_file:
        json_config = json.load(json_file)
    except Exception as e:
      print( "ERROR reading the json config:" + os.linesep + str(e) )
      sys.exit( ErrorCodes.ERROR_INCOMPLETE_CONFIG )

    try:
      conf.dVirtualAcousticDirectories = json_config["dVAServerDirectories"]
      conf.sLocalIP = json_config["sLocalIP"]
      conf.nLauncherPort = json_config["nLauncherPort"]
      conf.nVAServerPort = json_config["nVAServerPort"]
      conf.nWaitForVAServerStart = json_config["nWaitForVAServerStart"]
    except KeyError as e:
      print( "ERROR reading the json config. Missing " + str(e.args[0]) )
      sys.exit( ErrorCodes.ERROR_INCOMPLETE_CONFIG )
    
    try:
      conf.sVASetupIni = json_config["sVASetupIni"]
    except KeyError:
      conf.sVASetupIni = None

    try:
      conf.lsBinauralReproductionModules = json_config["lsBinauralReproductionModules"]
    except KeyError:
      conf.lsBinauralReproductionModules = None
    try:
      conf.lsAmbisonicsReproductionModules = json_config["lsAmbisonicsReproductionModules"]
    except KeyError:
      conf.lsAmbisonicsReproductionModules = None
    try:
      conf.lsCustomReproductionModules = json_config["lsCustomReproductionModules"]
    except KeyError:
      conf.lsCustomReproductionModules = None


class VAComposedIniParser:
  def __init__(self, oLauncherConf : LauncherConfig):
    self.sVASetupIni = oLauncherConf.sVASetupIni
    self.lsBinauralReproductionModules = oLauncherConf.lsBinauralReproductionModules
    self.lsAmbisonicsReproductionModules = oLauncherConf.lsAmbisonicsReproductionModules
    self.lsCustomReproductionModules = oLauncherConf.lsCustomReproductionModules
    self.eReproductionInput = ReproductionInput.NOT_SPECIFIED
    self.sRendererIniPath = None
    self.sConfFolder = "../conf/"

  def _create_parser(self):
    parser = ConfigParser()
    parser.optionxform = str
    return parser

  def get_main_inifile(self):
    return self.sConfFolder + "VACore.Main.ini"

  def prepare_inis(self):
    print("Parsing VA ini-files")
    self.link_setup_and_renderer_ini()
    return self.prepare_reproduction_ini()

  def link_setup_and_renderer_ini(self):
    if not self.sRendererIniPath:
      print("No renderer ini-file sent by client, using default one: 'VARenderer.Default.ini'")
      self.sRendererIniPath = "VARenderer.Default.ini"
    if not self.sVASetupIni:
      print("No setup ini-file specified, using default one: 'VASetup.Launcher.ini'")
      self.sVASetupIni = "VASetup.Launcher.ini"
    
    sMainInifile = self.get_main_inifile()
    if not os.path.isfile(sMainInifile):
      print("ERROR: Could not find 'VACore.Main.ini' in 'conf' folder. Did you delete it?")
      sys.exit( ErrorCodes.ERROR_MISSING_VA_INI )

    mainIni = self._create_parser()
    try:
      mainIni.read( sMainInifile )
      mainIni["Files"]["VARendererIni"] = self.sRendererIniPath
      mainIni["Files"]["VASetupIni"] = self.sVASetupIni
    except Exception as e:
      print( "ERROR: " + str(e) )
      sys.exit( ErrorCodes.ERROR_INCOMPLETE_VA_INI )
    
    with open( sMainInifile, 'w' ) as inifile:
      mainIni.write(inifile)
  
  def prepare_reproduction_ini(self):
    sFileToRead = self.sConfFolder + "VAReproduction.Prototype.ini"
    sFileToWrite = self.sConfFolder + "VAReproduction.ini"
    if not os.path.isfile(sFileToRead):
      print("ERROR: Could not find 'VAReproduction.Prototype.ini' in 'conf' folder. Did you delete it?")
      sys.exit( ErrorCodes.ERROR_MISSING_VA_INI )

    reproductionIni = self._create_parser()
    try:
      reproductionIni.read( sFileToRead )
    except Exception as e:
      print( "ERROR: " + str(e) )
      sys.exit( ErrorCodes.ERROR_INCOMPLETE_VA_INI )
    
    if self.eReproductionInput == ReproductionInput.BINAURAL:
      if self.lsBinauralReproductionModules:
        lsReproductionModuleIDs = self.lsBinauralReproductionModules
      else:
        print("ERROR: Requested BINAURAL reproduction modules are not specified")
        return False
    elif self.eReproductionInput == ReproductionInput.AMBISONICS:
      if self.lsAmbisonicsReproductionModules:
        lsReproductionModuleIDs = self.lsAmbisonicsReproductionModules
      else:
        print("ERROR: Requested AMBISONICS reproduction modules are not specified")
        return False
    elif self.eReproductionInput == ReproductionInput.CUSTOM:
      if self.lsCustomReproductionModules:
        lsReproductionModuleIDs = self.lsCustomReproductionModules
      else:
        print("ERROR: Requested CUSTOM reproduction modules are not specified")
        return False

    for sReproductionID in lsReproductionModuleIDs:
      sSection = "Reproduction:" + sReproductionID
      if not reproductionIni.has_section(sSection):
        print( "ERROR: Reproduction module with ID: '" + sReproductionID  + "' not available in respective ini file '" + sFileToRead + "'")
        #sys.exit( ErrorCodes.ERROR_INVALID_REPRODUCTION_ID )
        return False
      reproductionIni[sSection]["Enabled"] = "True"
    
    with open( sFileToWrite, 'w' ) as inifile:
      reproductionIni.write(inifile)
    
    return True


#Main class representing the VA Launcher app
class VirtualAcousticsLauncher:
  def __init__(self):
    print("init")
    self.oConfig = None
    self.vaIniParser = None
    self.oVAProcess = None 
    self.oLauncherServerSocket = None 
    self.oLauncherConnection = None 
    self.sCurrentScriptsDirectory = os.path.dirname( os.path.realpath( sys.argv[0] ) )
    self.sVAServerID = None
    self.sVAServerDir = None

    self.start()

  #Start the launcher
  def start(self):
    print( "VirtualAcoustics Starter script - press ctrl+c to quit" )
    self.read_config()
    self.open_server_socket()
    self.main_loop()


  #Reads the launcher config and initializes all respective class parameters
  def read_config(self):
    sHostConfigurationFile = "VirtualAcousticStarterConfig." + socket.gethostname() + ".json"
    sGeneralConfigurationFile = "VirtualAcousticStarterConfig.json"

    #check which config file exists and load it
    sUsedConfigFile = ""
    if os.path.isfile( sHostConfigurationFile ):
      sUsedConfigFile = sHostConfigurationFile
    elif os.path.isfile( self.sCurrentScriptsDirectory + "/" + sHostConfigurationFile ):
      sUsedConfigFile = self.sCurrentScriptsDirectory + "/" + sHostConfigurationFile
    elif os.path.isfile( sGeneralConfigurationFile ):
      sUsedConfigFile = sGeneralConfigurationFile
    elif os.path.isfile( self.sCurrentScriptsDirectory + "/" + sGeneralConfigurationFile ):
      sUsedConfigFile = self.sCurrentScriptsDirectory + "/" + sGeneralConfigurationFile
    else:
      print( "ERROR: No configuration file found - please create " + sHostConfigurationFile + " or " + sGeneralConfigurationFile )
      sys.exit( ErrorCodes.ERROR_NO_CONFIG_FOUND )

    print("Using config: " + sUsedConfigFile)
    self.oConfig = LauncherConfig( sUsedConfigFile )

    self.vaIniParser = VAComposedIniParser(self.oConfig)

  #Open network socket used for the communication
  def open_server_socket(self):
    print( "Creating server socket at " + self.oConfig.sLocalIP + ":" + str( self.oConfig.nLauncherPort ) )
    
    self.oLauncherServerSocket = socket.socket()
    if self.oConfig.sLocalIP == "":
      self.oConfig.sLocalIP = socket.gethostname()
    try:
      self.oLauncherServerSocket.bind( ( self.oConfig.sLocalIP, self.oConfig.nLauncherPort ) )
      self.oLauncherServerSocket.listen( 3 ) 
    except socket.error:
      print( "Error on binding socket" )
      sys.exit( ErrorCodes.ERROR_BINDING_SOCKET )
    self.oLauncherServerSocket.settimeout( 1.0 )

  def _reset_connection(self):
    if self.oLauncherConnection:
      self.oLauncherConnection.close()
      self.oLauncherConnection = None
    self.sVAServerDir = None
    self.sVAServerID = None
    self.vaIniParser.sRendererIniPath = None

  def _close_va_and_reset_connection(self):
    if self.oVAProcess:
      print( "Closing VA instance" )
      self.oVAProcess.terminate()
      time.sleep( 1 )
      self.oVAProcess.kill()
      self.oVAProcess = None
    self._reset_connection()

  def main_loop(self):
    try:
      while True:
        if not self.oLauncherConnection:
          self.wait_for_connection()
            
        elif self.sVAServerDir and not self.oVAProcess:
          self.start_va_server()

        elif self.oVAProcess:
          self.listen_for_requests()

        else:
          print("ERROR: Undefined state launcher state leading to infinite loop")
          sys.exit( ErrorCodes.ERROR_UNDEFINED_LAUNCHER_STATE )

    except KeyboardInterrupt:
      print( "Caught keyboard interrupt, quitting" )

    self._reset_connection()

  def wait_for_connection(self):
    print( "Waiting for launcher connection..." )
    while not self.oLauncherConnection:
      try:
        self.oLauncherConnection, sAddress = self.oLauncherServerSocket.accept()
      except socket.timeout:
        self.oLauncherConnection = None
      except socket.error:
        print( "Error while listening for launcher connection" )
        sys.exit( ErrorCodes.ERROR_CONNECTING_SOCKET )
      except (KeyboardInterrupt, SystemExit):
        raise #re-raising the received exception

    print( "Connection received from " + sAddress[0] )
    
    if self.oVAProcess:
      print( "Closing current VA instance" )
      self.oVAProcess.kill()
      self.oVAProcess = None

    self.receive_va_start_info()
    if not self.vaIniParser.prepare_inis():
      print( "Resetting launcher connection" )
      self._reset_connection()


  #Checks for a message containing the ID of the VAServer instance to be started and returns the respective VAServer directory
  def receive_va_start_info(self):
    try:
      sMessage = self.oLauncherConnection.recv( 512 )
      if type( sMessage ) is bytes:
        sMessage = sMessage.decode( 'utf-8' )
      
      if ":" not in sMessage: #VAServer ID, should be received last
        self.sVAServerID = sMessage
      elif sMessage.startswith("reproduction_input_type:"): #ReproductionInput type (Binaural / Ambisonics), optional
        self.receive_reproduction_input(sMessage)
        return self.receive_va_start_info()
      elif sMessage.startswith("file:"): #VARenderer.ini file, optional
        self.vaIniParser.sRendererIniPath = self.receive_file(sMessage)
        return self.receive_va_start_info()
      else:
        lMessageParts = sMessage.split(":")
        print("ERROR: Invalid message keyword '" + lMessageParts[0] + "' while receiving VA start info")
        return False

      print( "Received launch request for VAServer ID: " + self.sVAServerID )
    except socket.error:
      print( "ERROR: Socket error while reading VAServer ID" )
      self._reset_connection()
      return False
    else:
      try:
        self.sVAServerDir = self.oConfig.dVirtualAcousticDirectories[self.sVAServerID]
      except KeyError:
        self.sVAServerDir = None

      if not self.sVAServerDir:
        print( 'Requested VA Instance "' + self.sVAServerID + '" not available' )
        self.oLauncherConnection.send( b'f' ) #answer 'requested version not available
        self._reset_connection()
        return False

      return True

  def receive_reproduction_input(self, sMessage):
    try:
      lsMessageParts = sMessage.split(":")
      sReproductionInput = lsMessageParts[1].upper()
      self.vaIniParser.eReproductionInput = ReproductionInput[sReproductionInput]
    except IndexError:
      print("ERROR: Message for receiving reproduction input type was empty")
      self.oLauncherConnection.send( b'fail' )
    except ValueError:
      print("ERROR: Invalid ID (case-insensitive) for reproduction input: '" + sReproductionInput + "'")
      self.oLauncherConnection.send( b'fail' )
    else: #send acceptance
      self.oLauncherConnection.send( b'ack' )


  #Starts the VAServer from given directory
  def start_va_server(self):

    # Check for VAServer.exe
    sVAExecutableFile = "bin/VAServer.exe"
    try:
      if not os.path.isfile( sVAExecutableFile ):
        if self.sVAServerDir and os.path.isfile( self.sVAServerDir + "/" + sVAExecutableFile ):
          sVAExecutableFile = self.sVAServerDir + "/" + sVAExecutableFile
        else:
          print( "ERROR: Invalid config for " + self.sVAServerID + " -- file " + sVAExecutableFile + " does not exist" )
          self.oLauncherConnection.send( b'n' ) #answer 'binary file cannot be found or invalid'
          return
    except KeyError:
      sVAExecutableFile = None
      print( "ERROR: config for " + self.sVAServerID + " has no valid \"file\" entry" )
      self.oLauncherConnection.send( b'i' ) #answer 'invalid file entry in the config'
      self._reset_connection()
      return

    if not sVAExecutableFile:
      return

    # Create start command
    sConnectionParam = self.oConfig.sLocalIP + ":" + str( self.oConfig.nVAServerPort )
    sVACoreIniParam = self.vaIniParser.get_main_inifile()
    sParams = sConnectionParam + " " + sVACoreIniParam
    sCommand = sVAExecutableFile + " " + sParams
    
    # start instance
    print( 'executing "' + sCommand + '"' )
          
    self.oVAProcess = subprocess.Popen( sCommand, cwd = self.sVAServerDir, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP )

    # wait for requested duration before sending the go signal
    time.sleep( self.oConfig.nWaitForVAServerStart )

    if self.oVAProcess.poll() != None:
      print( "VA Process died - sending abort token" )
      self.oLauncherConnection.send( b'a' ) #answer 'VAServer was aborted
      self._reset_connection()
      return
    else:        
      print( "sending go token" )
      self.oLauncherConnection.send( b'g' )#answer 'go, VAServer is correctly started'

  #Listens for requests while the VAServer is running
  def listen_for_requests(self):
    while True:
      try:
        sResult = self.oLauncherConnection.recv( 512 )

        if sResult != '':
          #check whether we are about to reveive a file
          sMessage = sResult.decode("utf-8")
          if sMessage.startswith("file"):
            self.receive_file(sMessage)
          else: #NOT sMessage.startswith("file")      
            print( "Received quit event: " + sMessage )
            self._close_va_and_reset_connection()
            break

      except socket.timeout:
        # timeouts are okay
        print( "Launcher Socket timeout, keep running anyways" )
      except socket.error:
        print( "Launcher Connection terminated unexpectedly" )
        break
      except (KeyboardInterrupt, SystemExit):
        raise #re-raise for higher instance to catch
  

  # Receives a file from a client, copies it to a tmp folder and returns the respective fullpath
  #   Input is a string message starting with "file:"
  def receive_file(self, sMessage):
    aMessageParts = sMessage.split(":")
    iBytesToReceive = int(aMessageParts[2])
    Path, Filename = os.path.split(aMessageParts[1])
    ProjectName = aMessageParts[3]
    Fullpath = os.path.join(self.sCurrentScriptsDirectory, "..", "tmp", ProjectName, Path, "")
    print("Should receive file: "+Filename+" in path "+Fullpath+ " with "+str(iBytesToReceive)+" bytes")

    #check whether the file with this exact size already exists
    if os.path.isfile(Fullpath+Filename) and os.stat(Fullpath+Filename).st_size==iBytesToReceive:
      self.oLauncherConnection.send( b'exists' )
      print("File already exists with this size, so no need for resending")

    else: #file need to be received
      #create dir if it does not exist
      if not os.path.exists(Fullpath):
        os.makedirs(Fullpath)

      #send acceptance
      self.oLauncherConnection.send( b'ack' )

      #receive file
      iBytesReceived = 0
      with open(Fullpath+Filename, "wb") as f:
        bReceivingFile = True
        while bReceivingFile:
          # read 1024 bytes from the socket (receive)
          bytes_read = self.oLauncherConnection.recv(1024)
          if not bytes_read:    
            # nothing is received
            # file transmitting is done
            bReceivingFile = False
          else:
            # write to the file the bytes we just received
            f.write(bytes_read)
            iBytesReceived += len(bytes_read)
          if iBytesReceived == iBytesToReceive:
            bReceivingFile = False
        f.close()

      #check whether received file seems ok  
      if iBytesReceived == iBytesToReceive:
        self.oLauncherConnection.send( b'ack' ) #send acceptance
        print("File received successfully")
      else:
        self.oLauncherConnection.send( b'fail' ) #send failure
        print("File receive failed")

    return Fullpath+Filename



#create an instance of the class
oLauncher = VirtualAcousticsLauncher()