#!/usr/bin/env python3

"""The.interpreterementation of Shogoth's lexical analyzer."""

from abc import ABC
from dataclasses import dataclass
import typing as t
from uuid import UUID

from shoggoth.types import Keyword, List, Symbol


class Expr(ABC):
    pass


@dataclass
class ConstExpr(Expr):
    val: t.Any


@dataclass
class ListExpr(Expr):
    children: t.List[t.Any]


@dataclass
class MapExpr(Expr):
    kvs: t.List[t.Tuple[t.Any, t.Any]]


@dataclass
class InvokeExpr(Expr):
    target: Expr
    args: t.List[Expr]
    # FIXME: kwargs/star-args?


@dataclass
class IfExpr(Expr):
    """(if* test then else)"""
    test: Expr
    pos_branch: Expr
    neg_branch: Expr


@dataclass
class LetExpr(Expr):
    """(let* [name binding name2 binding2] body)"""
    bindings: t.List[t.Tuple[Symbol, Expr]]
    body: Expr


@dataclass
class FnExpr(Expr):
    """(fn* args body)"""
    args: t.List[Symbol]
    body: Expr


# Related reading -
# - http://www-formal.stanford.edu/jmc/elephant/elephant.html
# - https://www.cs.utah.edu/plt/scope-sets/
#
# FIXME: Do I make the multiverse directly durable? Or does that live elsewhere?
@dataclass
class Multiverse:
    """A namespace with branches.

    We can think about a universe or multiverse of program states as existing within histories of names and evaluations.

    When we evaluate a form, four things can happen:
    - We can do program effects (I/O, generate heat, consume time)
    - Produce a value (returned or raised/thrown error)
    - Produce definition(s) with which other evaluation will occur
    - Produce context with which other evaluation will occur

    In a sense, there exists a 'multiverse' of naming into which all top level values are entered.

    Each alteration to the 'multiverse' produces a new 'universe' of names - you could call this a fork or session -
    which maps a name in that universe to a value in the multiverse.

    A universe is a sequential or causal history.
    Traveling backwards in time and deviating produces a different universe.
    Truncating or discarding that history produces a different although in some sense equivalent universe.

    """

    @dataclass
    class Namespace:
        pass

    @dataclass
    class Entry:
        ns: "Namespace"
        expr: t.Any
        value: t.Any

    soup: t.Dict[UUID, Entry] = {}




BOOTSTRAP = "lang.shoggoth.v0.bootstrap"
SPECIALS = Multiverse.Namespace(Symbol(BOOTSTRAP), {
    Symbol("if*"): None, # FIXME: This isn't really if anymore, it's MATCH or TEST. Something. Unless bool is in bootstrap.
    Symbol("let*"): None,
    Symbol("fn*"): None,
})


GLOBALS = Namespace(Symbol("lang.shoggoth.v0.core"), {
})


# FIXME: This is really expand... isn't it.
class Analyzer:
    def __init__(self, specials, globals):
        self._specials = specials
        self._globals = globals

    def resolve(self, module, name: Symbol):
        return module.resolve(name) or self._globals.resolve(name) or self._specials.resolve(name)

    def macroexpand1(self, module, form):
        """macroexpand¹"""

    def macroexpand(self, module, form):
        """Perform macroexpansion."""

    def analyze(self, module: Namespace, expr: t.Any):
        """Perform analysis, of which macroexpansion is part."""

        def _analyze(e):
            return self.analyze(module, e)

        match expr:
            case int() | float() | str() | Keyword():
                return ConstExpr(expr)

            case Symbol():
                return ConstExpr(expr)

            case List() if len(expr) > 1 and isinstance(expr[0], Symbol):
                expr = self.macroexpand(module, expr)
                if (target := self.resolve(module, expr[0])):
                    match target:
                        # FIXME: Macros go here? Or is macroexpansion separate?
                        # Macroexpansion needs to go here

                        # in-ns or equivalent needs to go here
                        # ns / module needs to go here

                        case Symbol("if*", BOOTSTRAP):
                            assert len(expr) == 4
                            return IfExpr(_analyze(expr[1]), _analyze(expr[2]), _analyze(expr[3]))

                        case Symbol("let*", BOOTSTRAP):
                            assert len(expr) == 3
                            return LetExpr([(name, _analyze(e)) for name, e in expr[1]], _analyze(expr[2]))

                        case Symbol("fn*", BOOTSTRAP):
                            assert len(expr) == 3
                            return FnExpr(expr[1], _analyze(expr[2]))

                        case _:
                            pass

                return InvokeExpr(_analyze(target), [_analyze(e) for e in expr[1:]])

            case List():
                return ListExpr([self.analyze(module, e) for e in expr])

        raise ValueError(f"Unable to analyze {expr!r} ({type(expr)})")
