# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1.  Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
# 2.  Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""Supports the parsing of command-line options for check-webkit-style."""

import logging
from optparse import OptionParser
import os.path
import sys

from filter import validate_filter_rules
# This module should not import anything from checker.py.

_log = logging.getLogger(__name__)

_USAGE = """usage: %prog [--help] [options] [path1] [path2] ...

Overview:
  Check coding style according to WebKit style guidelines:

      http://webkit.org/coding/coding-style.html

  Path arguments can be files and directories.  If neither a git commit nor
  paths are passed, then all changes in your source control working directory
  are checked.

Style errors:
  This script assigns to every style error a confidence score from 1-5 and
  a category name.  A confidence score of 5 means the error is certainly
  a problem, and 1 means it could be fine.

  Category names appear in error messages in brackets, for example
  [whitespace/indent].  See the options section below for an option that
  displays all available categories and which are reported by default.

Filters:
  Use filters to configure what errors to report.  Filters are specified using
  a comma-separated list of boolean filter rules.  The script reports errors
  in a category if the category passes the filter, as described below.

  All categories start out passing.  Boolean filter rules are then evaluated
  from left to right, with later rules taking precedence.  For example, the
  rule "+foo" passes any category that starts with "foo", and "-foo" fails
  any such category.  The filter input "-whitespace,+whitespace/braces" fails
  the category "whitespace/tab" and passes "whitespace/braces".

  Examples: --filter=-whitespace,+whitespace/braces
            --filter=-whitespace,-runtime/printf,+runtime/printf_format
            --filter=-,+build/include_what_you_use

Paths:
  Certain style-checking behavior depends on the paths relative to
  the WebKit source root of the files being checked.  For example,
  certain types of errors may be handled differently for files in
  WebKit/gtk/webkit/ (e.g. by suppressing "readability/naming" errors
  for files in this directory).

  Consequently, if the path relative to the source root cannot be
  determined for a file being checked, then style checking may not
  work correctly for that file.  This can occur, for example, if no
  WebKit checkout can be found, or if the source root can be detected,
  but one of the files being checked lies outside the source tree.

  If a WebKit checkout can be detected and all files being checked
  are in the source tree, then all paths will automatically be
  converted to paths relative to the source root prior to checking.
  This is also useful for display purposes.

  Currently, this command can detect the source root only if the
  command is run from within a WebKit checkout (i.e. if the current
  working directory is below the root of a checkout).  In particular,
  it is not recommended to run this script from a directory outside
  a checkout.

  Running this script from a top-level WebKit source directory and
  checking only files in the source tree will ensure that all style
  checking behaves correctly -- whether or not a checkout can be
  detected.  This is because all file paths will already be relative
  to the source root and so will not need to be converted."""

_EPILOG = ("This script can miss errors and does not substitute for "
           "code review.")


# This class should not have knowledge of the flag key names.
class DefaultCommandOptionValues(object):

    """Stores the default check-webkit-style command-line options.

    Attributes:
      output_format: A string that is the default output format.
      min_confidence: An integer that is the default minimum confidence level.

    """

    def __init__(self, min_confidence, output_format):
        self.min_confidence = min_confidence
        self.output_format = output_format


# This class should not have knowledge of the flag key names.
class CommandOptionValues(object):

    """Stores the option values passed by the user via the command line.

    Attributes:
      is_verbose: A boolean value of whether verbose logging is enabled.

      filter_rules: The list of filter rules provided by the user.
                    These rules are appended to the base rules and
                    path-specific rules and so take precedence over
                    the base filter rules, etc.

      git_commit: A string representing the git commit to check.
                  The default is None.

      min_confidence: An integer between 1 and 5 inclusive that is the
                      minimum confidence level of style errors to report.
                      The default is 1, which reports all errors.

      output_format: A string that is the output format.  The supported
                     output formats are "emacs" which emacs can parse
                     and "vs7" which Microsoft Visual Studio 7 can parse.

    """
    def __init__(self,
                 filter_rules=None,
                 git_commit=None,
                 diff_files=None,
                 is_verbose=False,
                 min_confidence=1,
                 output_format="emacs",
                 commit_queue=False,
                 git_index=False):
        if filter_rules is None:
            filter_rules = []

        if (min_confidence < 1) or (min_confidence > 5):
            raise ValueError('Invalid "min_confidence" parameter: value '
                             "must be an integer between 1 and 5 inclusive. "
                             'Value given: "%s".' % min_confidence)

        if output_format not in ("emacs", "vs7"):
            raise ValueError('Invalid "output_format" parameter: '
                             'value must be "emacs" or "vs7". '
                             'Value given: "%s".' % output_format)

        self.filter_rules = filter_rules
        self.git_commit = git_commit
        self.diff_files = diff_files
        self.is_verbose = is_verbose
        self.min_confidence = min_confidence
        self.output_format = output_format
        self.commit_queue = commit_queue
        self.git_index = git_index

    # Useful for unit testing.
    def __eq__(self, other):
        """Return whether this instance is equal to another."""
        if self.filter_rules != other.filter_rules:
            return False
        if self.git_commit != other.git_commit:
            return False
        if self.diff_files != other.diff_files:
            return False
        if self.is_verbose != other.is_verbose:
            return False
        if self.min_confidence != other.min_confidence:
            return False
        if self.output_format != other.output_format:
            return False
        if self.git_index != other.git_index:
            return False

        return True

    # Useful for unit testing.
    def __ne__(self, other):
        # Python does not automatically deduce this from __eq__().
        return not self.__eq__(other)


class ArgumentPrinter(object):

    """Supports the printing of check-webkit-style command arguments."""

    def _flag_pair_to_string(self, flag_key, flag_value):
        return '--%(key)s=%(val)s' % {'key': flag_key, 'val': flag_value}

    def to_flag_string(self, options):
        """Return a flag string of the given CommandOptionValues instance.

        This method orders the flag values alphabetically by the flag key.

        Args:
          options: A CommandOptionValues instance.

        """
        flags = {}
        flags['min-confidence'] = options.min_confidence
        flags['output'] = options.output_format
        # Only include the filter flag if user-provided rules are present.
        filter_rules = options.filter_rules
        if filter_rules:
            flags['filter'] = ",".join(filter_rules)
        if options.git_commit:
            flags['git-commit'] = options.git_commit
        if options.diff_files:
            flags['diff_files'] = options.diff_files

        flag_string = ''
        # Alphabetizing lets us unit test this method.
        for key in sorted(flags.keys()):
            flag_string += self._flag_pair_to_string(key, flags[key]) + ' '

        return flag_string.strip()


class ArgumentParser(object):

    # FIXME: Move the documentation of the attributes to the __init__
    #        docstring after making the attributes internal.
    """Supports the parsing of check-webkit-style command arguments.

    Attributes:
      create_usage: A function that accepts a DefaultCommandOptionValues
                    instance and returns a string of usage instructions.
                    Defaults to the function that generates the usage
                    string for check-webkit-style.
      default_options: A DefaultCommandOptionValues instance that provides
                       the default values for options not explicitly
                       provided by the user.
      stderr_write: A function that takes a string as a parameter and
                    serves as stderr.write.  Defaults to sys.stderr.write.
                    This parameter should be specified only for unit tests.

    """

    def __init__(self,
                 all_categories,
                 default_options,
                 base_filter_rules=None,
                 mock_stderr=None,
                 usage=None):
        """Create an ArgumentParser instance.

        Args:
          all_categories: The set of all available style categories.
          default_options: See the corresponding attribute in the class
                           docstring.
        Keyword Args:
          base_filter_rules: The list of filter rules at the beginning of
                             the list of rules used to check style.  This
                             list has the least precedence when checking
                             style and precedes any user-provided rules.
                             The class uses this parameter only for display
                             purposes to the user.  Defaults to the empty list.
          create_usage: See the documentation of the corresponding
                        attribute in the class docstring.
          stderr_write: See the documentation of the corresponding
                        attribute in the class docstring.

        """
        if base_filter_rules is None:
            base_filter_rules = []
        stderr = sys.stderr if mock_stderr is None else mock_stderr
        if usage is None:
            usage = _USAGE

        self._all_categories = all_categories
        self._base_filter_rules = base_filter_rules

        # FIXME: Rename these to reflect that they are internal.
        self.default_options = default_options
        self.stderr_write = stderr.write

        self._parser = self._create_option_parser(stderr=stderr,
            usage=usage,
            default_min_confidence=self.default_options.min_confidence,
            default_output_format=self.default_options.output_format)

    def _create_option_parser(self, stderr, usage,
                              default_min_confidence, default_output_format):
        # Since the epilog string is short, it is not necessary to replace
        # the epilog string with a mock epilog string when testing.
        # For this reason, we use _EPILOG directly rather than passing it
        # as an argument like we do for the usage string.
        parser = OptionParser(usage=usage, epilog=_EPILOG)

        filter_help = ('set a filter to control what categories of style '
                       'errors to report.  Specify a filter using a comma-'
                       'delimited list of boolean filter rules, for example '
                       '"--filter -whitespace,+whitespace/braces".  To display '
                       'all categories and which are enabled by default, pass '
                       """no value (e.g. '-f ""' or '--filter=').""")
        parser.add_option("-f", "--filter-rules", metavar="RULES",
                          dest="filter_value", help=filter_help)

        git_commit_help = ("check all changes in the given commit. "
                           "Use 'commit_id..' to check all changes after commmit_id")
        parser.add_option("-g", "--git-diff", "--git-commit",
                          metavar="COMMIT", dest="git_commit", help=git_commit_help,)

        diff_files_help = "diff the files passed on the command line rather than checking the style of every line"
        parser.add_option("--diff-files", action="store_true", dest="diff_files", default=False, help=diff_files_help)

        min_confidence_help = ("set the minimum confidence of style errors "
                               "to report.  Can be an integer 1-5, with 1 "
                               "displaying all errors.  Defaults to %default.")
        parser.add_option("-m", "--min-confidence", metavar="INT",
                          type="int", dest="min_confidence",
                          default=default_min_confidence,
                          help=min_confidence_help)

        output_format_help = ('set the output format, which can be "emacs" '
                              'or "vs7" (for Visual Studio).  '
                              'Defaults to "%default".')
        parser.add_option("-o", "--output-format", metavar="FORMAT",
                          choices=["emacs", "vs7"],
                          dest="output_format", default=default_output_format,
                          help=output_format_help)

        verbose_help = "enable verbose logging."
        parser.add_option("-v", "--verbose", dest="is_verbose", default=False,
                          action="store_true", help=verbose_help)

        commit_queue_help = "force commit queue to check contributors.json change"
        parser.add_option("--commit-queue", action="store_true", dest="commit_queue", default=False, help=commit_queue_help)
        parser.add_option("--git-index", action="store_true", dest="git_index", default=False, help="Scan staged files only")

        # Override OptionParser's error() method so that option help will
        # also display when an error occurs.  Normally, just the usage
        # string displays and not option help.
        parser.error = self._parse_error

        # Override OptionParser's print_help() method so that help output
        # does not render to the screen while running unit tests.
        print_help = parser.print_help
        parser.print_help = lambda file=stderr: print_help(file=file)

        return parser

    def _parse_error(self, error_message):
        """Print the help string and an error message, and exit."""
        # The method format_help() includes both the usage string and
        # the flag options.
        help = self._parser.format_help()
        # Separate help from the error message with a single blank line.
        self.stderr_write(help + "\n")
        if error_message:
            _log.error(error_message)

        # Since we are using this method to replace/override the Python
        # module optparse's OptionParser.error() method, we match its
        # behavior and exit with status code 2.
        #
        # As additional background, Python documentation says--
        #
        # "Unix programs generally use 2 for command line syntax errors
        #  and 1 for all other kind of errors."
        #
        # (from http://docs.python.org/library/sys.html#sys.exit )
        sys.exit(2)

    def _exit_with_categories(self):
        """Exit and print the style categories and default filter rules."""
        self.stderr_write('\nAll categories:\n')
        for category in sorted(self._all_categories):
            self.stderr_write('    ' + category + '\n')

        self.stderr_write('\nDefault filter rules**:\n')
        for filter_rule in sorted(self._base_filter_rules):
            self.stderr_write('    ' + filter_rule + '\n')
        self.stderr_write('\n**The command always evaluates the above rules, '
                          'and before any --filter flag.\n\n')

        sys.exit(0)

    def _parse_filter_flag(self, flag_value):
        """Parse the --filter flag, and return a list of filter rules.

        Args:
          flag_value: A string of comma-separated filter rules, for
                      example "-whitespace,+whitespace/indent".

        """
        filters = []
        for uncleaned_filter in flag_value.split(','):
            filter = uncleaned_filter.strip()
            if not filter:
                continue
            filters.append(filter)
        return filters

    def parse(self, args):
        """Parse the command line arguments to check-webkit-style.

        Args:
          args: A list of command-line arguments as returned by sys.argv[1:].

        Returns:
          A tuple of (paths, options)

          paths: The list of paths to check.
          options: A CommandOptionValues instance.

        """
        (options, paths) = self._parser.parse_args(args=args)

        filter_value = options.filter_value
        git_commit = options.git_commit
        diff_files = options.diff_files
        is_verbose = options.is_verbose
        min_confidence = options.min_confidence
        output_format = options.output_format
        commit_queue = options.commit_queue
        git_index = options.git_index

        if filter_value is not None and not filter_value:
            # Then the user explicitly passed no filter, for
            # example "-f ''" or "--filter=".
            self._exit_with_categories()

        # Validate user-provided values.

        min_confidence = int(min_confidence)
        if (min_confidence < 1) or (min_confidence > 5):
            self._parse_error('option --min-confidence: invalid integer: '
                              '%s: value must be between 1 and 5'
                              % min_confidence)

        if filter_value:
            filter_rules = self._parse_filter_flag(filter_value)
        else:
            filter_rules = []

        try:
            validate_filter_rules(filter_rules, self._all_categories)
        except ValueError, err:
            self._parse_error(err)

        options = CommandOptionValues(filter_rules=filter_rules,
                                      git_commit=git_commit,
                                      diff_files=diff_files,
                                      is_verbose=is_verbose,
                                      min_confidence=min_confidence,
                                      output_format=output_format,
                                      commit_queue=commit_queue,
                                      git_index=git_index)

        return (paths, options)
