# 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 logging
import os
import subprocess
from abc import abstractmethod, abstractproperty
from contextlib import contextmanager

from six import string_types
from twitter.common.collections import maybe_list

from pants.base.build_environment import get_buildroot
from pants.java.distribution.distribution import Distribution
from pants.util.contextutil import environment_as
from pants.util.dirutil import relativize_paths
from pants.util.meta import AbstractClass


logger = logging.getLogger(__name__)


class Executor(AbstractClass):
  """Executes java programs."""

  @staticmethod
  def _scrub_args(classpath, main, jvm_options, args, cwd):
    classpath = maybe_list(classpath)
    if not isinstance(main, string_types) or not main:
      raise ValueError('A non-empty main classname is required, given: {}'.format(main))
    jvm_options = maybe_list(jvm_options or ())
    args = maybe_list(args or ())
    return classpath, main, jvm_options, args, cwd

  class Error(Exception):
    """Indicates an error launching a java program."""

  class Runner(object):
    """A re-usable executor that can run a configured java command line."""

    @abstractproperty
    def executor(self):
      """Returns the executor this runner uses to run itself."""

    @property
    def cmd(self):
      """Returns a string representation of the command that will be run."""
      return ' '.join(self.command)

    @abstractproperty
    def command(self):
      """Returns a copy of the command line that will be run as a list of command line tokens."""

    @abstractmethod
    def run(self, stdout=None, stderr=None, cwd=None):
      """Runs the configured java command.

      If there is a problem executing tha java program subclasses should raise Executor.Error.
      Its guaranteed that all arguments are valid as documented in `execute`

      :param stdout: An optional stream to pump stdout to; defaults to `sys.stdout`.
      :param stderr: An optional stream to pump stderr to; defaults to `sys.stderr`.
      :param string cwd: optionally set the working directory
      """

  def __init__(self, distribution=None):
    """Constructs an Executor that can be used to launch java programs.

    :param distribution: an optional validated java distribution to use when launching java
      programs
    """
    if distribution:
      if not isinstance(distribution, Distribution):
        raise ValueError('A valid distribution is required, given: {}'.format(distribution))
      distribution.validate()
    else:
      distribution = Distribution.cached()

    self._distribution = distribution

  @property
  def distribution(self):
    """Returns the `Distribution` this executor runs via."""
    return self._distribution

  def runner(self, classpath, main, jvm_options=None, args=None, cwd=None):
    """Returns an `Executor.Runner` for the given java command."""
    return self._runner(*self._scrub_args(classpath, main, jvm_options, args, cwd=cwd))

  def execute(self, classpath, main, jvm_options=None, args=None, stdout=None, stderr=None,
      cwd=None):
    """Launches the java program defined by the classpath and main.

    :param list classpath: the classpath for the java program
    :param string main: the fully qualified class name of the java program's entry point
    :param list jvm_options: an optional sequence of options for the underlying jvm
    :param list args: an optional sequence of args to pass to the java program
    :param string cwd: optionally set the working directory

    Returns the exit code of the java program.
    Raises Executor.Error if there was a problem launching java itself.
    """
    executor = self.runner(classpath=classpath, main=main, jvm_options=jvm_options, args=args,
                           cwd=cwd)
    return executor.run(stdout=stdout, stderr=stderr, cwd=cwd)

  @abstractmethod
  def _runner(self, classpath, main, jvm_options, args, cwd=None):
    """Subclasses should return a `Runner` that can execute the given java main."""

  def _create_command(self, classpath, main, jvm_options, args, cwd=None):
    cmd = [self._distribution.java]
    cmd.extend(jvm_options)
    if cwd:
      classpath = relativize_paths(classpath, cwd)
    cmd.extend(['-cp', os.pathsep.join(classpath), main])
    cmd.extend(args)
    return cmd


class CommandLineGrabber(Executor):
  """Doesn't actually execute anything, just captures the cmd line."""
  def __init__(self, distribution=None):
    super(CommandLineGrabber, self).__init__(distribution=distribution)
    self._command = None  # Initialized when we run something.