#!/usr/bin/python
#----------------------------------------------------------------------------#
#                                                                            #
# ozz-animation is hosted at http://github.com/guillaumeblanc/ozz-animation  #
# and distributed under the MIT License (MIT).                               #
#                                                                            #
# Copyright (c) 2019 Guillaume Blanc                                         #
#                                                                            #
# Permission is hereby granted, free of charge, to any person obtaining a    #
# copy of this software and associated documentation files (the "Software"), #
# to deal in the Software without restriction, including without limitation  #
# the rights to use, copy, modify, merge, publish, distribute, sublicense,   #
# and/or sell copies of the Software, and to permit persons to whom the      #
# Software is furnished to do so, subject to the following conditions:       #
#                                                                            #
# The above copyright notice and this permission notice shall be included in #
# all copies or substantial portions of the Software.                        #
#                                                                            #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,   #
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL    #
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING    #
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER        #
# DEALINGS IN THE SOFTWARE.                                                  #
#                                                                            #
#----------------------------------------------------------------------------#

# CMake python helper script.

import subprocess
import shutil
import sys
import os
import os.path
import re
import platform
from functools import partial


# Build global path variables.
root = os.path.abspath(os.path.join(os.getcwd(), '.'))
build_dir = os.path.join(root, 'build')
build_dir_cc = os.path.join(root, 'build-cc')
cmake_cache_file = os.path.join(build_dir, 'CMakeCache.txt')
ctest_cache_file = os.path.join(build_dir, 'CTestTestfile.cmake')
config = 'Release'
generators = {0: 'default'}
generator = generators[0]
enable_testing = False
emscripten_path = os.environ.get('EMSCRIPTEN')

def ValidateCMake():
  try:
    # Test that cmake can be executed, silently...
    pipe = subprocess.Popen(['cmake'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    out, err = pipe.communicate()
  except OSError as e:
    print("CMake is not installed or properly setup. Please visit www.cmake.org.")
    return False

  print("CMake is installed and setup properly.")
  return True

def CheckEmscripten():
  if(emscripten_path == None):
    return False

  try:
    # Test that cmake can be executed, silently...
    pipe = subprocess.Popen(['emcc'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    out, err = pipe.communicate()
  except OSError as e:
    print("Emscripten is not installed or properly setup.")
    return False

  print("Emscripten is installed and setup properly.")
  return True

def MakeBuildDir(_build_dir = build_dir):
  print("Creating out-of-source build directory: \"" + _build_dir + "\".")
  if not os.path.exists(_build_dir):
    os.makedirs(_build_dir)
  return True

def CleanBuildDir():
  print("Deleting out-of-source build directory: \"" + build_dir + "\".")
  if os.path.exists(build_dir):
    shutil.rmtree(build_dir)
  print("Deleting out-of-source cross compilation build directory: \"" + build_dir_cc + "\".")
  if os.path.exists(build_dir_cc):
    shutil.rmtree(build_dir_cc)
  return True

def Configure():
  # Configure build process.
  print("Configuring build project.")
  options = ['cmake']
  options += ['-D', 'CMAKE_BUILD_TYPE=' + config]

  if (enable_testing) :
    options += ['-D', 'ozz_build_tests=1']
  else:
    options += ['-D', 'ozz_build_tests=0']

  global generator
  if(generator != 'default'):
    options += ['-G', generator]
  options += [root]
  config_process = subprocess.Popen(options, cwd=build_dir)
  config_process.wait()
  if(config_process.returncode != 0):
    print("Configuration failed.")
    return False
  print("Configuration succeeded.")

  # Updates generator once configuration is complete
  generator = DetectGenerator()

  return True

def ConfigureCC():
  # Configure build process.
  print("Configuring emscripten cross compilation build with path: " + emscripten_path)
  options = ['cmake']

  options += ['-D', 'CMAKE_BUILD_TYPE=' + config]
  options += ['-D', 'CMAKE_TOOLCHAIN_FILE=' + emscripten_path + '/cmake/Modules/Platform/Emscripten.cmake']

  if(platform.system() == 'Windows'):
    options += ['-G', 'MinGW Makefiles']
  else:
    options += ['-G', 'Unix Makefiles']

  options += [root]
  config_process = subprocess.Popen(options, cwd=build_dir_cc)
  config_process.wait()
  if(config_process.returncode != 0):
    print("Configuration failed.")
    return False
  print("Configuration succeeded.")

  # Updates generator once configuration is complete
  generator = DetectGenerator()

  return True

def Build(_build_dir = build_dir):
  # Configure build process.
  print("Building project.")
  options = ['cmake', '--build', _build_dir, '--config', config, '--use-stderr'];
  # Appends parallel build option if supported by the generator.
  if "Unix Makefiles" in generator:
    options += ['--', '-j4']
  config_process = subprocess.Popen(options, cwd=_build_dir)
  config_process.wait()
  if(config_process.returncode != 0):
    print("Build failed.")
    return False
  print("Build succeeded.")
  return True

def Test():
  # Configure Test process, parallelize a lot of tests in order to stress their dependencies
  print("Running unit tests.")
  options = ['ctest' ,'--output-on-failure', '-j8', '--build-config', config]
  config_process = subprocess.Popen(options, cwd=build_dir)
  config_process.wait()
  if(config_process.returncode != 0):
    print("Testing failed.")
    return False
  print("Testing succeeded.")
  return True

def PackSources(_type):
  print("Packing sources.")
  options = ['cpack', '-G', _type, '--config', 'CPackSourceConfig.cmake']
  config_process = subprocess.Popen(options, cwd=build_dir)
  config_process.wait()
  if(config_process.returncode != 0):
    print("Packing sources of type " + _type + " failed.")
    return False
  print("Packing sources of type " + _type + " succeeded.")
  return True

def PackBinaries(_type, _build_dir = build_dir):
  print("Packing binaries.")
  options = ['cpack', '-G', _type, '-C', config]
  config_process = subprocess.Popen(options, cwd=_build_dir)
  config_process.wait()
  if(config_process.returncode != 0):
    print("Packing binaries of type " + _type + " failed.")
    return False
  print("Packing binaries of type " + _type + " succeeded.")
  return True

def SelecConfig():
  configs = {
    1: 'Debug',
    2: 'Release',
    3: 'RelWithDebInfo',
    4: 'MinSizeRel'}

  while True:
    print("Select build configuration:")
    for num, message in sorted(configs.iteritems()):
      print("%d: %s") % (num, message)

    # Get input and check validity
    try:
      answer = int(raw_input("Enter a value: "))
    except:
      continue
    if not answer in configs:
      continue

    # Affect global configuration variable
    global config
    config = configs[answer]
    return True

def FindGenerators():
  # Finds all generators outputted from cmake usage 
  process = subprocess.Popen(['cmake', '--help'], stdout=subprocess.PIPE)
  stdout = process.communicate()[0]
  sub_stdout = stdout[stdout.rfind('Generators'):]
  matches = re.findall(r"\s*\**\s*(.+)\s*=.+", sub_stdout, re.MULTILINE)
  # Fills generators list
  global generators  
  for match in matches:
    generator_name = match.strip()
    # Appends also Win64/ARM option if generator is VS
    if " [arch]" in generator_name:
      gen_name = generator_name[0:len(generator_name) - 7]
      generators[len(generators)] = gen_name
      generators[len(generators)] = gen_name + " Win64"
      generators[len(generators)] = gen_name + " ARM"
    else:
      generators[len(generators)] = generator_name

def FindInCache(_regex):
  try:
    cache_file = open(cmake_cache_file)
  except:
    return None
  return re.search(_regex, cache_file.read())

def DetectGenerator():
  match = FindInCache(r"CMAKE_GENERATOR:INTERNAL=(.*)")
  if match:
    global generators
    global generator
    for num, message in sorted(generators.iteritems()):
      if match.group(1) == message:
        return message
  return 'default'

def SelecGenerator():
  global generators
  while True:
    print("Select generator:")
    for num, message in sorted(generators.iteritems()):
      print("%d: %s") % (num, message)

    # Get input and check validity
    try:
      answer = int(raw_input("Enter a value: "))
    except:
      continue
    if not answer in generators:
      continue
    
    # Check if this is the current generator
    current_generator = DetectGenerator()
    if current_generator == 'default':
      global generator
      generator = generators[answer]
      return True
    if current_generator != generators[answer]:
      print("Selected generator '%s' is different from the current one '%s'.")  % (generators[answer], current_generator)
      clean = raw_input("Do you want to clean build directory to apply the change? (y/n): ") == "y"
      if clean:
        generator = generators[answer]
        return CleanBuildDir()
    return True

def DetectTesting():
  global enable_testing
  enable_testing = os.path.isfile(ctest_cache_file)

def EnableTesting():
  global enable_testing
  while True:
    # Get input and check validity
    answer = raw_input("enable testing (y/n): ")
    if answer != 'y' and answer != 'n':
      continue
    wanted = (answer == 'y')
    
    # Get current state
    if (enable_testing != wanted):
      enable_testing = wanted
      print("Testing state has changed.")
      
    return True

def ClearScreen():
  os.system('cls' if os.name=='nt' else 'clear')

def Quit():
  sys.exit(0)
  return True

def main():

  # Checks CMake installation is correct.
  if not ValidateCMake():
    return

  # Emscripten is optional
  CheckEmscripten()

  # Detects available generators
  FindGenerators()

  # Detects testing state
  DetectTesting()

  # Update current generator
  print("DetectGenerator")
  global generator
  generator = DetectGenerator()

  options = {
    '1': ["Build", [MakeBuildDir, Configure, Build]],
    '2': ["Run unit tests", [MakeBuildDir, Configure, Build, Test]],
    '3': ["Execute CMake generation step (don't build)", [MakeBuildDir, Configure]],
    '4': ["Clean out-of-source build directory\n  ------------------", [CleanBuildDir]],
    '5': ["Pack binaries", [MakeBuildDir, Configure, Build, partial(PackBinaries, "ZIP"), partial(PackBinaries, "TBZ2")]],
    '6': ["Pack sources\n  ------------------", [MakeBuildDir, Configure, partial(PackSources, "ZIP"), partial(PackSources, "TBZ2")]],
    '7': ["Enable testing", [EnableTesting]],
    '8': ["Select build configuration", [SelecConfig]],
    '9': ["Select cmake generator\n  ------------------", [SelecGenerator]],
    'q': ["Quit\n------------------", [Quit]]}

  # Adds emscripten
  global emscripten_path
  if emscripten_path != None:
    options['1a'] = ["Build emscripten", [partial(MakeBuildDir, build_dir_cc), ConfigureCC, partial(Build, build_dir_cc)]]
    options['5a'] = ["Pack emscripten binaries", [MakeBuildDir, Configure, Build, partial(MakeBuildDir, build_dir_cc), ConfigureCC, partial(Build, build_dir_cc), partial(PackBinaries, "ZIP", build_dir_cc)]]

  while True:
    # Displays options
    ClearScreen()
    print("ozz CMake build helper tool")
    print("")
    print("Selected build configuration: %s") % config
    print("Selected generator: %s") % generator
    print("Testing enabled: %s") % enable_testing
    print("")
    print("Choose an option:")
    print("------------------")
    for key, message in sorted(options.iteritems()):
      print("  %s: %s") % (key, message[0])

    # Get input and check validity
    answer = raw_input("Enter a value: ")
    if not answer in options:
      continue

    # Execute command in a try catch to avoid crashes and allow retries.
    ClearScreen()
    try:
      for command in options[answer][1]:
        if command():
          print("\nExecution success.\n")
        else:
          print("\nExecution failed.\n")
          break
    except Exception as e:
      print("\nAn error occured during script execution: %s\n") % e

    raw_input("Press enter to continue...")

  return 0

if __name__ == '__main__':
  main()
