#!/usr/bin/env python
# Copyright (c) 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

from __future__ import absolute_import
import argparse
import logging
import os
import platform
import shutil
import stat
import subprocess
import sys
import time
import threading
import re
import json
import tempfile
import six
from six.moves import range


_V8_DIR = os.path.abspath(
    os.path.join(os.path.dirname(__file__), os.path.pardir, 'third_party',
                 'v8'))

_JS_PARSER_DIR = os.path.abspath(
    os.path.join(os.path.dirname(__file__), os.path.pardir, 'third_party',
                 'parse5', 'parse5.js'))


_BOOTSTRAP_JS_DIR = os.path.abspath(
    os.path.join(os.path.dirname(__file__), 'd8_bootstrap.js'))

_BASE64_COMPAT_DIR = os.path.abspath(
    os.path.join(os.path.dirname(__file__), 'base64_compat.js'))

_PATH_UTILS_JS_DIR = os.path.abspath(
    os.path.join(os.path.dirname(__file__), 'path_utils.js'))

_HTML_IMPORTS_LOADER_JS_DIR = os.path.abspath(
    os.path.join(os.path.dirname(__file__), 'html_imports_loader.js'))

_HTML_TO_JS_GENERATOR_JS_DIR = os.path.abspath(
    os.path.join(os.path.dirname(__file__), 'html_to_js_generator.js'))


_BOOTSTRAP_JS_CONTENT = None
_NUM_TRIALS = 3


def _ValidateSourcePaths(source_paths):
  if source_paths is None:
    return
  for x in source_paths:
    assert os.path.exists(x)
    assert os.path.isdir(x)
    assert os.path.isabs(x)


def _EscapeJsString(s):
  assert isinstance(s, str)
  return json.dumps(s)

def _RenderTemplateStringForJsSource(source, template, replacement_string):
  return source.replace(template, _EscapeJsString(replacement_string))


def _GetBootStrapJsContent(source_paths):
  assert isinstance(source_paths, list)
  global _BOOTSTRAP_JS_CONTENT
  if not _BOOTSTRAP_JS_CONTENT:
    with open(_BOOTSTRAP_JS_DIR, 'r') as f:
      _BOOTSTRAP_JS_CONTENT = f.read()

  bsc = _BOOTSTRAP_JS_CONTENT


  # Ensure that source paths are unique.
  source_paths = list(set(source_paths))
  source_paths_string = '[%s]' % (
      ','.join(_EscapeJsString(s) for s in source_paths))
  bsc = bsc.replace('<%source_paths%>', source_paths_string)
  bsc = _RenderTemplateStringForJsSource(
      bsc, '<%current_working_directory%>', os.getcwd())
  bsc = _RenderTemplateStringForJsSource(
      bsc, '<%path_utils_js_path%>', _PATH_UTILS_JS_DIR)
  bsc = _RenderTemplateStringForJsSource(
    bsc, '<%html_imports_loader_js_path%>', _HTML_IMPORTS_LOADER_JS_DIR)
  bsc = _RenderTemplateStringForJsSource(
      bsc, '<%html_to_js_generator_js_path%>', _HTML_TO_JS_GENERATOR_JS_DIR)
  bsc = _RenderTemplateStringForJsSource(
      bsc, '<%js_parser_path%>', _JS_PARSER_DIR)
  bsc = _RenderTemplateStringForJsSource(
      bsc, '<%base64_compat_path%>', _BASE64_COMPAT_DIR)
  bsc += '\n//@ sourceURL=%s\n' % _BOOTSTRAP_JS_DIR
  return bsc


def _IsValidJsOrHTMLFile(parser, js_file_arg):
  if not os.path.exists(js_file_arg):
    parser.error('The file %s does not exist' % js_file_arg)
  _, extension = os.path.splitext(js_file_arg)
  if extension not in ('.js', '.html'):
    parser.error('Input must be a JavaScript or HTML file')
  return js_file_arg


def _GetD8BinaryPathForPlatform():
  def _D8Path(*paths):
    """Join paths and make it executable."""
    assert isinstance(paths, tuple)
    exe = os.path.join(_V8_DIR, *paths)
    st = os.stat(exe)
    if not st.st_mode & stat.S_IEXEC:
      os.chmod(exe, st.st_mode | stat.S_IEXEC)
    return exe

  if platform.system() == 'Linux' and platform.machine() == 'x86_64':
    return _D8Path('linux', 'x86_64', 'd8')
  elif platform.system() == 'Linux' and platform.machine() == 'aarch64':
    return _D8Path('linux', 'arm', 'd8')
  elif platform.system() == 'Linux' and platform.machine() == 'armv7l':
    return _D8Path('linux', 'arm', 'd8')
  elif platform.system() == 'Linux' and platform.machine() == 'mips':
    return _D8Path('linux', 'mips', 'd8')
  elif platform.system() == 'Linux' and platform.machine() == 'mips64':
    return _D8Path('linux', 'mips64', 'd8')
  elif platform.system() == 'Darwin' and platform.machine() == 'x86_64':
    return _D8Path('mac', 'x86_64', 'd8')
  elif platform.system() == 'Darwin' and platform.machine() == 'arm64':
    return _D8Path('mac', 'arm', 'd8')
  elif platform.system() == 'Windows' and platform.machine() == 'AMD64':
    return _D8Path('win', 'AMD64', 'd8.exe')
  else:
    raise NotImplementedError(
        'd8 binary for this platform (%s) and architecture (%s) is not yet'
        ' supported' % (platform.system(), platform.machine()))


# Speculative change to workaround a failure on Windows: speculation is that the
# script attempts to remove a file before the process using the file has
# completely terminated. So the function here attempts to retry a few times with
# a second timeout between retries. More details at https://crbug.com/946012
# TODO(sadrul): delete this speculative change since it didn't work.
def _RemoveTreeWithRetry(tree, retry=3):
  for count in range(retry):
    try:
      shutil.rmtree(tree)
      return
    except:
      if count == retry - 1:
        raise
      logging.warning('Removing %s failed. Retrying in 1 second ...' % tree)
      time.sleep(1)


class RunResult(object):
  def __init__(self, returncode, stdout):
    self.returncode = returncode
    self.stdout = stdout


def ExecuteFile(file_path, source_paths=None, js_args=None, v8_args=None,
                stdout=subprocess.PIPE, stdin=subprocess.PIPE):
  """Execute JavaScript program in |file_path|.

  Args: