from collections import OrderedDict
import re
import pprint
import sys

from typing import Dict, List, Mapping, Optional, Pattern, Set, Tuple
MYPY = False
if MYPY:
    from typing import ClassVar

from mypy import defaults
from mypy.util import get_class_descriptors, replace_object_state


class BuildType:
    STANDARD = 0  # type: ClassVar[int]
    MODULE = 1  # type: ClassVar[int]
    PROGRAM_TEXT = 2  # type: ClassVar[int]


PER_MODULE_OPTIONS = {
    # Please keep this list sorted
    "always_false",
    "always_true",
    "check_untyped_defs",
    "debug_cache",
    "disallow_any_decorated",
    "disallow_any_explicit",
    "disallow_any_expr",
    "disallow_any_generics",
    "disallow_any_unimported",
    "disallow_incomplete_defs",
    "disallow_subclassing_any",
    "disallow_untyped_calls",
    "disallow_untyped_decorators",
    "disallow_untyped_defs",
    "follow_imports",
    "follow_imports_for_stubs",
    "ignore_errors",
    "ignore_missing_imports",
    "local_partial_types",
    "no_implicit_optional",
    "show_none_errors",
    "strict_boolean",
    "strict_optional",
    "strict_optional_whitelist",
    "warn_no_return",
    "warn_return_any",
    "warn_unused_ignores",
}

OPTIONS_AFFECTING_CACHE = ((PER_MODULE_OPTIONS |
                            {"quick_and_dirty", "platform", "bazel"})
                           - {"debug_cache"})


class Options:
    """Options collected from flags."""

    def __init__(self) -> None:
        # Cache for clone_for_module()
        self.per_module_cache = None  # type: Optional[Dict[str, Options]]

        # -- build options --
        self.build_type = BuildType.STANDARD
        self.python_version = sys.version_info[:2]  # type: Tuple[int, int]
        # The executable used to search for PEP 561 packages. If this is None,
        # then mypy does not search for PEP 561 packages.
        self.python_executable = sys.executable  # type: Optional[str]
        self.platform = sys.platform
        self.custom_typing_module = None  # type: Optional[str]
        self.custom_typeshed_dir = None  # type: Optional[str]
        self.mypy_path = []  # type: List[str]
        self.report_dirs = {}  # type: Dict[str, str]
        # Show errors in PEP 561 packages/site-packages modules
        self.no_silence_site_packages = False
        self.ignore_missing_imports = False
        self.follow_imports = 'normal'  # normal|silent|skip|error
        # Whether to respect the follow_imports setting even for stub files.
        # Intended to be used for disabling specific stubs.
        self.follow_imports_for_stubs = False  # type: bool

        # disallow_any options
        self.disallow_any_generics = False
        self.disallow_any_unimported = False
        self.disallow_any_expr = False
        self.disallow_any_decorated = False
        self.disallow_any_explicit = False

        # Disallow calling untyped functions from typed ones
        self.disallow_untyped_calls = False

        # Disallow defining untyped (or incompletely typed) functions
        self.disallow_untyped_defs = False

        # Disallow defining incompletely typed functions
        self.disallow_incomplete_defs = False

        # Type check unannotated functions
        self.check_untyped_defs = False

        # Disallow decorating typed functions with untyped decorators
        self.disallow_untyped_decorators = False

        # Disallow subclassing values of type 'Any'
        self.disallow_subclassing_any = False

        # Also check typeshed for missing annotations
        self.warn_incomplete_stub = False

        # Warn about casting an expression to its inferred type
        self.warn_redundant_casts = False

        # Warn about falling off the end of a function returning non-None
        self.warn_no_return = True

        # Warn about returning objects of type Any when the function is
        # declared with a precise type
        self.warn_return_any = False

        # Warn about unused '# type: ignore' comments
        self.warn_unused_ignores = False

        # Warn about unused '[mypy-<pattern>] config sections
        self.warn_unused_configs = False

        # Files in which to ignore all non-fatal errors
        self.ignore_errors = False

        # Only allow booleans in conditions
        self.strict_boolean = False

        # Apply strict None checking
        self.strict_optional = True

        # Show "note: In function "foo":" messages.
        self.show_error_context = False

        # Files in which to allow strict-Optional related errors
        # TODO: Kill this in favor of show_none_errors
        self.strict_optional_whitelist = None   # type: Optional[List[str]]

        # Alternate way to show/hide strict-None-checking related errors
        self.show_none_errors = True

        # Don't assume arguments with default values of None are Optional
        self.no_implicit_optional = False

        # Variable names considered True
        self.always_true = []  # type: List[str]

        # Variable names considered False
        self.always_false = []  # type: List[str]

        # Use script name instead of __main__
        self.scripts_are_modules = False

        # Config file name
        self.config_file = None  # type: Optional[str]

        # Write junit.xml to given file
        self.junit_xml = None  # type: Optional[str]

        # Caching and incremental checking options
        self.incremental = True
        self.cache_dir = defaults.CACHE_DIR
        self.debug_cache = False
        self.quick_and_dirty = False
        self.skip_version_check = False
        self.fine_grained_incremental = False
        # Include fine-grained dependencies in written cache files
        self.cache_fine_grained = False
        # Read cache files in fine-grained incremental mode (cache must include dependencies)
        self.use_fine_grained_cache = False

        # Paths of user plugins
        self.plugins = []  # type: List[str]

        # Per-module options (raw)
        self.per_module_options = OrderedDict()  # type: OrderedDict[str, Dict[str, object]]
        self.glob_options = []  # type: List[Tuple[str, Pattern[str]]]
        self.unused_configs = set()  # type: Set[str]

        # -- development options --
        self.verbosity = 0  # More verbose messages (for troubleshooting)
        self.pdb = False
        self.show_traceback = False
        self.dump_type_stats = False
        self.dump_inference_stats = False

        # -- test options --
        # Stop after the semantic analysis phase
        self.semantic_analysis_only = False

        # Use stub builtins fixtures to speed up tests
        self.use_builtins_fixtures = False

        # -- experimental options --
        self.shadow_file = None  # type: Optional[List[List[str]]]
        self.show_column_numbers = False  # type: bool
        self.dump_graph = False
        self.dump_deps = False
        self.logical_deps = False
        # If True, partial types can't span a module top level and a function
        self.local_partial_types = False
        # Some behaviors are changed when using Bazel (https://bazel.build).
        self.bazel = False
        # If True, export inferred types for all expressions as BuildResult.types
        self.export_types = False
        # List of package roots -- directories under these are packages even
        # if they don't have __init__.py.
        self.package_root = []  # type: List[str]
        self.cache_map = {}  # type: Dict[str, Tuple[str, str]]

    def snapshot(self) -> object:
        """Produce a comparable snapshot of this Option"""
        # Under mypyc, we don't have a __dict__, so we need to do worse things.
        d = dict(getattr(self, '__dict__', ()))
        for k in get_class_descriptors(Options):
            if hasattr(self, k):
                d[k] = getattr(self, k)
        del d['per_module_cache']
        return d

    def __repr__(self) -> str:
        return 'Options({})'.format(pprint.pformat(self.snapshot()))

    def apply_changes(self, changes: Dict[str, object]) -> 'Options':
        new_options = Options()
        # Under mypyc, we don't have a __dict__, so we need to do worse things.
        replace_object_state(new_options, self, copy_dict=True)
        for key, value in changes.items():
            setattr(new_options, key, value)
        return new_options

    def build_per_module_cache(self) -> None:
        self.per_module_cache = {}

        # Config precedence is as follows:
        #  1. Concrete section names: foo.bar.baz
        #  2. "Unstructured" glob patterns: foo.*.baz, in the order
        #     they appear in the file (last wins)
        #  3. "Well-structured" wildcard patterns: foo.bar.*, in specificity order.

        # Since structured configs inherit from structured configs above them in the hierarchy,
        # we need to process per-module configs in a careful order.
        # We have to process foo.* before foo.bar.* before foo.bar,
        # and we need to apply *.bar to foo.bar but not to foo.bar.*.
        # To do this, process all well-structured glob configs before non-glob configs and
        # exploit the fact that foo.* sorts earlier ASCIIbetically (unicodebetically?)
        # than foo.bar.*.
        # (A section being "processed last" results in its config "winning".)
        # Unstructured glob configs are stored and are all checked for each module.
        unstructured_glob_keys = [k for k in self.per_module_options.keys()
                                  if '*' in k[:-1]]
        structured_keys = [k for k in self.per_module_options.keys()
                           if '*' not in k[:-1]]
        wildcards = sorted(k for k in structured_keys if k.endswith('.*'))
        concrete = [k for k in structured_keys if not k.endswith('.*')]

        for glob in unstructured_glob_keys:
            self.glob_options.append((glob, self.compile_glob(glob)))

        # We (for ease of implementation) treat unstructured glob
        # sections as used if any real modules use them or if any
        # concrete config sections use them. This means we need to
        # track which get used while constructing.
        self.unused_configs = set(unstructured_glob_keys)

        for key in wildcards + concrete:
            # Find what the options for this key would be, just based
            # on inheriting from parent configs.
            options = self.clone_for_module(key)
            # And then update it with its per-module options.
            self.per_module_cache[key] = options.apply_changes(self.per_module_options[key])

        # Add the more structured sections into unused configs, since
        # they only count as used if actually used by a real module.
        self.unused_configs.update(structured_keys)

    def clone_for_module(self, module: str) -> 'Options':
        """Create an Options object that incorporates per-module options.

        NOTE: Once this method is called all Options objects should be
        considered read-only, else the caching might be incorrect.
        """
        if self.per_module_cache is None:
            self.build_per_module_cache()
        assert self.per_module_cache is not None

        # If the module just directly has a config entry, use it.
        if module in self.per_module_cache:
            self.unused_configs.discard(module)
            return self.per_module_cache[module]

        # If not, search for glob paths at all the parents. So if we are looking for
        # options for foo.bar.baz, we search foo.bar.baz.*, foo.bar.*, foo.*,
        # in that order, looking for an entry.
        # This is technically quadratic in the length of the path, but module paths
        # don't actually get all that long.
        options = self
        path = module.split('.')
        for i in range(len(path), 0, -1):
            key = '.'.join(path[:i] + ['*'])
            if key in self.per_module_cache:
                self.unused_configs.discard(key)
                options = self.per_module_cache[key]
                break

        # OK and *now* we need to look for unstructured glob matches.
        # We only do this for concrete modules, not structured wildcards.
        if not module.endswith('.*'):
            for key, pattern in self.glob_options:
                if pattern.match(module):
                    self.unused_configs.discard(key)
                    options = options.apply_changes(self.per_module_options[key])

        # We could update the cache to directly point to modules once
        # they have been looked up, but in testing this made things
        # slower and not faster, so we don't bother.

        return options

    def compile_glob(self, s: str) -> Pattern[str]:
        # Compile one of the glob patterns to a regex so that '.*' can
        # match *zero or more* module sections. This means we compile
        # '.*' into '(\..*)?'.
        parts = s.split('.')
        expr = re.escape(parts[0]) if parts[0] != '*' else '.*'
        for part in parts[1:]:
            expr += re.escape('.' + part) if part != '*' else '(\..*)?'
        return re.compile(expr + '\\Z')

    def select_options_affecting_cache(self) -> Mapping[str, object]:
        return {opt: getattr(self, opt) for opt in OPTIONS_AFFECTING_CACHE}
