from typing import Dict, Optional, Tuple, List, Any

class MaMa:
    def __init__(self, prog: Dict[int, str] | List[str], stack: Dict[int, int] | List[int] | None = None) -> None:
        self.step_counter = 0
        self.halted = False

        # Init prog from list or dict
        self.p_prog = 0
        if isinstance(prog, list):
            self.prog = {i: micro for i, micro in enumerate(prog)}
        else:
            self.prog = prog

        # Default stack
        self.p_stack = -1
        self.stack: Dict[int, int] = {}

        # Init custom stack from list or dict
        if stack is not None:
            if isinstance(stack, list):
                self.stack = {i: v for i, v in enumerate(stack)}
            else:
                self.stack = dict(stack)
            self.p_stack = max(self.stack.keys(), default=-1)
        self.initial_stack = dict(self.stack)

    # Runs the machine and returns execution journal
    def run(self, max_steps: int = 1000) -> List[Dict[str, int | str | dict | list]]:
        steps = 0
        # always insert init configuration as step 0
        journal = [self.config("init")]
        while not self.halted and steps < max_steps:
            journal.append(self.__step())
            steps += 1
        return journal

    # Decodes string MaMa instructions to function callables
    @staticmethod
    def decode(micro: str) -> Tuple[str, Optional[List[Any]]]:
        if "(" in micro:
            name, rest = micro.split("(", 1)
            args_str = rest[:-1].strip()
            if args_str == "":
                return name, []
            args = []
            for a in args_str.split(","):
                a = a.strip()
                try:
                    a = int(a)
                except ValueError:
                    pass  # keep symbolic argument (e.g., 'n')
                args.append(a)
            return name, args
        return micro, None

    # Generates journal entry for a step
    def config(self, micro: str) -> Dict[str, int | str | dict | list]:
        return {
            "step": self.step_counter,
            "micro": micro,
            "p_prog": self.p_prog,
            "p_stack": self.p_stack,
            "stack": dict(self.stack),
        }

    # Executes one step of the machine
    def __step(self) -> Dict[str, int | str | dict | list]:
        if self.halted or self.p_prog not in self.prog:
            self.halted = True
            return self.config("halted")

        micro = self.prog[self.p_prog]
        name, args = MaMa.decode(micro)
        method = getattr(self, f"_{name}", None)
        if method is None:
            raise ValueError(f"Unknown instruction: {micro}")
        method(args)
        self.step_counter += 1
        return self.config(micro)

    # DEFINE MaMa Micros

    # Stop execution
    def _stop(self, _: Optional[int]) -> None:
        self.halted = True

    # Remove top element from stack
    def _pop(self, _: Optional[int]) -> None:
        self.p_stack -= 1
        self.p_prog += 1

    # Load stack pointer value onto stack
    def _ldsp(self, _: Optional[int]) -> None:
        self.p_stack += 1
        self.stack[self.p_stack] = self.p_stack
        self.p_prog += 1

    # Push constant n onto stack
    def _push(self, n: Optional[List[int]]) -> None:
        assert n is not None and len(n) == 1
        val = n[0]
        self.p_stack += 1
        self.stack[self.p_stack] = val
        self.p_prog += 1

    # Add top two stack elements
    def _add(self, _: Optional[int]) -> None:
        self.stack[self.p_stack - 1] += self.stack[self.p_stack]
        self.p_stack -= 1
        self.p_prog += 1

    # Subtract top element from second-top element
    def _sub(self, _: Optional[int]) -> None:
        self.stack[self.p_stack - 1] -= self.stack[self.p_stack]
        self.p_stack -= 1
        self.p_prog += 1

    # Multiply top two stack elements
    def _mult(self, _: Optional[int]) -> None:
        self.stack[self.p_stack - 1] *= self.stack[self.p_stack]
        self.p_stack -= 1
        self.p_prog += 1

    # Integer division of second-top by top stack element
    def _div(self, _: Optional[int]) -> None:
        self.stack[self.p_stack - 1] //= self.stack[self.p_stack]
        self.p_stack -= 1
        self.p_prog += 1

    # Load value from relative address n in stack
    def _ldo(self, n: Optional[List[int]]) -> None:
        assert n is not None and len(n) == 1
        offset = n[0]
        old_sp = self.p_stack
        self.p_stack += 1
        self.stack[self.p_stack] = self.stack[old_sp + offset]
        self.p_prog += 1

    # Store top value to relative address n in stack
    def _sto(self, n: Optional[List[int]]) -> None:
        assert n is not None and len(n) == 1
        offset = n[0]
        self.stack[self.p_stack + offset] = self.stack[self.p_stack]
        self.p_stack -= 1
        self.p_prog += 1

    # Unconditional jump to address a
    def _ujp(self, a: Optional[List[int]]) -> None:
        assert a is not None and len(a) == 1
        self.p_prog = a[0]

    # Conditional jump if top two elements are equal
    def _equal(self, a: Optional[List[int]]) -> None:
        assert a is not None and len(a) == 1
        addr = a[0]
        if self.stack[self.p_stack - 1] == self.stack[self.p_stack]:
            self.p_stack -= 2
            self.p_prog = addr
        else:
            self.p_stack -= 2
            self.p_prog += 1

    # Conditional jump if second-top ≤ top element
    def _leq(self, a: Optional[List[int]]) -> None:
        assert a is not None and len(a) == 1
        addr = a[0]
        if self.stack[self.p_stack - 1] <= self.stack[self.p_stack]:
            self.p_stack -= 2
            self.p_prog = addr
        else:
            self.p_stack -= 2
            self.p_prog += 1
