from lark.exceptions import UnexpectedToken


class _ValidationError(Exception):
    def __init__(self, *args, **kwargs):
        if self.__class__ is _ValidationError:
            raise TypeError("Do not raise _ValidationError directly.")
        super().__init__(*args, **kwargs)


class ErrorCollector:
    def __init__(self):
        self.errors = []

    def add(self, error):
        self.errors.append(error)

    def raise_if_any(self):
        if self.errors:
            raise CollectedValidationErrors(self.errors)


class CollectedValidationErrors(_ValidationError):
    def __init__(self, errors):
        self.errors = errors

    def asdict(self, with_message=True):
        return [e.asdict(with_message=with_message) for e in self.errors]

    def __str__(self):
        return f"{len(self.errors)} validation error(s) collected:\n" + "\n\n".join(
            str(e) for e in self.errors
        )


class SyntaxError(_ValidationError):
    def __init__(self, filecontent, exception):
        self.filecontent = filecontent
        self.exception = exception

    def asdict(self, with_message=True):
        return {
            "type": (
                "unexpected_token"
                if isinstance(self.exception, UnexpectedToken)
                else "unexpected_character"
            ),
            "lineno": self.exception.line,
            "column": self.exception.column,
            "found_type": self.exception.token.type.lower(),
            "found_value": self.exception.token.value,
            "expected": sorted(x for x in self.exception.accepts if "__ANON" not in x),
            "line": self.filecontent.split("\n")[self.exception.line - 1],
            **({"message": str(self)} if with_message else {}),
        }

    def __str__(self):
        d = self.asdict(with_message=False)
        if len(d["expected"]) == 1:
            exp = d["expected"][0]
        else:
            exp = f"one of {' '.join(d['expected'])}"

        sth = "character" if d["type"] == "unexpected_character" else ""

        return f"On line {d['lineno']} column {d['column']}:\nUnexpected {sth}{d['found_type']} ('{d['found_value']}')\nExpecting {exp}\n{d['lineno']:05d} | {d['line']}\n        {' ' * (self.exception.column - 1)}^"


class DuplicateNameError(_ValidationError):
    def __init__(self, filecontent, name, linenumbers):
        self.name = name
        self.filecontent = filecontent
        self.linenumbers = linenumbers

    def asdict(self, with_message=True):
        return {
            "type": "duplicate_name",
            "name": self.name,
            "lineno": self.linenumbers[0],
            "line": self.filecontent.split("\n")[self.linenumbers[0] - 1],
            **({"message": str(self)} if with_message else {}),
        }

    def __str__(self):
        d = self.asdict(with_message=False)

        def build():
            yield f"On line {d['lineno']}:\nDuplicate instance name #{d['name']}"
            yield f"{d['lineno']:05d} | {d['line']}"
            yield " " * 8 + "^" * len(d["line"].rstrip())

        return "\n".join(build())


class HeaderFieldError(_ValidationError):
    def __init__(self, field, found_len, expected_len):
        self.field = field
        self.found_len = found_len
        self.expected_len = expected_len

    def asdict(self, with_message=True):
        return {
            "type": "invalid_header_field",
            "field": self.field,
            "expected_field_count": self.expected_len,
            "actual_field_count": self.found_len,
            **({"message": str(self)} if with_message else {}),
        }

    def __str__(self):
        return (
            f"Invalid number of parameters for HEADER field '{self.field}'. "
            f"Expected {self.expected_len}, found {self.found_len}."
        )


class InvalidNameError(_ValidationError):
    def __init__(self, filecontent, name, linenumbers):
        self.name = name
        self.filecontent = filecontent
        self.linenumbers = linenumbers

    def asdict(self, with_message=True):
        return {
            "type": "invalid_name",
            "name": self.name,
            "lineno": self.linenumbers[0],
            "line": self.filecontent.split("\n")[self.linenumbers[0] - 1],
            **({"message": str(self)} if with_message else {}),
        }

    def __str__(self):
        d = self.asdict(with_message=False)

        def build():
            yield f"On line {d['lineno']}:\nInvalid instance name #{d['name']}"
            yield f"{d['lineno']:05d} | {d['line']}"
            yield " " * 8 + "^" * len(d["line"].rstrip())

        return "\n".join(build())
