# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import argparse
import copy
import os

from mach.decorators import Command, CommandArgument
from mozbuild.base import BuildEnvironmentNotFoundException
from mozbuild.base import MachCommandConditions as conditions

here = os.path.abspath(os.path.dirname(__file__))
EXCLUSION_FILES = [
    os.path.join("tools", "rewriting", "Generated.txt"),
    os.path.join("tools", "rewriting", "ThirdPartyPaths.txt"),
]

EXCLUSION_FILES_OPTIONAL = []
thunderbird_excludes = os.path.join("comm", "tools", "lint", "GlobalExclude.txt")
if os.path.exists(thunderbird_excludes):
    EXCLUSION_FILES_OPTIONAL.append(thunderbird_excludes)

GLOBAL_EXCLUDES = ["**/node_modules", "tools/lint/test/files", ".hg", ".git"]

VALID_FORMATTERS = {"black", "clang-format", "rustfmt", "isort"}
VALID_ANDROID_FORMATTERS = {"android-format"}

# Code-review bot must index issues from the whole codebase when pushing
# to autoland or try repositories. In such cases, output warnings in the
# task's JSON artifact but do not fail if only warnings are found.
REPORT_WARNINGS = os.environ.get("GECKO_HEAD_REPOSITORY", "").rstrip("/") in (
    "https://hg.mozilla.org/mozilla-central",
    "https://hg.mozilla.org/integration/autoland",
    "https://hg.mozilla.org/try",
)


def setup_argument_parser():
    from mozlint import cli

    return cli.MozlintParser()


def get_global_excludes(**lintargs):
    # exclude misc paths
    excludes = GLOBAL_EXCLUDES[:]
    topsrcdir = lintargs["root"]

    # exclude top level paths that look like objdirs
    excludes.extend(
        [
            name
            for name in os.listdir(topsrcdir)
            if name.startswith("obj") and os.path.isdir(name)
        ]
    )

    if lintargs.get("include_thirdparty"):
        # For some linters, we want to include the thirdparty code too.
        # Example: trojan-source linter should run also on third party code.
        return excludes

    for path in EXCLUSION_FILES + EXCLUSION_FILES_OPTIONAL:
        with open(os.path.join(topsrcdir, path), "r") as fh:
            excludes.extend([f.strip() for f in fh.readlines()])

    return excludes


@Command(
    "lint",
    category="devenv",
    description="Run linters.",
    parser=setup_argument_parser,
    virtualenv_name="lint",
)
def lint(command_context, *runargs, **lintargs):
    """Run linters."""
    command_context.activate_virtualenv()
    from mozlint import cli, parser

    try:
        buildargs = {}
        buildargs["substs"] = copy.deepcopy(dict(command_context.substs))
        buildargs["defines"] = copy.deepcopy(dict(command_context.defines))
        buildargs["topobjdir"] = command_context.topobjdir
        lintargs.update(buildargs)
    except BuildEnvironmentNotFoundException:
        pass

    lintargs.setdefault("root", command_context.topsrcdir)
    lintargs["exclude"] = get_global_excludes(**lintargs)
    lintargs["config_paths"].insert(0, here)
    lintargs["virtualenv_bin_path"] = command_context.virtualenv_manager.bin_path
    lintargs["virtualenv_manager"] = command_context.virtualenv_manager
    if REPORT_WARNINGS and lintargs.get("show_warnings") is None:
        lintargs["show_warnings"] = "soft"
    for path in EXCLUSION_FILES:
        parser.GLOBAL_SUPPORT_FILES.append(
            os.path.join(command_context.topsrcdir, path)
        )
    setupargs = {
        "mach_command_context": command_context,
    }
    return cli.run(*runargs, setupargs=setupargs, **lintargs)


@Command(
    "eslint",
    category="devenv",
    description="Run eslint or help configure eslint for optimal development.",
)
@CommandArgument(
    "paths",
    default=None,
    nargs="*",
    help="Paths to file or directories to lint, like "
    "'browser/' Defaults to the "
    "current directory if not given.",
)
@CommandArgument(
    "-s",
    "--setup",
    default=False,
    action="store_true",
    help="Configure eslint for optimal development.",
)
@CommandArgument("-b", "--binary", default=None, help="Path to eslint binary.")
@CommandArgument(
    "--fix",
    default=False,
    action="store_true",
    help="Request that eslint automatically fix errors, where possible.",
)
@CommandArgument(
    "--rule",
    default=[],
    dest="rules",
    action="append",
    help="Specify an additional rule for ESLint to run, e.g. 'no-new-object: error'",
)
@CommandArgument(
    "extra_args",
    nargs=argparse.REMAINDER,
    help="Extra args that will be forwarded to eslint.",
)
def eslint(command_context, paths, extra_args=[], **kwargs):
    command_context._mach_context.commands.dispatch(
        "lint",
        command_context._mach_context,
        linters=["eslint"],
        paths=paths,
        argv=extra_args,
        **kwargs
    )


@Command(
    "format",
    category="devenv",
    description="Format files, alternative to 'lint --fix' ",
    parser=setup_argument_parser,
)
def format_files(command_context, paths, extra_args=[], **kwargs):
    linters = kwargs["linters"]

    formatters = VALID_FORMATTERS
    if conditions.is_android(command_context):
        formatters |= VALID_ANDROID_FORMATTERS

    if not linters:
        linters = formatters
    else:
        invalid_linters = set(linters) - formatters
        if invalid_linters:
            print(
                "error: One or more linters passed are not valid formatters. "
                "Note that only the following linters are valid formatters:"
            )
            print("\n".join(sorted(formatters)))
            return 1

    kwargs["linters"] = list(linters)

    kwargs["fix"] = True
    command_context._mach_context.commands.dispatch(
        "lint", command_context._mach_context, paths=paths, argv=extra_args, **kwargs
    )
