#!/usr/bin/env python3
"""
╔══════════════════════════════════════════════════════════════╗
║              S N I P P E T   L A N G U A G E                ║
║                     Interpreter v1.0                         ║
╠══════════════════════════════════════════════════════════════╣
║  Syntax:                                                     ║
║    snippet <name>                                            ║
║        set <var> = <number>    # assign a variable           ║
║        increase <var>          # var += 1                    ║
║        decrease <var>          # var -= 1                    ║
║        print <var>             # print variable value        ║
║        if <var> > <number>     # conditional (> or <)        ║
║            ...                                               ║
║            else                                              ║
║                ...                                           ║
║                                                              ║
║  Rules:                                                      ║
║    • Snippets run top-to-bottom and share all variables      ║
║    • Indentation defines scope (spaces or tabs, be consistent)
║    • No loops                                                ║
║    • Numbers only (integers and decimals)                    ║
║    • Lines starting with # are comments                      ║
╚══════════════════════════════════════════════════════════════╝

Usage:
    python interpreter.py program.snip    # run a file
    python interpreter.py                 # REPL (blank line to run)
"""

import sys
import re
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple


# ══════════════════════════════════════════════════════════════
#  TOKEN TYPES
# ══════════════════════════════════════════════════════════════

class TT:
    SNIPPET  = 'SNIPPET'
    IF       = 'IF'
    ELSE     = 'ELSE'
    INCREASE = 'INCREASE'
    DECREASE = 'DECREASE'
    SET      = 'SET'
    PRINT    = 'PRINT'
    NAME     = 'NAME'
    NUMBER   = 'NUMBER'
    GT       = 'GT'
    LT       = 'LT'
    EQ       = 'EQ'

KEYWORDS: Dict[str, str] = {
    'snippet':  TT.SNIPPET,
    'if':       TT.IF,
    'else':     TT.ELSE,
    'increase': TT.INCREASE,
    'decrease': TT.DECREASE,
    'set':      TT.SET,
    'print':    TT.PRINT,
}


@dataclass
class Token:
    type:  str
    value: Any
    line:  int

    def __repr__(self) -> str:
        return f"Token({self.type}, {self.value!r}, line={self.line})"


# ══════════════════════════════════════════════════════════════
#  LEXER  →  list of (indent, [Token …], line_num)
# ══════════════════════════════════════════════════════════════

class LexerError(Exception):
    pass


class Lexer:
    def __init__(self, source: str):
        self.source = source

    def tokenize(self) -> List[Tuple[int, List[Token], int]]:
        result: List[Tuple[int, List[Token], int]] = []
        for line_num, raw in enumerate(self.source.splitlines(), 1):
            stripped = raw.rstrip()
            if not stripped or stripped.lstrip().startswith('#'):
                continue
            indent = len(raw) - len(raw.lstrip(' \t'))
            tokens = self._lex_words(stripped.lstrip().split(), line_num)
            if tokens:
                result.append((indent, tokens, line_num))
        return result

    def _lex_words(self, words: List[str], line_num: int) -> List[Token]:
        tokens: List[Token] = []
        for word in words:
            if word in KEYWORDS:
                tokens.append(Token(KEYWORDS[word], word, line_num))
            elif re.fullmatch(r'-?\d+(\.\d+)?', word):
                tokens.append(Token(TT.NUMBER, float(word), line_num))
            elif word == '>':
                tokens.append(Token(TT.GT, '>', line_num))
            elif word == '<':
                tokens.append(Token(TT.LT, '<', line_num))
            elif word == '=':
                tokens.append(Token(TT.EQ, '=', line_num))
            elif re.fullmatch(r'[a-zA-Z_]\w*', word):
                tokens.append(Token(TT.NAME, word, line_num))
            else:
                raise LexerError(f"[Line {line_num}] Unknown token: {word!r}")
        return tokens


# ══════════════════════════════════════════════════════════════
#  AST  NODES
# ══════════════════════════════════════════════════════════════

@dataclass
class ProgramNode:
    snippets: List

@dataclass
class SnippetNode:
    name: str
    body: List
    line: int

@dataclass
class ConditionNode:
    left:  str    # variable name
    op:    str    # '>' or '<'
    right: float  # literal number
    line:  int

@dataclass
class IfNode:
    condition: ConditionNode
    then_body: List
    else_body: List
    line: int

@dataclass
class IncreaseNode:
    name: str
    line: int

@dataclass
class DecreaseNode:
    name: str
    line: int

@dataclass
class SetNode:
    name:  str
    value: float
    line:  int

@dataclass
class PrintNode:
    name: str
    line: int


# ══════════════════════════════════════════════════════════════
#  PARSER
# ══════════════════════════════════════════════════════════════

class ParseError(Exception):
    pass


# Shorthand type for a lexed line
LexLine = Tuple[int, List[Token], int]


class Parser:
    def __init__(self, lines: List[LexLine]):
        self.lines = lines
        self.pos   = 0

    # ── low-level helpers ─────────────────────────────────────

    def _peek(self) -> Optional[LexLine]:
        return self.lines[self.pos] if self.pos < len(self.lines) else None

    def _consume(self) -> LexLine:
        line = self.lines[self.pos]
        self.pos += 1
        return line

    # ── top level ─────────────────────────────────────────────

    def parse(self) -> ProgramNode:
        snippets = []
        while self._peek():
            indent, tokens, line_num = self._peek()
            if tokens[0].type != TT.SNIPPET:
                raise ParseError(
                    f"[Line {line_num}] Expected 'snippet', got {tokens[0].value!r}"
                )
            snippets.append(self._parse_snippet())
        return ProgramNode(snippets)

    # ── snippet ───────────────────────────────────────────────

    def _parse_snippet(self) -> SnippetNode:
        indent, tokens, line_num = self._consume()
        if len(tokens) < 2 or tokens[1].type != TT.NAME:
            raise ParseError(f"[Line {line_num}] Expected a name after 'snippet'")
        name = tokens[1].value
        body = self._parse_block(parent_indent=indent)
        if not body:
            raise ParseError(f"[Line {line_num}] Snippet '{name}' has an empty body")
        return SnippetNode(name, body, line_num)

    # ── block ─────────────────────────────────────────────────

    def _parse_block(self, parent_indent: int) -> List:
        """
        Collect statements whose indentation is strictly greater than
        parent_indent and all equal to the first line's indentation.
        """
        nxt = self._peek()
        if nxt is None or nxt[0] <= parent_indent:
            return []

        block_indent = nxt[0]
        stmts = []

        while True:
            nxt = self._peek()
            if nxt is None or nxt[0] < block_indent:
                break
            indent, tokens, line_num = nxt
            if indent > block_indent:
                raise ParseError(
                    f"[Line {line_num}] Unexpected indentation "
                    f"(expected {block_indent}, got {indent})"
                )
            if tokens[0].type == TT.ELSE:
                break  # parent if-parser will handle this
            stmts.append(self._parse_statement(block_indent))

        return stmts

    # ── statements ────────────────────────────────────────────

    def _parse_statement(self, block_indent: int):
        indent, tokens, line_num = self._peek()
        tt = tokens[0].type
        dispatch = {
            TT.IF:       self._parse_if,
            TT.INCREASE: self._parse_increase,
            TT.DECREASE: self._parse_decrease,
            TT.SET:      self._parse_set,
            TT.PRINT:    self._parse_print,
        }
        if tt not in dispatch:
            raise ParseError(
                f"[Line {line_num}] Unexpected keyword: {tokens[0].value!r}"
            )
        return dispatch[tt](block_indent) if tt == TT.IF else dispatch[tt]()

    def _parse_if(self, current_indent: int) -> IfNode:
        indent, tokens, line_num = self._consume()
        # Expected shape:  IF  NAME  (GT|LT)  NUMBER
        if len(tokens) < 4:
            raise ParseError(f"[Line {line_num}] Incomplete 'if' condition")
        if tokens[1].type != TT.NAME:
            raise ParseError(f"[Line {line_num}] Expected variable name after 'if'")
        if tokens[2].type not in (TT.GT, TT.LT):
            raise ParseError(f"[Line {line_num}] Expected '>' or '<' in condition")
        if tokens[3].type != TT.NUMBER:
            raise ParseError(f"[Line {line_num}] Expected a number in condition")

        cond = ConditionNode(
            left  = tokens[1].value,
            op    = tokens[2].value,
            right = tokens[3].value,
            line  = line_num,
        )
        then_body = self._parse_block(parent_indent=indent)

        # Optional else at the SAME indent level as the if
        else_body: List = []
        nxt = self._peek()
        if nxt and nxt[0] == indent and nxt[1][0].type == TT.ELSE:
            self._consume()  # eat 'else'
            else_body = self._parse_block(parent_indent=indent)

        return IfNode(cond, then_body, else_body, line_num)

    def _parse_increase(self) -> IncreaseNode:
        indent, tokens, line_num = self._consume()
        if len(tokens) < 2 or tokens[1].type != TT.NAME:
            raise ParseError(f"[Line {line_num}] Expected variable name after 'increase'")
        return IncreaseNode(tokens[1].value, line_num)

    def _parse_decrease(self) -> DecreaseNode:
        indent, tokens, line_num = self._consume()
        if len(tokens) < 2 or tokens[1].type != TT.NAME:
            raise ParseError(f"[Line {line_num}] Expected variable name after 'decrease'")
        return DecreaseNode(tokens[1].value, line_num)

    def _parse_set(self) -> SetNode:
        indent, tokens, line_num = self._consume()
        # set  NAME  =  NUMBER
        if len(tokens) < 4:
            raise ParseError(f"[Line {line_num}] Syntax: set <var> = <number>")
        if tokens[1].type != TT.NAME:
            raise ParseError(f"[Line {line_num}] Expected variable name after 'set'")
        if tokens[2].type != TT.EQ:
            raise ParseError(f"[Line {line_num}] Expected '=' after variable name")
        if tokens[3].type != TT.NUMBER:
            raise ParseError(f"[Line {line_num}] Expected a number after '='")
        return SetNode(tokens[1].value, tokens[3].value, line_num)

    def _parse_print(self) -> PrintNode:
        indent, tokens, line_num = self._consume()
        if len(tokens) < 2 or tokens[1].type != TT.NAME:
            raise ParseError(f"[Line {line_num}] Expected variable name after 'print'")
        return PrintNode(tokens[1].value, line_num)


# ══════════════════════════════════════════════════════════════
#  INTERPRETER  (tree-walk evaluator)
# ══════════════════════════════════════════════════════════════

class SnippetRuntimeError(Exception):
    pass


class Interpreter:
    def __init__(self):
        # All snippets share this variable store
        self.variables: Dict[str, float] = {}

    def run(self, program: ProgramNode) -> None:
        for snippet in program.snippets:
            self._exec_snippet(snippet)

    # ── snippet ───────────────────────────────────────────────

    def _exec_snippet(self, node: SnippetNode) -> None:
        print(f"── snippet {node.name} ──")
        self._exec_block(node.body)

    # ── block / dispatch ──────────────────────────────────────

    def _exec_block(self, stmts: List) -> None:
        for stmt in stmts:
            self._exec(stmt)

    def _exec(self, node) -> None:
        dispatch = {
            IfNode:       self._exec_if,
            IncreaseNode: self._exec_increase,
            DecreaseNode: self._exec_decrease,
            SetNode:      self._exec_set,
            PrintNode:    self._exec_print,
        }
        handler = dispatch.get(type(node))
        if handler is None:
            raise SnippetRuntimeError(f"Unknown AST node: {type(node).__name__}")
        handler(node)

    # ── statements ────────────────────────────────────────────

    def _exec_if(self, node: IfNode) -> None:
        if self._eval_condition(node.condition):
            self._exec_block(node.then_body)
        else:
            self._exec_block(node.else_body)

    def _eval_condition(self, cond: ConditionNode) -> bool:
        val = self._get_var(cond.left, cond.line)
        if cond.op == '>':
            return val > cond.right
        if cond.op == '<':
            return val < cond.right
        raise SnippetRuntimeError(f"Unknown operator: {cond.op!r}")

    def _exec_increase(self, node: IncreaseNode) -> None:
        self.variables[node.name] = self._get_var(node.name, node.line) + 1

    def _exec_decrease(self, node: DecreaseNode) -> None:
        self.variables[node.name] = self._get_var(node.name, node.line) - 1

    def _exec_set(self, node: SetNode) -> None:
        self.variables[node.name] = node.value

    def _exec_print(self, node: PrintNode) -> None:
        val = self._get_var(node.name, node.line)
        # Show as int when the value is a whole number
        display = int(val) if val == int(val) else val
        print(f"  {node.name} = {display}")

    # ── helpers ───────────────────────────────────────────────

    def _get_var(self, name: str, line: int) -> float:
        if name not in self.variables:
            raise SnippetRuntimeError(
                f"[Line {line}] Undefined variable: '{name}'"
            )
        return self.variables[name]


# ══════════════════════════════════════════════════════════════
#  PUBLIC ENTRY POINT
# ══════════════════════════════════════════════════════════════

def run_source(source: str) -> None:
    """Lex → parse → interpret a source string."""
    try:
        lines  = Lexer(source).tokenize()
        ast    = Parser(lines).parse()
        Interpreter().run(ast)
    except (LexerError, ParseError, SnippetRuntimeError) as err:
        print(f"\n  ✖  {err}", file=sys.stderr)
        sys.exit(1)


# ══════════════════════════════════════════════════════════════
#  COMMAND LINE
# ══════════════════════════════════════════════════════════════

REPL_BANNER = """\
╔════════════════════════════════╗
║  Snippet Language REPL  v1.0   ║
║  blank line → run              ║
║  'exit'     → quit             ║
╚════════════════════════════════╝
"""

def _repl() -> None:
    print(REPL_BANNER)
    while True:
        collected: List[str] = []
        prompt = '> '
        try:
            while True:
                line = input(prompt)
                if line.strip().lower() == 'exit':
                    sys.exit(0)
                if line == '' and collected:
                    break
                collected.append(line)
                prompt = '  '
        except EOFError:
            break
        if collected:
            run_source('\n'.join(collected))
            print()


def main() -> None:
    if len(sys.argv) == 1:
        _repl()
    elif len(sys.argv) == 2:
        path = sys.argv[1]
        try:
            with open(path) as fh:
                source = fh.read()
        except FileNotFoundError:
            print(f"File not found: {path}", file=sys.stderr)
            sys.exit(1)
        run_source(source)
    else:
        print(f"Usage: {sys.argv[0]} [file.snip]", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()
