#!/usr/bin/env python3

"""The core VM/interpreter model."""

import typing as t

from ichor import isa
from lark import Lark, Token, Transformer, v_args


class Identifier(t.NamedTuple):
    name: str


GRAMMAR = r"""
fun: constraints ";" name ";" arguments ";" ret

arguments: (type ","?)*
ret: (type ","?)*
constraints: (constraint ","?)*
constraint: type

var: constraints ";" name ";" arms
arms: (arm ","?)+
arm: name "(" bindings ")"

bindings: (binding ","?)*
binding: name ":" type

type: NAME
name: NAME
NAME: /[^;,:⊢(){}|]+/
"""


class FuncT(Transformer):
    @v_args(inline=True)
    def fun(self, constraints, name, arguments, ret):
        return (constraints, name, arguments, ret)

    def constraints(self, args):
        return (*args,)

    def arguments(self, args):
        return tuple(args)

    def ret(self, args):
        return tuple(args)

    @v_args(inline=True)
    def NAME(self, name: Token):
        return name.value

    @v_args(inline=True)
    def name(self, name):
        return name

    @v_args(inline=True)
    def type(self, name):
        return name


FUNC = Lark(GRAMMAR, start="fun", parser="lalr", transformer=FuncT())


class FunctionRef(t.NamedTuple):
    name: str

    @classmethod
    def parse(cls, raw: str):
        return cls(raw)


class Function(t.NamedTuple):
    name: str
    arguments: t.List[str]
    returns: t.List[str]
    instructions: t.List[isa.Opcode]
    typevars: t.List[t.Any] = []
    typeconstraints: t.List[t.Any] = []
    metadata: dict = {}

    @classmethod
    def build(cls, name: str, instructions: t.List[isa.Opcode]):
        constraints, name, args, rets = FUNC.parse(name)
        # FIXME: Constraints probably needs some massaging
        # FIXME: Need to get typevars from somewhere
        # They both probably live in the same list
        return cls(name, args, rets, instructions, typeconstraints=constraints)

    @property
    def signature(self):
        # FIXME: This should be more meaningful - likely including a type and code fingerprint
        return f"{self.name};{len(self.arguments)};{len(self.returns)}"


class TypeT(FuncT):
    @v_args(inline=True)
    def var(self, constraints, name, arms):
        return (constraints, name, arms)

    @v_args(inline=True)
    def constraint(self, name):
        return name

    def arms(self, arms):
        return tuple(arms)

    def binding(self, binding):
        return tuple(binding)

    def bindings(self, bindings):
        return tuple(bindings)

    @v_args(inline=True)
    def arm(self, name, bindings):
        return (name, bindings)



TYPE = Lark(GRAMMAR, start="var", parser="lalr", transformer=TypeT())


class TypeRef(t.NamedTuple):
    name: str


class VariantRef(t.NamedTuple):
    type: TypeRef
    arm: str


class Type(t.NamedTuple):
    name: str
    constructors: t.Dict[str, t.List[str]]
    typevars: t.List[t.Any] = []
    typeconstraints: t.List[t.Any] = []
    metadata: dict = {}

    @classmethod
    def build(cls, name: str):
        constraints, name, arms = TYPE.parse(name)
        # FIXME: Constraints probably needs some massaging
        # FIXME: Need to get typevars from somewhere
        # They both probably live in the same list
        return cls(name, dict(arms), typeconstraints=constraints)

    @property
    def signature(self):
        return self.name


class Variant(t.NamedTuple):
    type: str
    variant: str
    fields: t.Tuple


class Closure(t.NamedTuple):
    # Note that a closure over a closure is equivalent to a single closure which extends the captured stack fragment, so
    # there's no need for a union here as we can .interpretery convert nested closures.
    funref: FunctionRef
    frag: t.List[t.Any]


class Struct(t.NamedTuple):
    name: str
    type_params: list
    children: t.Mapping[str, t.Any]


class Module(t.NamedTuple):
    functions: t.Dict[str, Function] = {}
    labels: t.Dict[str, int] = {}
    codepage: list = []
    types: t.Dict[str, Type] = {}
    constants: dict = {}

    def copy(self) -> "Module":
        return Module(
            self.functions.copy(),
            self.labels.copy(),
            self.codepage.copy(),
            self.types.copy(),
            self.constants.copy(),
        )

    @staticmethod
    def translate(start: int, end: int, i: isa.Opcode):
        # FIXME: Consolidate bounds checks somehow
        match i:
            case isa.ATEST(t):
                d = t + start
                assert start <= d < end
                return isa.ATEST(d)

            case isa.GOTO(t):
                d = t + start
                assert start <= d < end
                return isa.GOTO(d)

            case _:
                return i

    def define_function(self, name, opcodes):
        """Enter a function into module.

        Side-effects the codepage and name table.

        """
        func = Function.build(name, opcodes)
        self.functions[func.signature] = func
        self.labels[func.signature] = start = len(self.codepage)
        for op in opcodes:
            self.codepage.append(self.translate(start, start + len(opcodes), op))
        return func.signature

    def define_type(self, name):
        type = Type.build(name)
        self.types[type.signature] = type
        return type.signature

    def __str__(self):
        b = []
        b.append("types:")
        for sig, type in self.types.items():
            b.append(f"  {sig!r}:")
            b.append(f"    name: {type.name}")
            b.append(f"    typeconstraints: {type.typeconstraints}")
            b.append(f"    arms:")
            for arm in type.constructors:
                b.append(f"    - {arm}")

        b.append("functions:")
        for sig, fun in self.functions.items():
            b.append(f"  {sig!r}:")
            b.append(f"    name: {fun.name}")
            b.append(f"    typeconstraints: {fun.typeconstraints}")
            b.append(f"    arguments: {fun.arguments}")
            b.append(f"    returns: {fun.returns}")
            b.append(f"    ip: {self.labels[fun.signature]}")

        b.append("codepage:")
        marks = {v: k for k, v in self.labels.items()}
        for i, o in zip(range(1<<64), self.codepage):
            if(i in marks):
                b.append(f"  {marks[i]!r}:")
            b.append(f"  {i: >10}: {o}")
        return "\n".join(b)


def rotate(l):
    return [l[-1]] + l[:-1]


class Stackframe(object):
    def __init__(self,
                 fun: Function,
                 ip: int,
                 stack: t.Optional[t.List[t.Any]] = None,
                 parent: t.Optional["Stackframe"] = None):
        self._fun = fun
        self._ip = ip
        self._stack: t.List = stack or []
        self._parent = parent

    def push(self, obj):
        self._stack.append(obj)

    def pop(self):
        return self._stack.pop()

    def dup(self, nargs):
        self._stack.extend(self._stack[-nargs:])

    def drop(self, nargs):
        self._stack = self._stack[:-nargs]

    def rot(self, nargs):
        frag = self._stack[-nargs:]
        base = self._stack[:-nargs]
        rotated = rotate(frag)
        self._stack = base + rotated

    def slot(self, n):
        self.push(self._stack[n])

    def call(self, fun: Function, ip) -> "Stackframe":
        assert isinstance(fun, Function)
        assert isinstance(ip, int)
        self._ip += 1
        nargs = len(fun.arguments)
        args, self._stack = self._stack[:nargs], self._stack[nargs:]
        return Stackframe(
            fun,
            ip,
            stack=args,
            parent=self,
        )

    def ret(self, nargs) -> "Stackframe":
        assert nargs >= 0
        assert isinstance(self._parent, Stackframe)
        self._parent._stack.extend(self._stack[:nargs])
        return self._parent

    def goto(self, target: int):
        self._ip = target

    @property
    def depth(self):
        if self._parent == None:
            return 0
        else:
            return self._parent.depth + 1

    def __getitem__(self, key):
        return self._stack.__getitem__(key)

    def __len__(self):
        return len(self._stack)

    def __iter__(self):
        return iter(self._stack)
