#!/usr/bin/env python

# Copyright 2013 Ben Vanik. All Rights Reserved.

"""
"""

__author__ = 'ben.vanik@gmail.com (Ben Vanik)'


import os
import re
import shutil
import subprocess
import sys


def main():
  # Add self to the root search path.
  sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))

  # Augment path to include our fancy things.
  os.environ['PATH'] += os.pathsep + os.pathsep.join([
      os.path.abspath('third_party/ninja/'),
      os.path.abspath('third_party/gyp/')
      ])

  # Check python version.
  if not sys.version_info[:2] == (2, 7):
    print('ERROR: python 2.7 required')
    print('(unfortunately gyp doesn\'t work with 3!)')
    sys.exit(1)
    return

  # Grab Visual Studio version and execute shell to set up environment.
  if sys.platform == 'win32':
    vs_version = import_vs_environment()
    if not vs_version == 2013:
      print('ERROR: Visual Studio 2013 not found!')
      print('Ensure you have the VS120COMNTOOLS environment variable!')
      sys.exit(1)
      return

  # Grab all commands.
  commands = discover_commands()

  # Parse command name and dispatch.
  try:
    if len(sys.argv) < 2:
      raise ValueError('No command given')
    command_name = sys.argv[1]
    if not command_name in commands:
      raise ValueError('Command "%s" not found' % (command_name))

    command = commands[command_name]
    return_code = run_command(command=command,
                              args=sys.argv[2:],
                              cwd=os.getcwd())
  except ValueError:
    print(usage(commands))
    return_code = 1
  except Exception as e:
    #print e
    raise
    return_code = 1
  sys.exit(return_code)


def import_vs_environment():
  """Finds the installed Visual Studio version and imports
  interesting environment variables into os.environ.

  Returns:
    A version such as 2010, 2012, or 2013 or None if no VS is found.
  """
  version = 0
  tools_path = ''
  if 'VS120COMNTOOLS' in os.environ:
    version = 2013
    tools_path = os.environ['VS120COMNTOOLS']
  elif 'VS110COMNTOOLS' in os.environ:
    version = 2012
    tools_path = os.environ['VS110COMNTOOLS']
  elif 'VS100COMNTOOLS' in os.environ:
    version = 2010
    tools_path = os.environ['VS100COMNTOOLS']
  if version == 0:
    return None
  tools_path = os.path.join(tools_path, '..\\..\\vc\\vcvarsall.bat')

  args = [tools_path, '&&', 'set']
  popen = subprocess.Popen(
      args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  variables, _ = popen.communicate()
  envvars_to_save = (
      'devenvdir',
      'include',
      'lib',
      'libpath',
      'path',
      'pathext',
      'systemroot',
      'temp',
      'tmp',
      'windowssdkdir',
      )
  for line in variables.splitlines():
    for envvar in envvars_to_save:
      if re.match(envvar + '=', line.lower()):
        var, setting = line.split('=', 1)
        if envvar == 'path':
          setting = os.path.dirname(sys.executable) + os.pathsep + setting
        os.environ[var.upper()] = setting
        break

  os.environ['VSVERSION'] = str(version)
  return version


def discover_commands():
  """Looks for all commands and returns a dictionary of them.
  In the future commands could be discovered on disk.

  Returns:
    A dictionary containing name-to-Command mappings.
  """
  commands = {
      'setup': SetupCommand(),
      'pull': PullCommand(),
      'gyp': GypCommand(),
      'build': BuildCommand(),
      'test': TestCommand(),
      'clean': CleanCommand(),
      'nuke': NukeCommand(),
      }
  return commands


def usage(commands):
  """Gets usage info that can be displayed to the user.

  Args:
    commands: A command dictionary from discover_commands.

  Returns:
    A string containing usage info and a command listing.
  """
  s = 'xenia-build.py command [--help]\n'
  s += '\n'
  s += 'Commands:\n'
  command_names = sorted(commands.keys())
  for command_name in command_names:
    s += '  %s\n' % (command_name)
    command_help = commands[command_name].help_short
    if command_help:
      s += '    %s\n' % (command_help)
  return s


def run_command(command, args, cwd):
  """Runs a command with the given context.

  Args:
    command: Command to run.
    args: Arguments, with the app and command name stripped.
    cwd: Current working directory.

  Returns:
    0 if the command succeeded and non-zero otherwise.

  Raises:
    ValueError: The command could not be found or was not specified.
  """
  # TODO(benvanik): parse arguments/etc.
  return command.execute(args, cwd)


def has_bin(bin):
  """Checks whether the given binary is present.
  """
  for path in os.environ["PATH"].split(os.pathsep):
    path = path.strip('"')
    exe_file = os.path.join(path, bin)
    if os.path.isfile(exe_file) and os.access(exe_file, os.X_OK):
      return True
    exe_file = exe_file + '.exe'
    if os.path.isfile(exe_file) and os.access(exe_file, os.X_OK):
      return True
  return None


def shell_call(command, throw_on_error=True):
  """Executes a shell command.

  Args:
    command: Command to execute.
    throw_on_error: Whether to throw an error or return the status code.

  Returns:
    If throw_on_error is False the status code of the call will be returned.
  """
  if throw_on_error:
    subprocess.check_call(command, shell=True)
    return 0
  else:
    return subprocess.call(command, shell=True)


class Command(object):
  """Base type for commands.
  """

  def __init__(self, name, help_short=None, help_long=None, *args, **kwargs):
    """Initializes a command.

    Args:
      name: The name of the command exposed to the management script.
      help_short: Help text printed alongside the command when queried.
      help_long: Extended help text when viewing command help.
    """
    self.name = name
    self.help_short = help_short
    self.help_long = help_long

  def execute(self, args, cwd):
    """Executes the command.

    Args:
      args: Arguments list.
      cwd: Current working directory.

    Returns:
      Return code of the command.
    """
    return 1


def post_update_deps(config):
  """Runs common tasks that should be executed after any deps are changed.

  Args:
    config: 'debug' or 'release'.
  """
  pass


class SetupCommand(Command):
  """'setup' command."""

  def __init__(self, *args, **kwargs):
    super(SetupCommand, self).__init__(
        name='setup',
        help_short='Setup the build environment.',
        *args, **kwargs)

  def execute(self, args, cwd):
    print('Setting up the build environment...')
    print('')

    # Setup submodules.
    print('- git submodule init / update...')
    shell_call('git submodule init')
    shell_call('git submodule update')
    print('')

    # Disable core.filemode on Windows to prevent weird file mode diffs in git.
    # TODO(benvanik): check cygwin test - may be wrong when using Windows python
    if os.path.exists('/Cygwin.bat'):
      print('- setting filemode off on cygwin...')
      shell_call('git config core.filemode false')
      shell_call('git submodule foreach git config core.filemode false')
      print('')

    # Run the ninja bootstrap to build it, if it's missing.
    if (not os.path.exists('third_party/ninja/ninja') and
       not os.path.exists('third_party/ninja/ninja.exe')):
      print('- preparing ninja...')
      # Windows needs --x64 to force building the 64-bit ninja.
      extra_args = ''
      #if sys.platform == 'win32':
      #  extra_args = '--x64'
      shell_call('python third_party/ninja/bootstrap.py ' + extra_args)
      print('')

    # binutils (with vmx128).
    print('- Building binutils...')
    if sys.platform == 'win32':
      # TODO(benvanik): cygwin or vagrant
      print('WARNING: binutils build not supported yet')
    else:
      shell_call('third_party/binutils/build.sh')
    print('')

    # wxWidgets.
    print('- Building wxWidgets (will take awhile)...')
    os.chdir('third_party/wxWidgets')
    if sys.platform == 'win32':
      shutil.copyfile('include/wx/msw/setup0.h', 'include/wx/msw/setup.h')
      shell_call(' '.join([
          'msbuild',
          'build\msw\wx_vc10.sln',
          '/nologo',
          '/verbosity:quiet',
          '/p:Configuration=Release',
          '/p:Platform=x64',
          ]))
    else:
      print('WARNING: wxWidgets build not supported yet')
    os.chdir(cwd)
    print('')

    post_update_deps('debug')
    post_update_deps('release')

    print('- running gyp...')
    run_all_gyps()
    print('')

    print('Success!')
    return 0


class PullCommand(Command):
  """'pull' command."""

  def __init__(self, *args, **kwargs):
    super(PullCommand, self).__init__(
        name='pull',
        help_short='Pulls the repo and all dependencies.',
        *args, **kwargs)

  def execute(self, args, cwd):
    print('Pulling...')
    print('')

    print('- pulling self...')
    shell_call('git pull')
    print('')

    print('- pulling dependencies...')
    shell_call('git submodule update')
    print('')

    post_update_deps('debug')
    post_update_deps('release')

    print('- running gyp...')
    run_all_gyps()
    print('')

    print('Success!')
    return 0


def run_gyp(format):
  """Runs gyp on the main project with the given format.

  Args:
    format: gyp -f value.
  """
  shell_call(' '.join([
      'gyp',
      '-f %s' % (format),
      # Removes the out/ from ninja builds.
      '-G output_dir=.',
      '--depth=.',
      '--toplevel-dir=.',
      '--generator-output=build/xenia/',
      # Set the VS version.
      '-G msvs_version=%s' % (os.environ.get('VSVERSION', 2013)),
      #'-D windows_sdk_dir=%s' % (os.environ['WINDOWSSDKDIR']),
      '-D windows_sdk_dir="C:\\Program Files (x86)\\Windows Kits\\8.1"',
      'xenia.gyp',
      ]))


def run_all_gyps():
  """Runs all gyp configurations.
  """
  run_gyp('ninja')
  if sys.platform == 'darwin':
    run_gyp('xcode')
  elif sys.platform == 'win32':
    run_gyp('msvs')


class GypCommand(Command):
  """'gyp' command."""

  def __init__(self, *args, **kwargs):
    super(GypCommand, self).__init__(
        name='gyp',
        help_short='Runs gyp to update all projects.',
        *args, **kwargs)

  def execute(self, args, cwd):
    print('Running gyp...')
    print('')

    # Update GYP.
    run_all_gyps()

    print('Success!')
    return 0


class BuildCommand(Command):
  """'build' command."""

  def __init__(self, *args, **kwargs):
    super(BuildCommand, self).__init__(
        name='build',
        help_short='Builds the project.',
        *args, **kwargs)

  def execute(self, args, cwd):
    # TODO(benvanik): add arguments:
    # --force
    debug = '--debug' in args
    config = 'Debug' if debug else 'Release'
    target = ''
    for arg in args:
      if not arg.startswith('--'):
        target = arg

    print('Building %s...' % (config))
    print('')

    print('- running gyp for ninja...')
    run_gyp('ninja')
    print('')

    if not target:
      print('- building all:%s...' % (config))
      result = shell_call('ninja -C build/xenia/%s' % (config),
                          throw_on_error=False)
    else:
      print('- building %s:%s...' % (target, config))
      result = shell_call('ninja -C build/xenia/%s %s' % (config, target),
                          throw_on_error=False)
    print('')
    if result != 0:
      return result

    print('Success!')
    return 0


class TestCommand(Command):
  """'test' command."""

  def __init__(self, *args, **kwargs):
    super(TestCommand, self).__init__(
        name='test',
        help_short='Runs all tests.',
        *args, **kwargs)

  def execute(self, args, cwd):
    print('Testing...')
    print('')

    # Run base alloy tests.
    print('Launching alloy-test runner...')
    result = shell_call('"build/xenia/Debug/alloy-test"')
    print('')
    if result != 0:
      return result

    # First run make and update all of the test files.
    if sys.platform == 'win32':
      # TODO(benvanik): use cygwin/vagrant/whatever
      print('WARNING: test files not updated!');
    else:
      print('Updating test files...')
      result = shell_call('./src/alloy/frontend/ppc/test/update.sh')
      print('')
      if result != 0:
        return result

    # Start the test runner.
    print('Launching alloy-ppc-test runner...')
    result = shell_call('"build/xenia/Debug/alloy-ppc-test"')
    print('')
    if result != 0:
      return result

    return result


class CleanCommand(Command):
  """'clean' command."""

  def __init__(self, *args, **kwargs):
    super(CleanCommand, self).__init__(
        name='clean',
        help_short='Removes intermediate files and build output.',
        *args, **kwargs)

  def execute(self, args, cwd):
    print('Cleaning build artifacts...')
    print('')

    print('- removing build/xenia/...')
    if os.path.isdir('build/xenia/'):
      shutil.rmtree('build/xenia/')
    print('')

    print('Success!')
    return 0


class NukeCommand(Command):
  """'nuke' command."""

  def __init__(self, *args, **kwargs):
    super(NukeCommand, self).__init__(
        name='nuke',
        help_short='Removes all build/ output.',
        *args, **kwargs)

  def execute(self, args, cwd):
    print('Cleaning build artifacts...')
    print('')

    print('- removing build/...')
    if os.path.isdir('build/'):
      shutil.rmtree('build/')
    print('')

    print('Success!')
    return 0


if __name__ == '__main__':
  main()