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

import logging

import pycodestyle

from pylsp import hookimpl, lsp
from pylsp._utils import get_eol_chars

try:
    from autopep8 import continued_indentation as autopep8_c_i
except ImportError:
    pass
else:
    # Check if autopep8's continued_indentation implementation
    # is overriding pycodestyle's and if so, re-register
    # the check using pycodestyle's implementation as expected
    if autopep8_c_i in pycodestyle._checks["logical_line"]:
        del pycodestyle._checks["logical_line"][autopep8_c_i]
        pycodestyle.register_check(pycodestyle.continued_indentation)

log = logging.getLogger(__name__)


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

        opts = {
            "exclude": settings.get("exclude"),
            "filename": settings.get("filename"),
            "hang_closing": settings.get("hangClosing"),
            "ignore": settings.get("ignore"),
            "max_line_length": settings.get("maxLineLength"),
            "indent_size": settings.get("indentSize"),
            "select": settings.get("select"),
        }
        kwargs = {k: v for k, v in opts.items() if v}
        styleguide = pycodestyle.StyleGuide(kwargs)

        # Use LF to lint file because other line endings can give false positives.
        # See spyder-ide/spyder#19565 for context.
        source = document.source
        eol_chars = get_eol_chars(source)
        if eol_chars in ["\r", "\r\n"]:
            source = source.replace(eol_chars, "\n")
            lines = source.splitlines(keepends=True)
        else:
            lines = document.lines

        c = pycodestyle.Checker(
            filename=document.path,
            lines=lines,
            options=styleguide.options,
            report=PyCodeStyleDiagnosticReport(styleguide.options),
        )
        c.check_all()
        diagnostics = c.report.diagnostics

        return diagnostics


class PyCodeStyleDiagnosticReport(pycodestyle.BaseReport):
    def __init__(self, options) -> None:
        self.diagnostics = []
        super().__init__(options=options)

    def error(self, line_number, offset, text, check):
        code = text[:4]
        if self._ignore_code(code):
            return

        # Don't care about expected errors or warnings
        if code in self.expected:
            return

        # PyCodeStyle will sometimes give you an error the line after the end of the file
        #   e.g. no newline at end of file
        # In that case, the end offset should just be some number ~100
        # (because why not? There's nothing to underline anyways)
        err_range = {
            "start": {"line": line_number - 1, "character": offset},
            "end": {
                # FIXME: It's a little naiive to mark until the end of the line, can we not easily do better?
                "line": line_number - 1,
                "character": 100
                if line_number > len(self.lines)
                else len(self.lines[line_number - 1]),
            },
        }
        diagnostic = {
            "source": "pycodestyle",
            "range": err_range,
            "message": text,
            "code": code,
            # Are style errors really ever errors?
            "severity": _get_severity(code),
        }
        if code.startswith("W6"):
            diagnostic["tags"] = [lsp.DiagnosticTag.Deprecated]
        self.diagnostics.append(diagnostic)


def _get_severity(code):
    # Are style errors ever really errors?
    if code[0] == "E" or code[0] == "W":
        return lsp.DiagnosticSeverity.Warning
    # If no severity is specified, why wouldn't this be informational only?
    return lsp.DiagnosticSeverity.Information
