# Top level script for launching Wing -- users should instead invoke
# wing.exe or the wing bash script on Linux and OS X

#
# Copyright (c) 2000-2018, Archaeopteryx Software, Inc.  All rights reserved.
#
# Written by Stephan R.A. Deibel and John P. Ehresman

import sys
import os

# When Wing debugs itself, _winghome is already loaded but we don't want the
# debugger's version
if 'WINGDB_ACTIVE' in os.environ and '_winghome' in sys.modules:
  del sys.modules['_winghome']
  
import _winghome

# This is the non-patchable IDE startup module

# Set __file__ if not already set
try:
  __file__
except NameError:
  __file__ = sys.argv[0]

#----------------------------------------------------------------------
def NP_LoadModuleHere(modname):
  """Load the given module from same directory as this file
  without adding it to sys.modules"""

  sys.modules.pop(modname, None)

  sys.path.insert(0, os.path.dirname(__file__))
  mod = __import__(modname)
  sys.modules.pop(modname, None)

  sys.path = sys.path[1:]
  return mod
  
bootstrap_utils = NP_LoadModuleHere('bootstrap_utils')

# Flag for showing garbage collection debug output
if '--gc-debug-stats' in sys.argv:
  sys.argv.remove('--gc-debug-stats')
  import gc
  gc.set_debug(gc.DEBUG_STATS)

# Flag for verbose call tracing
if '--verbose-trace' in sys.argv:
  def trace(frame, event, arg):
    try:
      fname = repr(frame.f_code)
    except:
      fname = '<unknown>'
    sys.__stderr__.write(event + ' ' + fname) 
    sys.__stderr__.write('\n')
  sys.setprofile(trace)
  
# Flags to explicitly enable or disable loading updates
if '--enable-updates' in sys.argv:
  _winghome.kLoadUpdates = True
  sys.argv.remove('--enable-updates')
elif '--disable-updates' in sys.argv:
  _winghome.kLoadUpdates = False
  sys.argv.remove('--disable-updates')
  
# Load updates only if not running from sources
else:
  indicator_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
                                'src', 'config.py')
  if os.path.exists(indicator_file):
    _winghome.kLoadUpdates = False
  else:
    _winghome.kLoadUpdates = True
  
#-----------------------------------------------------------------------
def NP_FindWingHome(argv):
  """ Returns abspath of winghome as an unicode object. """
  
  # Note that encoding / fs_encoding are ignored w/ python 3.8
  fs_encoding = sys.getfilesystemencoding()
  if fs_encoding is None:
    fs_encoding = sys.getdefaultencoding()
  if '--utf8-args' in argv:
    encoding = 'utf_8'
  else:
    encoding = fs_encoding
  
  winghome = None
  
  # This lets us debug the IDE with an installed copy of itself on win32
  if '--use-devel-winghome' in argv:
    i = argv.index('--use-devel-winghome')
    winghome = argv[i + 1]
    del argv[i:i + 2]
    
  # This is used when running the binary installation on win32
  elif '--use-winghome' in argv:
    i = argv.index('--use-winghome')
    winghome=argv[i + 1]
    del argv[i:i + 2]
    
  # Look in environment for a given value (used on Posix and on win32
  # when running Wing from sources)
  elif 'WINGHOME' in os.environ:
    winghome=os.environ['WINGHOME']
    encoding = fs_encoding
  
  # If none given, compute WINGHOME based on location of this file
  else:
    winghome = os.path.dirname(os.path.dirname(os.path.abspath(argv[0])))
  
  if winghome is None:
    return None
  
  if sys.version_info < (3, 0):
    if encoding is None:
      encoding = sys.getdefaultencoding()
  
    winghome = winghome.decode(encoding, 'strict')

  if not os.path.isabs(winghome):
    try:
      if sys.version_info < (3, 0):
        cwd = os.getcwdu()
      else:
        cwd = os.getcwd()
    except Exception:
      pass
    else:
      winghome = os.path.join(cwd, winghome)
      
  return os.path.normpath(winghome)

#-----------------------------------------------------------------------
def NP_Delete(name, recursive=True, force=True):
  """ Delete files or directories (if recursive is true).  Returns whether it
  was able to delete everything; it tries to delete as much as it can even
  if it encounters errors.  This avoids bugs in shutil.rmtree that prevent
  removing read-only files."""
  
  deleted_all = True
  
  if os.path.isfile(name) or os.path.islink(name):
    if force and not os.path.islink(name):
      try:
        os.chmod(name, 438)  # octal 0666
      except OSError:
        pass
    try:  
      os.remove(name)
    except OSError:
      deleted_all = False
      
  elif os.path.isdir(name) and recursive:
    if force:
      try:
        os.chmod(name, 511)  # octal 0777
      except OSError:
        pass

    try:
      children = os.listdir(name)
    except OSError:
      children = ()
      deleted_all = False
    for child in children:
      sub_all = NP_Delete(os.path.join(name, child), recursive, force)
      if not sub_all:
        deleted_all = False
        
    try:
      os.rmdir(name)
    except OSError:
      deleted_all = False
      
  return deleted_all

#-----------------------------------------------------------------------
def NP_ApplyPendingRevert(winghome, user_settings):
  
  errors = []
  
  pending_fn = os.path.join(user_settings, 'revert.pending')
  if not os.path.exists(pending_fn):
    return errors
  
  try:
    with open(pending_fn, 'rb') as f:
      ver = f.read()
      ver = ver.decode('utf-8')
  except Exception:
    errors.append("Could not read pending revert file: " + pending_fn)
    NP_Delete(pending_fn)
    return errors
    
  def parse_version(ver):
    ver = ver.split('.')
    if len(ver) != 4:
      return None
    try:
      return tuple([int(v) for v in ver])
    except Exception:
      return None    
    
  ver_tuple = parse_version(ver)
  if ver_tuple is None:
    errors.append("Could not parse version number: " + repr(ver))
    NP_Delete(pending_fn)
    return errors
    
  for dirname in [user_settings, winghome]:
    dirname = os.path.join(dirname, 'updates', bootstrap_utils._NP_GetUpdateDirname(winghome))
    if not os.path.exists(dirname):
      continue
    
    try:
      fn_list = os.listdir(dirname)
    except Exception:
      errors.append('Could not read directory: ' + dirname)
      fn_list = []
    
    for fn in fn_list:
      fn_ver_tuple = parse_version(fn)
      if fn_ver_tuple is None:
        continue
      if fn_ver_tuple > ver_tuple:
        update_dir = os.path.join(dirname, fn)
        if not NP_Delete(update_dir):
          errors.append("Failed to remove update directory %s -- try removing it manually" % (update_dir,))
          
  NP_Delete(pending_fn)
  
  return errors
  
#-----------------------------------------------------------------------
def NP_Initialize(argv):
  """The non-patchable initialization phase"""

  err_list = []
  class CTempLog:
    def write(self, s):
      assert err_list != None
      err_list.append(s)

  try:
    # Save original language setting before we muck with it internally
    os.environ['WING_ORIG_LANGUAGE'] = os.environ.get('LANGUAGE', '__undefined__')

    # Work around for Python codec bug on Windows (only affects Wing running as console app)
    if sys.platform == 'win32':
      import codecs
      codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None)
      
    # Some initial debug info
    logger = CTempLog()
    logger.write("=" * 80 + '\n')
    logger.write("STARTING WING with args:\n")
    logger.write(str(argv)+'\n')
    logger.write(f'{sys.flags=}\n')
    logger.write(f'{sys.path=}\n')
    logger.write("sys.executable=%s\n" % sys.executable)
    logger.write("sys.maxsize=%s\n" % sys.maxsize)
    logger.write("LD_LIBRARY_PATH=%s\n" % os.environ.get('LD_LIBRARY_PATH', "''"))
    logger.write("DYLD_LIBRARY_PATH=%s\n" % os.environ.get('DYLD_LIBRARY_PATH', "''"))
    logger.write("DYLD_FALLBACK_LIBRARY_PATH=%s\n" % os.environ.get('DYLD_FALLBACK_LIBRARY_PATH', "''"))
    logger.write("PATH=%s\n" % os.environ.get('PATH', "''"))
    
    # Find winghome and user settings directory and put into the _winghome module
    winghome, user_settings = bootstrap_utils.NP_SetupWingHomeModule(NP_FindWingHome(argv))

    # Remove any updates if there is a pending revert
    for err in NP_ApplyPendingRevert(winghome, user_settings):
      logger.write(err)
      
    # Set up merging import manager, which supports update; everything after
    # this can be patched
    importer = bootstrap_utils.NP_CreateMetaImporter(winghome, user_settings,
                                                     'ide', logger)
    return importer, err_list

  except:
    # If strings in err_list, send them to stderr
    if err_list:
      for err in err_list:
        sys.stderr.write(err)
    raise

  
#-----------------------------------------------------------------------
INITIALIZED = False
if __name__ == '__main__':
  try:
    
    # First run the non-patchable initialization
    argv = list(sys.argv)
    importer, err_list = NP_Initialize(argv)
    INITIALIZED = True
    
    # Everything after this point can be updated with the update manager
    
    # Flag to run installation validation check
    if '--check-install' in argv:
      argv.remove('--check-install')
      import vinfo
      import config
      from wingbase import updateutils
      code_dirs = bootstrap_utils.NP_GetCodeDirs(config.kWingHome, config.kUserSettings,
                                                logger=sys.stdout)
      errors = updateutils.ValidateInstall(code_dirs, [])
      if errors:
        sys.__stderr__.write("Some validation errors were found:\n")
        sys.__stderr__.write('   '+ '\n   '.join(errors) + '\n')
        if config.kAvail101Support:
          prod = 'wing-101'
        elif config.kAvailPersonalSupport:
          prod = 'wing-personal'
        else:
          prod = 'wingpro'
        sys.__stderr__.write("You may want to reinstall from https://wingware.com/downloads/%s/%s\n" % (prod, vinfo.kVersion))
      else:
        sys.__stderr__.write("No validation errors found\n")

      # Start Wing
    else:
      import startwing
      startwing.Main(argv, importer, err_list)
    
  finally:
    # Make sure any pending log entries are written to log files
    try:
      from wingutils import reflect
    except:
      sys.stderr.write("Unable to import wingutils.reflect")
    else:
      reflect._DoWrite()

    
    
