# Copyright 2017-2020 Palantir Technologies, Inc.
# Copyright 2021- Python Language Server Contributors.

"""Linter pluging for flake8"""
import logging
import os.path
import re
from pathlib import PurePath
from subprocess import PIPE, Popen

from pylsp import hookimpl, lsp

log = logging.getLogger(__name__)
FIX_IGNORES_RE = re.compile(r'([^a-zA-Z0-9_,]*;.*(\W+||$))')


@hookimpl
def pylsp_settings():
    # Default flake8 to disabled
    return {'plugins': {'flake8': {'enabled': False}}}


@hookimpl
def pylsp_lint(workspace, document):
    config = workspace._config
    settings = config.plugin_settings('flake8', document_path=document.path)
    log.debug("Got flake8 settings: %s", settings)

    ignores = settings.get("ignore", [])
    per_file_ignores = settings.get("perFileIgnores")

    if per_file_ignores:
        for path in per_file_ignores:
            file_pat, errors = path.split(":")
            if PurePath(document.path).match(file_pat):
                ignores.extend(errors.split(","))

    opts = {
        'config': settings.get('config'),
        'exclude': settings.get('exclude'),
        'filename': settings.get('filename'),
        'hang-closing': settings.get('hangClosing'),
        'ignore': ignores or None,
        'max-line-length': settings.get('maxLineLength'),
        'select': settings.get('select'),
    }

    # flake takes only absolute path to the config. So we should check and
    # convert if necessary
    if opts.get('config') and not os.path.isabs(opts.get('config')):
        opts['config'] = os.path.abspath(os.path.expanduser(os.path.expandvars(
            opts.get('config')
        )))
        log.debug("using flake8 with config: %s", opts['config'])

    # Call the flake8 utility then parse diagnostics from stdout
    flake8_executable = settings.get('executable', 'flake8')

    args = build_args(opts)
    output = run_flake8(flake8_executable, args, document)
    return parse_stdout(document, output)


def run_flake8(flake8_executable, args, document):
    """Run flake8 with the provided arguments, logs errors
    from stderr if any.
    """
    # a quick temporary fix to deal with Atom
    args = [(i if not i.startswith('--ignore=') else FIX_IGNORES_RE.sub('', i))
            for i in args if i is not None]

    # if executable looks like a path resolve it
    if not os.path.isfile(flake8_executable) and os.sep in flake8_executable:
        flake8_executable = os.path.abspath(
            os.path.expanduser(os.path.expandvars(flake8_executable))
        )

    log.debug("Calling %s with args: '%s'", flake8_executable, args)
    try:
        cmd = [flake8_executable]
        cmd.extend(args)
        p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)  # pylint: disable=consider-using-with
    except IOError:
        log.debug("Can't execute %s. Trying with 'python -m flake8'", flake8_executable)
        cmd = ['python', '-m', 'flake8']
        cmd.extend(args)
        p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)  # pylint: disable=consider-using-with
    (stdout, stderr) = p.communicate(document.source.encode())
    if stderr:
        log.error("Error while running flake8 '%s'", stderr.decode())
    return stdout.decode()


def build_args(options):
    """Build arguments for calling flake8.

    Args:
        options: dictionary of argument names and their values.
    """
    args = ['-']  # use stdin
    for arg_name, arg_val in options.items():
        if arg_val is None:
            continue
        arg = None
        if isinstance(arg_val, list):
            arg = '--{}={}'.format(arg_name, ','.join(arg_val))
        elif isinstance(arg_val, bool):
            if arg_val:
                arg = '--{}'.format(arg_name)
        else:
            arg = '--{}={}'.format(arg_name, arg_val)
        args.append(arg)
    return args


def parse_stdout(document, stdout):
    """
    Build a diagnostics from flake8's output, it should extract every result and format
    it into a dict that looks like this:
        {
            'source': 'flake8',
            'code': code, # 'E501'
            'range': {
                'start': {
                    'line': start_line,
                    'character': start_column,
                },
                'end': {
                    'line': end_line,
                    'character': end_column,
                },
            },
            'message': msg,
            'severity': lsp.DiagnosticSeverity.*,
        }

    Args:
        document: The document to be linted.
        stdout: output from flake8
    Returns:
        A list of dictionaries.
    """

    diagnostics = []
    lines = stdout.splitlines()
    for raw_line in lines:
        parsed_line = re.match(r'(.*):(\d*):(\d*): (\w*) (.*)', raw_line)
        if not parsed_line:
            log.debug("Flake8 output parser can't parse line '%s'", raw_line)
            continue

        parsed_line = parsed_line.groups()
        if len(parsed_line) != 5:
            log.debug("Flake8 output parser can't parse line '%s'", raw_line)
            continue

        _, line, character, code, msg = parsed_line
        line = int(line) - 1
        character = int(character) - 1
        # show also the code in message
        msg = code + ' ' + msg
        diagnostics.append(
            {
                'source': 'flake8',
                'code': code,
                'range': {
                    'start': {
                        'line': line,
                        'character': character
                    },
                    'end': {
                        'line': line,
                        # no way to determine the column
                        'character': len(document.lines[line])
                    }
                },
                'message': msg,
                'severity': lsp.DiagnosticSeverity.Warning,
            }
        )

    return diagnostics
