#!/usr/bin/env python

# Top level script for invoking Wing IDE.  If --use-src is specified
#  as an arg, then the files in WINGHOME/src, WINGHOME/external,
#  WINGHOME/opensource will be used; otherwise, the files in the version
#  specific bin directory will be used if it exists.
#
# Copyright (c) 2000-2012, Archaeopteryx Software, Inc.  All rights reserved.
#
# Written by Stephan R.A. Deibel and John P. Ehresman

import sys
import os

# Hack to set default encoding to utf-8 because most Wing code was
# developed to run with pygtk, which also changed the default encoding
# Since Wing changes the default encoding, there are potential bugs
# with any externally developed module that assumes a different default
# encoding.  Remember that dealing with encodings is _hard_ even though
# the most developers assume is that it should be easy (rather like
# threads or floating point).  The encoding story changes with Python 3.
# XXXQT May want to change this to calling PyUnicode_SetDefaultEncoding
# XXXQT directly and avoid reloading sys, but this works for now.
if 'WINGIDE_USE_QT4' in os.environ:
  reload(sys)
  import sys
  sys.setdefaultencoding('utf-8')
  import site
  site.main()

PATCHES_INSTALLED = False
kWingHome = None

def RelativePathJoin(*parts):
  """ Join where 2nd+ parts are relative. """
  
  if __debug__:
    for p in parts[1:]:
      assert not os.path.isabs(p)

  return os.path.join(*parts)


def FindWingHome():
  """ Returns abspath of winghome as an unicode object. """
  
  fs_encoding = sys.getfilesystemencoding()
  if fs_encoding is None:
    fs_encoding = sys.getdefaultencoding()
  if '--utf8-args' in sys.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 sys.argv:
    i = sys.argv.index('--use-devel-winghome')
    winghome = sys.argv[i + 1]
    del sys.argv[i:i + 2]
    
  # This is used when running the binary installation on win32
  elif '--use-winghome' in sys.argv:
    i = sys.argv.index('--use-winghome')
    winghome=sys.argv[i + 1]
    del sys.argv[i:i + 2]
    
  # Look in environment for a given value (used on Posix and on win32
  # when running Wing IDE from sources)
  elif os.environ.has_key('WINGHOME'):
    winghome=os.environ['WINGHOME']
    encoding = fs_encoding
  
  # If none given, guess WINGHOME based on location of this script
  else:
    winghome = os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0])))
  
  # Hack so I can debug this with Wing IDE 1.1.6 [SRAD]
  if os.environ.has_key('FORCE_WINGHOME'):
    winghome = os.environ['FORCE_WINGHOME']
    encoding = fs_encoding

  if winghome is None:
    return None
  
  if encoding is None:
    encoding = sys.getdefaultencoding()
  winghome = unicode(winghome, encoding, 'strict')

  if not os.path.isabs(winghome):
    try:
      cwd = os.getcwdu()
    except Exception:
      pass
    else:
      winghome = os.path.join(cwd, winghome)
      
  return os.path.normpath(winghome)


def FindBasePath(log_func):
  """ Find the base directory to load .py / .pyc files from.  Throws System exception
  if no base path can be found """

  # Caller requested forcing execution from source code even if binaries are available
  if '--use-src' in sys.argv:
    base_path = kWingHome
    log_func("Running Wing from source code in %s\n" % base_path)
    sys.argv.remove('--use-src')
    return base_path

  # Otherwise locate the wing executable base path:  We look through bin dir
  # in sub-directory according to the Python major.minor version that's running, and
  # try all lesser releases with same major.minor number if we don't
  # have an exact match
  major, minor, release = sys.version_info[:3]
  log_func("Running Wing IDE with Python version %d.%d.%d\n" % (major, minor, release))
  success = 0
  version_id = '%d.%d' % (major, minor)
  base_path = RelativePathJoin(kWingHome, 'bin',  version_id)
  if os.path.isdir(base_path):
    log_func("Using precompiled binaries for Python version " + version_id + "\n")
    return base_path

  # If not found, then try to run from source code
  base_path = kWingHome
  # Check whether we managed to find a valid base path
  if not os.path.isdir(base_path):
    log_func("Running with invalid WINGHOME: %s\n" % base_path)
    log_func("Please check your installation\n")
    sys.exit(-1)

  log_func("Running from source code files.\n")

  # Run with pychecker if it's present
  pychecker_dir = os.path.normpath(RelativePathJoin(kWingHome, '..', 'pychecker'))
  if os.path.isdir(pychecker_dir):
    os.environ['PYCHECKER'] = 'errors no-imports no-pkgimport no-local no-argsused no-varargsused no-unusednames'
    sys.path.append(pychecker_dir)
    import pychecker.checker
      
  return base_path

kInternalPaths = [
  'src',
  'opensource',
  'external/docutils/extras',
  'external/docutils',
  'external/pysqlite',
]
if 'WINGIDE_USE_QT4' in os.environ:
  if 'WING_USE_BINARY_PYSIDE' in os.environ:
    kInternalPaths.extend([
      'external/wing-scintilla/bin',
    ])
  else:
    kInternalPaths.extend([
      # XXXQT: Needed at least on OS X b/c of link issues when PySide is found
      # XXXQT: via /Users/Shared/src/python instead of ../pyside.  Fix later.
      '../pyside/sandbox-debug/lib/python2.7/site-packages', 
      'external/wing-scintilla/bin',
    ])
else:
  kInternalPaths.extend([
    'external/pyscintilla2',
    'external/pygtk',
  ])

if __debug__:
  kInternalPaths.append('external/cython')
def SetupPythonPath(log_func):
  """ Adds directories to python path. """

  base_path = FindBasePath(log_func)

  for path in reversed(kInternalPaths):
    parts = path.split('/')
    zip_name = RelativePathJoin(base_path, parts[0] + '.zip')
    if os.path.exists(zip_name):
      full_name = RelativePathJoin(zip_name, *parts[1:])
    else:
      full_name = RelativePathJoin(base_path, *parts)
    if sys.path[0] != full_name:
      if full_name in sys.path:
        sys.path.remove(full_name)
      sys.path.insert(0, full_name)

  # Note sure this is needed -- may add some safety against using system-wide
  # Python libs when present
  pylib = RelativePathJoin(kWingHome, 'bin', 'PyCore', 'lib', 'python%d.%d' % sys.version_info[:2])
  if os.path.exists(pylib + '.zip'):
    sys.path.insert(0, pylib + '.zip')
  else:
    sys.path.insert(0, pylib)

def GetSquelchOutputFlag():
  if '--squelch-output' in sys.argv:
    sys.argv.remove('--squelch-output')
    return True
  else:
    return False
  

def GetProfileFlag():
  # Override profile setting is --profile is in argv
  if '--profile' in sys.argv:
    sys.argv.remove('--profile')
    return True
  else:
    return False
  
def GetDebugFlag():
  """ Check whether issued debug option which turns off blanket 
  exception handlers so that the Wing debugger can report 
  exceptions right away. """
  if '--debug' in sys.argv:
    sys.argv.remove('--debug')
    return True
  else:
    return __debug__

#########################################################################
# Add dynamic library and patch directories
#########################################################################

def InitializeDynamicPaths(log_func):
  """ Init dynamic (non-standard paths), including patch directories.  Note
  the following are imported before patch dirs are added: config (w/ win32misc on
  win32), vinfo, capabilities, encoding_utils, & patchimporter. """
  
  from wingutils import patchimporter
  meta_path = patchimporter.PatchImporter()
  sys.meta_path.insert(0, meta_path)

  _AddDynamicLibDirs(meta_path, log_func)
  _AddPatchDirs(meta_path, log_func)

def _AddDynamicLibDirs(importer, log_func):
  """Add dynamic library directories -- needed when the pyo/pyc files are stored
  in a zip archive, since dynamic libs cannot be loaded from zips due to OS
  limitations"""
  
  interp_version = '%d.%d' % sys.version_info[:2]
  full_paths = [RelativePathJoin(kWingHome, 'bin', interp_version, *dir.split('/'))
                for dir in kInternalPaths]
  full_paths.append(RelativePathJoin(kWingHome, 'bin', 'PyCore', 'lib',
                                     'python%s' % interp_version))
  for full in full_paths:
    if os.path.isdir(full):
      importer.add_patch_dir(full)

  log_func("DYNLIB_DIRS=%s\n" % ', '.join(full_paths))

def _GetPatchDirs(wing_version, winghome, userwingdir):
  """ Get (name, dirname) pairs for all patches. Use all dirs from both wing
  home & user settings dir but reverse sort by local name so names with >
  alphabetical order take precedence. Patches are preceded by the patch number
  in ### format, so this lets a newer patch override files in an older one.
  Returned list is a new list instance and is sorted in alpha order. """
  
  dirs = [
    RelativePathJoin(winghome, 'patches', wing_version),
    RelativePathJoin(userwingdir, 'patches', wing_version)
  ]

  pair_list = []
  for patch_dir in dirs:
    try:
      name_list = os.listdir(patch_dir)
    except (IOError, OSError):
      name_list = []
    else:
      pair_list.extend([(name, RelativePathJoin(patch_dir, name))
                        for name in name_list])
  pair_list.sort()
  return pair_list  

def _AddPatchDirs(importer, log_func):
  """Add patch directories. """

  global PATCHES_INSTALLED
  assert not PATCHES_INSTALLED

  import config
  
  # Don't load patches when working from sources
  if __debug__:
    config.gPatchPairList = []
    PATCHES_INSTALLED = True
    return
  
  wing_version = config.kVersion
  if config.kBuild.startswith('b') or config.kBuild.startswith('rc'):
    wing_version += config.kBuild

  interp_version = '%d.%d' % sys.version_info[:2]

  pair_list = _GetPatchDirs(wing_version, kWingHome, config.kUserWingDir)
  for name, dirname in pair_list:
    interp_dir = RelativePathJoin(dirname, interp_version)
    if os.path.isdir(interp_dir):
      importer.add_patch_dir(interp_dir)
      
  config.gPatchPairList = pair_list

  PATCHES_INSTALLED = True

  log_func("PATCHES=%s\n" % ', '.join([p[0] for p in pair_list]))

def InitializeDebugServer():

  assert PATCHES_INSTALLED

  from debug.tserver import abstract
  abstract._SetWingHome(kWingHome)

###########################################################################
# Save startup environment variables in config.gStartupEnv. This is used
# later when spawning sub-processes to avoid polluting them with environment
# values we set inside Wing's startup scripts (see runwing.sh). The original
# values are obtained from the ORIG_ settings, if the ORIG_ variables are
# present. If the ORIG_ variables are set to __undefined__, then we del the
# associated env variable from the env.
###########################################################################
  
def _GetStartupEnv():
  from wingutils import location
  env = dict([(key, location.GetEnvValue(key)) for key in location.GetAllEnvNames()])
  for key, orig_val in env.items():
    
    # Found an overridden key
    if key.startswith('WING_ORIG_'):
      
      # Get the original value and environment variable name
      orig_key = key[len('WING_ORIG_'):]
  
      # If the environment had a valid original value, then restore that
      if orig_val != '__undefined__':
        env[orig_key] = orig_val
      
      # If we set the environment variable but it wasn't originally set, then
      # just remove our temporary value
      else:
        try:
          del env[orig_key]
        except KeyError:
          pass
        
      # Also delete our override key, since it's no longer needed
      del env[key]
 
  # Strip out any non-string keys or values
  for key, val in list(env.iteritems()):
    if not isinstance(key, basestring) or not isinstance(val, basestring):
      try:
        del env[key]
      except KeyError:
        pass

  return env      
      
def InitializeConfig():

  assert PATCHES_INSTALLED
  
  import config
  config.gStartupEnv = _GetStartupEnv()


def InitializeStdoutLogging(squelch_output):
  # Redirect stdout and stderr to our logging implementation
  # This import will fail if something is wrong with the
  # installation so we make sure we output errors when it does
  
  assert PATCHES_INSTALLED

  import config

  try:
    import sys
    import wingutils.reflect
  except ImportError:
    sys.exit(1)
    
  sys.stderr = wingutils.reflect.CLoggedOutput()
  sys.stdout = wingutils.reflect.CLoggedOutput()
  
  # Set up exception log
  if squelch_output:
    logs = [ config.kErrorLogFile ]
  else:  
    logs = [ sys.__stderr__, config.kErrorLogFile ]
  threshold = None  # Don't truncate during startup
  wingutils.reflect.ConfigureExceptionReporting(logs, threshold)
  

def _LoadGtk():
  """ Load gtk dlls; currently only implemented on win32.  Looks in all patch
  dirs for a bin/gtk-bin dir """

  assert PATCHES_INSTALLED

 
  import config
  from guiutils import gtkloader

  if sys.platform != 'win32':
    return

  kWin32SystemGtkDir = None
  # win32 "system" gtk support, uncomment & modify next line to activate
  #kWin32SystemGtkDir = 'C:/src/gnome-cvs/head/install-tree'
  if kWin32SystemGtkDir:
    try:
      gtkloader.LoadGtkDlls([kWin32SystemGtkDir])
    except Exception:
      if __debug__:
        raise
      use = False
    else:
      use = True
      
    if use:
      pygtk_path = RelativePathJoin(kWin32SystemGtkDir, 'lib/site-packages')
      sys.path.insert(1, pygtk_path)
      import pygtk
      pygtk.require('2.0')
      return
 
  wing_version = config.kVersion
  if config.kBuild.startswith('b') or config.kBuild.startswith('rc'):
    wing_version += config.kBuild

  pair_list = _GetPatchDirs(wing_version, kWingHome, config.kUserWingDir)
  pair_list.reverse()
  candidates = []
  for name, dirname in pair_list:
    candidates.append(RelativePathJoin(dirname, 'bin', 'gtk-bin'))
  candidates.append(RelativePathJoin(config.kWingHome, 'bin', 'gtk-bin'))
  gtkloader.LoadGtkDlls(candidates)
  
  # This one is for scintilla.  Note that non-debug lib can be loaded when
  # using python_d, but this is harmless and msvcp71d.dll will be loaded later
  if sys.platform == 'win32':
    msvcp_dll = config.GetWingFilename('bin/msvcp71.dll')
    if os.path.exists(msvcp_dll):
      gtkloader.win32process.LoadLibrary(msvcp_dll)
      
def _SetupEnvForGtk():
  """ Sets the environment up for gtk.  Must be called after startup environment is saved.
  Only does something useful on non win32 systems right now. """
  
  import config
  
  assert PATCHES_INSTALLED
  assert len(config.gStartupEnv) != 0
    
  if sys.platform == 'win32':
    return

  use_system_gtk = os.environ.get('WING_USE_SYSTEM_GTK', '0')
  if use_system_gtk == '0':
    priv_gtk = config.GetWingFilename('bin', 'gtk-bin')
    if isinstance(priv_gtk, unicode):
      priv_gtk = priv_gtk.encode(config.kFileSystemEncoding)
    try:
      libs = os.listdir(RelativePathJoin(priv_gtk, 'lib'))
    except OSError:
      libs = []
    pango_found, gtk_found = False, False
    for name in libs:
      if 'pango' in name:
        pango_found = True
      if 'gtk' in name:
        gtk_found = True
                       
    if pango_found:
      os.environ['FONTCONFIG_FILE'] = priv_gtk + '/etc/fonts/fonts.conf'
      for env_name in ['PANGO_EXE_PREFIX', 'PANGO_DATA_PREFIX']:
        os.environ[env_name] = priv_gtk
    if gtk_found:
      for env_name in ['GTK_EXE_PREFIX', 'GTK_DATA_PREFIX']:
        os.environ[env_name] = priv_gtk
      for env_name in ['GDK_PIXBUF_MODULE_FILE']:
        os.environ.pop(env_name, None)
      
def InitializeQt4(winghome):
  """ Initializes environment for QT4 """
  
  # XXXQT Needed for building docs and running from wing; absolute
  # development paths need to be replaced by whatever we come up with
  # for the build environment going foward
  
  if sys.platform == 'win32':
    pyside_sandbox = os.path.normpath(winghome + '/../pyside/sandbox-debug')
    site_packages_dir = os.path.join(pyside_sandbox, 'lib', 'site-packages')
    sys.path.insert(0, site_packages_dir)
    dlls_needed = ['QtCored4', 'QtGuid4', 'phonond4', 'QtNetworkd4', 'QtWebKitd4',
                   'shiboken-python2.7_d', 'pyside-python2.7_d']
    
    from wingutils import win32
    for name in dlls_needed:
      fullname = os.path.normpath(pyside_sandbox + '/bin/%s.dll' % name)
      win32.LoadLibrary(fullname)

def InitializeGtk():
  """ Initializes gtk. """

  assert PATCHES_INSTALLED
  
  # Check if a display has been specified
  if sys.platform != 'win32' and not os.environ.has_key("DISPLAY") \
     and '--display' not in sys.argv:
    print _("No X Windows DISPLAY has been specified")
    print _("Exiting Wing IDE")
    sys.exit(-1)
    
  _LoadGtk()
  _SetupEnvForGtk()  
        
def Run(argv, squelch_output):
  """ Runs wing.  Note that some modules are intentionally not imported
  until they're needed and / or the environment has been set up correctly. """
  
  print sys.path
  # premain imports gobject and gtk so environment needs to be set up first
  import premain
  
  for arg in sys.argv:
    if arg.startswith('--command-reference='):
      return premain._GenerateCommandReference()
  if '--preferences-reference' in sys.argv:
    return premain._GeneratePreferenceReference()
  for arg in sys.argv:
    if arg.startswith('--keymap-reference='):
      return premain._GenerateKeyMapReference()
  
  args = premain.ParseArgv(argv)
  pref_files = premain.FindCmdLinePrefFiles(argv)
  cmdlinefiles = premain.FindCmdLineFiles(argv)
  remote_open = premain.RemoteOpenFiles(args, cmdlinefiles)
  
  # Stop now if existing instance should be used
  if remote_open == 1:
    pass

  # Otherwise, continue with startup
  else:
    import main
    
    # Start execution
    main.main(squelch_output, remote_open == 0, pref_files, args, cmdlinefiles)

def Profile(args, squelch_output):
  """ Run w/ profiler. """

  import config

  print 'Starting profile'
  
  import cProfile, pstats
  import time

  wall_start = time.time()
  
  prof = cProfile.Profile()
  prof.runcall(Run, args, squelch_output)
  
  wall_end = time.time()
  
  print 'Profile completed; wall time in seconds = %.2f' % (wall_end - wall_start)
  
  stats = pstats.Stats(prof)
  stats.strip_dirs()
  stats.sort_stats('cumulative', 'calls')
  stats.print_stats(40)
  stats.sort_stats('time', 'calls')
  stats.print_stats(40)

def Initialize():
  """ Set up to run.  Returns whether output should be squelched. """

  global kWingHome

  tmp_errs = []
  def log(s):
    assert tmp_errs != None
    tmp_errs.append(s)

  try:
    # Save original language setting before we muck with it internally
    os.environ['WING_ORIG_LANGUAGE'] = os.environ.get('LANGUAGE', '__undefined__')
  
    # Some initial debug info
    log("=" * 80 + '\n')
    log("STARTING WING with args:\n")
    log(str(sys.argv)+'\n')
    log("sys.executable=%s\n" % sys.executable)
    log("sys.maxint=%s\n" % sys.maxint)
    log("LD_LIBRARY_PATH=%s\n" % os.environ.get('LD_LIBRARY_PATH', "''"))
    log("DYLD_LIBRARY_PATH=%s\n" % os.environ.get('DYLD_LIBRARY_PATH', "''"))
    log("DYLD_FALLBACK_LIBRARY_PATH=%s\n" % os.environ.get('DYLD_FALLBACK_LIBRARY_PATH', "''"))
    log("PATH=%s\n" % os.environ.get('PATH', "''"))
    
    # Find WingHome, note that config.py depends on the kWingHome name    
    kWingHome = FindWingHome()
    SetupPythonPath(log)
    
    log("PYTHONPATH=%s\n" % str(sys.path))
    
    InitializeDynamicPaths(log)
    assert PATCHES_INSTALLED

    if 'WINGIDE_USE_QT4' in os.environ:
      InitializeQt4(kWingHome)
  
    InitializeDebugServer()
    InitializeConfig()
  
    squelch_output = GetSquelchOutputFlag()
    InitializeStdoutLogging(squelch_output)
  
    # Print any accumulated errors to the log
    for err in tmp_errs:
      sys.stderr.write(err)
    tmp_errs = None
  
    if 'WINGIDE_USE_QT4' not in os.environ:
      InitializeGtk()

    return squelch_output

  except:
    # If strings in tmp_errs, send them to stderr
    if tmp_errs:
      for err in tmp_errs:
        sys.stderr.write(err)
    raise
  
def Main():
  """ Setup & run wing. """

  squelch_output = Initialize()

  # Set up I18N 
  import gettext
  _ = gettext.translation('src', fallback = 1).ugettext

  profile = GetProfileFlag()
  debug = GetDebugFlag()
  if profile:
    Profile(sys.argv[:], squelch_output)
    return
  
  if debug:
    Run(sys.argv[:], squelch_output)
  
  else:
  
    try:
      Run(sys.argv[:], squelch_output)
    except:
      print _("Fatal startup error:  Exiting Wing IDE")
      raise

  # XXXQT Work around shutdown bug:
  # https://bugreports.qt-project.org/browse/PYSIDE-53
  # Disabled for now to find bugs, but may want to re-enable it later
  #if 'WINGIDE_USE_QT4' in os.environ:
  #  os._exit(0)
    
if __name__ == '__main__':
  Main()
