# coding=utf-8
# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import (absolute_import, division, generators, nested_scopes, print_function,
                        unicode_literals, with_statement)

import os
import pkgutil
import subprocess
from contextlib import contextmanager

from six import string_types
from twitter.common import log

from pants.base.revision import Revision
from pants.util.contextutil import temporary_dir


class Distribution(object):
  """Represents a java distribution - either a JRE or a JDK installed on the local system.

  In particular provides access to the distribution's binaries; ie: java while ensuring basic
  constraints are met.  For example a minimum version can be specified if you know need to compile
  source code or run bytecode that exercise features only available in that version forward.
  """

  class Error(Exception):
    """Indicates an invalid java distribution."""

  _CACHE = {}

  @classmethod
  def cached(cls, minimum_version=None, maximum_version=None, jdk=False):
    def scan_constraint_match():
      # Convert strings to Revision objects for apples-to-apples comparison.
      max_version = cls._parse_java_version("maximum_version", maximum_version)
      min_version = cls._parse_java_version("minimum_version", minimum_version)

      for dist in cls._CACHE.values():
        if min_version and dist.version < min_version:
          continue
        if max_version and dist.version > max_version:
          continue
        if jdk and not dist.jdk:
          continue
        return dist

    key = (minimum_version, maximum_version, jdk)
    dist = cls._CACHE.get(key)
    if not dist:
      dist = scan_constraint_match()
      if not dist:
        dist = cls.locate(minimum_version=minimum_version, maximum_version=maximum_version, jdk=jdk)
      cls._CACHE[key] = dist
    return dist

  @classmethod
  def locate(cls, minimum_version=None, maximum_version=None, jdk=False):
    """Finds a java distribution that meets any given constraints and returns it.

    First looks in JDK_HOME and JAVA_HOME if defined falling back to a search on the PATH.
    Raises Distribution.Error if no suitable java distribution could be found.
    """
    def home_bin_path(home_env_var):
      home = os.environ.get(home_env_var)
      return os.path.join(home, 'bin') if home else None

    def search_path():
      yield home_bin_path('JDK_HOME')
      yield home_bin_path('JAVA_HOME')
      path = os.environ.get('PATH')
      if path:
        for p in path.strip().split(os.pathsep):
          yield p

    for path in filter(None, search_path()):
      try:
        dist = cls(bin_path=path, minimum_version=minimum_version,
                   maximum_version=maximum_version, jdk=jdk)
        dist.validate()
        log.debug('Located %s for constraints: minimum_version'
                  ' %s, maximum_version %s, jdk %s' % (dist, minimum_version, maximum_version, jdk))
        return dist
      except (ValueError, cls.Error):
        pass

    raise cls.Error('Failed to locate a %s distribution with minimum_version %s, maximum_version %s'
                    % ('JDK' if jdk else 'JRE', minimum_version, maximum_version))

  @staticmethod
  def _parse_java_version(name, version):
    # Java version strings have been well defined since release 1.3.1 as defined here:
    #  http://www.oracle.com/technetwork/java/javase/versioning-naming-139433.html
    # These version strings comply with semver except that the traditional pre-release semver
    # slot (the 4th) can be delimited by an _ in the case of update releases of the jdk.
    # We accommodate that difference here.
    if isinstance(version, string_types):
      version = Revision.semver(version.replace('_', '-'))
    if version and not isinstance(version, Revision):
      raise ValueError('%s must be a string or a Revision object, given: %s' % (name, version))
    return version

  @staticmethod
  def _is_executable(path):
    return os.path.isfile(path) and os.access(path, os.X_OK)

  def __init__(self, bin_path='/usr/bin', minimum_version=None, maximum_version=None, jdk=False):
    """Creates a distribution wrapping the given bin_path.

    :param string bin_path: the path to the java distributions bin dir
    :param minimum_version: a modified semantic version string or else a Revision object
    :param maximum_version: a modified semantic version string or else a Revision object
    :param bool jdk: ``True`` to require the distribution be a JDK vs a JRE
    """

    if not os.path.isdir(bin_path):
      raise ValueError('The specified distribution path is invalid: %s' % bin_path)
    self._bin_path = bin_path

    self._minimum_version = self._parse_java_version("minimum_version", minimum_version)
    self._maximum_version = self._parse_java_version("maximum_version", maximum_version)

    self._jdk = jdk

    self._is_jdk = False
    self._system_properties = None
    self._version = None
    self._validated_binaries = {}

  @property
  def jdk(self):
    self.validate()
    return self._is_jdk

  @property
  def system_properties(self):
    """Returns a dict containing the system properties of this java distribution."""
    return dict(self._get_system_properties(self.java))

  @property
  def version(self):