# Copyright 2010 The Closure Library Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS-IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility to use the Closure Compiler CLI from Python."""

import logging
import os
import re
import subprocess
import tempfile

# Pulls just the major and minor version numbers from the first line of
# 'java -version'. Versions are in the format of [0-9]+(\.[0-9]+)? See:
# http://openjdk.java.net/jeps/223
_VERSION_REGEX = re.compile(r'"([0-9]+)(?:\.([0-9]+))?')


class JsCompilerError(Exception):
  """Raised if there's an error in calling the compiler."""
  pass


def _GetJavaVersionString():
  """Get the version string from the Java VM."""
  return subprocess.check_output(['java', '-version'], stderr=subprocess.STDOUT)


def _ParseJavaVersion(version_string):
  """Returns a 2-tuple for the current version of Java installed.

  Args:
    version_string: String of the Java version (e.g. '1.7.2-ea').

  Returns:
    The major and minor versions, as a 2-tuple (e.g. (1, 7)).
  """
  match = _VERSION_REGEX.search(version_string)
  if match:
    version = tuple(int(x or 0) for x in match.groups())
    assert len(version) == 2
    return version


def _JavaSupports32BitMode():
  """Determines whether the JVM supports 32-bit mode on the platform."""
  # Suppresses process output to stderr and stdout from showing up in the
  # console as we're only trying to determine 32-bit JVM support.
  supported = False
  try:
    devnull = open(os.devnull, 'wb')
    return subprocess.call(
        ['java', '-d32', '-version'], stdout=devnull, stderr=devnull) == 0
  except IOError:
    pass
  else:
    devnull.close()
  return supported


def _GetJsCompilerArgs(compiler_jar_path, java_version, jvm_flags):
  """Assembles arguments for call to JsCompiler."""

  if java_version < (1, 7):
    raise JsCompilerError('Closure Compiler requires Java 1.7 or higher. '
                          'Please visit http://www.java.com/getjava')

  args = ['java']

  # Add JVM flags we believe will produce the best performance.  See
  # https://groups.google.com/forum/#!topic/closure-library-discuss/7w_O9-vzlj4

  # Attempt 32-bit mode if available (Java 7 on Mac OS X does not support 32-bit
  # mode, for example).
  if _JavaSupports32BitMode():
    args += ['-d32']

  # Prefer the "client" VM.
  args += ['-client']

  # Add JVM flags, if any
  if jvm_flags:
    args += jvm_flags

  # Add the application JAR.
  args += ['-jar', compiler_jar_path]

  return args


def _GetFlagFile(source_paths, compiler_flags):
  """Writes given source paths and compiler flags to a --flagfile.

  The given source_paths will be written as '--js' flags and the compiler_flags
  are written as-is.

  Args:
    source_paths: List of string js source paths.
    compiler_flags: List of string compiler flags.

  Returns:
    The file to which the flags were written.
  """
  args = []
  for path in source_paths:
    args += ['--js', path]

  # Add compiler flags, if any.
  if compiler_flags:
    args += compiler_flags

  flags_file = tempfile.NamedTemporaryFile(delete=False)
  flags_file.write(' '.join(args))
  flags_file.close()

  return flags_file


def Compile(compiler_jar_path,
            source_paths,
            jvm_flags=None,
            compiler_flags=None):
  """Prepares command-line call to Closure Compiler.

  Args:
    compiler_jar_path: Path to the Closure compiler .jar file.
    source_paths: Source paths to build, in order.
    jvm_flags: A list of additional flags to pass on to JVM.
    compiler_flags: A list of additional flags to pass on to Closure Compiler.

  Returns:
    The compiled source, as a string, or None if compilation failed.
  """

  java_version = _ParseJavaVersion(str(_GetJavaVersionString()))

  args = _GetJsCompilerArgs(compiler_jar_path, java_version, jvm_flags)

  # Write source path arguments to flag file for avoiding "The filename or
  # extension is too long" error in big projects. See
  # https://github.com/google/closure-library/pull/678
  flags_file = _GetFlagFile(source_paths, compiler_flags)
  args += ['--flagfile', flags_file.name]

  logging.info('Compiling with the following command: %s', ' '.join(args))

  try:
    return subprocess.check_output(args)
  except subprocess.CalledProcessError:
    raise JsCompilerError('JavaScript compilation failed.')
  finally:
    os.remove(flags_file.name)
