from __future__ import annotations

import pathlib
import typing as ty
from typing import Optional, Union

from ..._vendor import flexcache as fc
from ..._vendor import flexparser as fp
from ..base_defparser import ParserConfig
from . import block, common, context, defaults, group, plain, system


class PintRootBlock(
    fp.RootBlock[
        ty.Union[
            plain.CommentDefinition,
            common.ImportDefinition,
            context.ContextDefinition,
            defaults.DefaultsDefinition,
            system.SystemDefinition,
            group.GroupDefinition,
            plain.AliasDefinition,
            plain.DerivedDimensionDefinition,
            plain.DimensionDefinition,
            plain.PrefixDefinition,
            plain.UnitDefinition,
        ],
        ParserConfig,
    ]
):
    body: fp.Multi[
        ty.Union[
            plain.CommentDefinition,
            common.ImportDefinition,
            context.ContextDefinition,
            defaults.DefaultsDefinition,
            system.SystemDefinition,
            group.GroupDefinition,
            plain.AliasDefinition,
            plain.DerivedDimensionDefinition,
            plain.DimensionDefinition,
            plain.PrefixDefinition,
            plain.UnitDefinition,
        ]
    ]


class PintSource(fp.ParsedSource[PintRootBlock, ParserConfig]):
    """Source code in Pint."""


class HashTuple(tuple):
    pass


class _PintParser(fp.Parser[PintRootBlock, ParserConfig]):
    """Parser for the original Pint definition file, with cache."""

    _delimiters = {
        "#": (
            fp.DelimiterInclude.SPLIT_BEFORE,
            fp.DelimiterAction.CAPTURE_NEXT_TIL_EOL,
        ),
        **fp.SPLIT_EOL,
    }
    _root_block_class = PintRootBlock
    _strip_spaces = True

    _diskcache: fc.DiskCache

    def __init__(self, config: ParserConfig, *args, **kwargs):
        self._diskcache = kwargs.pop("diskcache", None)
        super().__init__(config, *args, **kwargs)

    def parse_file(self, path: pathlib.Path) -> PintSource:
        if self._diskcache is None:
            return super().parse_file(path)
        content, basename = self._diskcache.load(path, super().parse_file)
        return content


class DefParser:
    skip_classes: tuple[type, ...] = (
        fp.BOF,
        fp.BOR,
        fp.BOS,
        fp.EOS,
        plain.CommentDefinition,
    )

    def __init__(self, default_config, diskcache):
        self._default_config = default_config
        self._diskcache = diskcache

    def iter_parsed_project(self, parsed_project: fp.ParsedProject):
        last_location = None
        for stmt in parsed_project.iter_blocks():
            if isinstance(stmt, fp.BOF):
                last_location = str(stmt.path)
            elif isinstance(stmt, fp.BOR):
                last_location = (
                    f"[package: {stmt.package}, resource: {stmt.resource_name}]"
                )

            if isinstance(stmt, self.skip_classes):
                continue

            if isinstance(stmt, common.DefinitionSyntaxError):
                # TODO: check why this assert fails
                # assert isinstance(last_location, str)
                stmt.set_location(last_location)
                raise stmt
            elif isinstance(stmt, block.DirectiveBlock):
                for exc in stmt.errors:
                    exc = common.DefinitionSyntaxError(str(exc))
                    exc.set_position(*stmt.get_position())
                    exc.set_raw(
                        (stmt.opening.raw or "") + " [...] " + (stmt.closing.raw or "")
                    )
                    exc.set_location(last_location)
                    raise exc

                try:
                    yield stmt.derive_definition()
                except Exception as exc:
                    exc = common.DefinitionSyntaxError(str(exc))
                    exc.set_position(*stmt.get_position())
                    exc.set_raw(stmt.opening.raw + " [...] " + stmt.closing.raw)
                    exc.set_location(last_location)
                    raise exc
            else:
                yield stmt

    def parse_file(
        self, filename: Union[pathlib.Path, str], cfg: Optional[ParserConfig] = None
    ):
        return fp.parse(
            filename,
            _PintParser,
            cfg or self._default_config,
            diskcache=self._diskcache,
            strip_spaces=True,
            delimiters=_PintParser._delimiters,
        )

    def parse_string(self, content: str, cfg: Optional[ParserConfig] = None):
        return fp.parse_bytes(
            content.encode("utf-8"),
            _PintParser,
            cfg or self._default_config,
            diskcache=self._diskcache,
            strip_spaces=True,
            delimiters=_PintParser._delimiters,
        )
