# Copyright (c) 2017-2026 Juancarlo Añez (apalala@gmail.com)
# SPDX-License-Identifier: BSD-4-Clause
from __future__ import annotations

import dataclasses as dc
import json
import weakref
from collections.abc import Iterable, Mapping, MutableMapping
from typing import Any, Self

from ..util.fromjson import fromjson
from .basenode import BaseNode, nodedataclass


__all__ = ['Node', 'nodedataclass']


_children_cache: MutableMapping[Node, tuple[Node, ...]] = weakref.WeakKeyDictionary()


@nodedataclass
class Node(BaseNode):
    _parent_ref: weakref.ref[Node] | None = dc.field(init=False, default=None)

    def __init__(self, ast: Any = None, **kwargs: Any):
        super().__init__(ast=ast, **kwargs)
        self._parent_ref = None

    @property
    def parent(self) -> Node | None:
        ref = self._parent_ref
        if ref is not None:
            # noinspection PyCallingNonCallable
            return ref()
        else:
            return None

    @property
    def text(self) -> str | None:
        pi = self.parseinfo
        if pi and hasattr(pi.cursor, "text"):
            return pi.cursor.textstr[pi.pos : pi.endpos]
        return None

    @property
    def line(self) -> int | None:
        return self.parseinfo.line if self.parseinfo else None

    @property
    def path(self) -> tuple[Node, ...]:
        ancestors: list[Node] = [self]
        parent = self.parent
        while parent is not None:
            ancestors.append(parent)
            parent = parent.parent
        return tuple(reversed(ancestors))

    @classmethod
    def load(cls, data: Any) -> Self:
        new = fromjson(data)
        assert isinstance(new, cls)
        return new

    @classmethod
    def loads(cls, data: str) -> Self:
        return cls.load(json.loads(data))

    def children(self) -> tuple[Node, ...]:
        return self._cached_children()

    def children_list(self) -> list[Node]:
        return list(self._cached_children())

    def _cached_children(self) -> tuple[Node, ...]:
        def dfs(obj: Any) -> Iterable[Node]:
            match obj:
                case Node() as node:
                    node._parent_ref = weakref.ref(self)
                    yield node
                case Mapping() as mapping:
                    for name, value in mapping.items():
                        if name.startswith('_'):
                            continue
                        if value is None:
                            continue
                        yield from dfs(value)
                case bytes() | str():
                    pass
                case Iterable() as seq:
                    for item in seq:
                        yield from dfs(item)
                case _:
                    pass

        if self not in _children_cache:
            _children_cache[self] = tuple(dfs(self.__pub__()))
        return _children_cache[self]
