#!/usr/bin/env python3
"""
el_ui_native_codegen.py — el-ui component → el-native codegen pass.

Reads a .el component file using the el-ui native component DSL and emits
valid el code that calls the el-native vessel API (vstack, label, button,
widget_set_text, etc.).

Usage:
    python3 el_ui_native_codegen.py <component.el>   # writes to stdout
    python3 el_ui_native_codegen.py <component.el> -o out.el

The output is a standalone .el file that can be compiled with elc and linked
against the el-native vessel. It is semantically equivalent to what a human
would write by hand (as demonstrated in native-hello/src/App.el).

Input format (native component DSL):
    component App {
        state {
            counter: Int = 0
            input_text: String = ""
        }

        fn increment(widget: Int, data: String) -> Void {
            state.counter = state.counter + 1
        }

        template {
            <vstack spacing=24 padding=20>
                <label text="Count: {state.counter}" />
                <button label="Increment" on_click=increment />
            </vstack>
        }
    }

Output format: valid el code targeting el-native vessel API.
"""

import re
import sys
import argparse
from dataclasses import dataclass, field
from typing import Optional


# ── AST types ──────────────────────────────────────────────────────────────────

@dataclass
class StateVar:
    name: str
    type_: str          # "Int", "String", "Bool", "Float"
    default: str        # raw default value string


@dataclass
class MethodDef:
    name: str
    params: list        # list of (param_name, param_type) tuples
    return_type: str
    body_lines: list    # raw body lines (indentation stripped)


@dataclass
class TemplateAttr:
    name: str
    value: str          # raw attribute value (stripped of quotes/braces)
    is_expr: bool       # True if was {expr}, False if was literal


@dataclass
class TemplateNode:
    tag: str            # "vstack", "label", "button", etc.
    attrs: list         # list of TemplateAttr
    children: list      # list of TemplateNode
    self_closing: bool


@dataclass
class Component:
    name: str
    state: list         # list of StateVar
    methods: list       # list of MethodDef
    template: Optional[TemplateNode]


# ── Lexer/parser ───────────────────────────────────────────────────────────────

class ParseError(Exception):
    pass


def _strip_line_comment(line: str) -> str:
    """Remove // line comments, but not inside strings."""
    in_str = False
    i = 0
    while i < len(line):
        c = line[i]
        if c == '"' and not in_str:
            in_str = True
        elif c == '"' and in_str:
            in_str = False
        elif c == '/' and i + 1 < len(line) and line[i+1] == '/' and not in_str:
            return line[:i].rstrip()
        i += 1
    return line


class Tokenizer:
    """Line-based tokenizer that tracks brace depth for block extraction."""

    def __init__(self, src: str):
        self.lines = src.splitlines()
        self.pos = 0

    def peek(self) -> Optional[str]:
        while self.pos < len(self.lines):
            line = _strip_line_comment(self.lines[self.pos]).strip()
            if line:
                return line
            self.pos += 1
        return None

    def next_line(self) -> Optional[str]:
        while self.pos < len(self.lines):
            raw = self.lines[self.pos]
            self.pos += 1
            stripped = _strip_line_comment(raw).strip()
            if stripped:
                return stripped
        return None

    def read_block(self) -> list:
        """Read lines inside the next { } block (already consumed opening brace
        on the triggering line). Returns lines with their raw indentation
        preserved (relative to block start), stopping before the matching }."""
        # Find the opening brace — it may already be on the current trigger
        # line (consumed by caller) or we scan for it.
        depth = 1
        block_lines = []
        while depth > 0:
            raw = None
            while self.pos < len(self.lines):
                raw = self.lines[self.pos]
                self.pos += 1
                if _strip_line_comment(raw).strip():
                    break
                block_lines.append(raw)  # preserve blank lines
                raw = None
            if raw is None:
                raise ParseError("Unexpected end of file inside block")
            stripped = _strip_line_comment(raw).strip()
            for ch in raw:
                if ch == '{':
                    depth += 1
                elif ch == '}':
                    depth -= 1
                    if depth == 0:
                        break
            if depth == 0:
                # stripped may still have partial content before the '}'
                before_close = stripped.rstrip('}').strip()
                if before_close:
                    block_lines.append(before_close)
            else:
                block_lines.append(raw.rstrip('\n'))
        return block_lines


def parse_state_block(lines: list) -> list[StateVar]:
    """Parse lines like:  name: Type = default"""
    vars_ = []
    for raw in lines:
        line = _strip_line_comment(raw).strip()
        if not line:
            continue
        # name: Type = default
        m = re.match(r'^(\w+)\s*:\s*(\w+)\s*=\s*(.+)$', line)
        if m:
            name, type_, default = m.group(1), m.group(2), m.group(3).strip()
            vars_.append(StateVar(name=name, type_=type_, default=default))
        else:
            # name: Type  (no default)
            m2 = re.match(r'^(\w+)\s*:\s*(\w+)\s*$', line)
            if m2:
                name, type_ = m2.group(1), m2.group(2)
                default = '0' if type_ == 'Int' else ('""' if type_ == 'String' else 'false')
                vars_.append(StateVar(name=name, type_=type_, default=default))
    return vars_


def parse_params(params_str: str) -> list:
    """Parse 'widget: Int, data: String' into [('widget','Int'), ('data','String')]."""
    params_str = params_str.strip()
    if not params_str:
        return []
    result = []
    for part in params_str.split(','):
        part = part.strip()
        if not part:
            continue
        m = re.match(r'^(\w+)\s*:\s*([\w\[\]?]+)$', part)
        if m:
            result.append((m.group(1), m.group(2)))
        else:
            result.append((part, 'Int'))
    return result


def parse_method(tok: 'Tokenizer', first_line: str) -> MethodDef:
    """Parse a method starting with 'fn name(...) -> Type {'."""
    m = re.match(r'^fn\s+(\w+)\s*\(([^)]*)\)\s*->\s*(\w+)\s*\{?\s*$', first_line)
    if not m:
        raise ParseError(f"Cannot parse method: {first_line!r}")
    name = m.group(1)
    params = parse_params(m.group(2))
    return_type = m.group(3)
    # Read body block if { is on this line
    body_lines = tok.read_block()
    return MethodDef(name=name, params=params, return_type=return_type, body_lines=body_lines)


# ── Template parser ────────────────────────────────────────────────────────────

def parse_template_attrs(attr_str: str) -> list[TemplateAttr]:
    """Parse attribute string from a tag into TemplateAttr list.

    Handles:
      spacing=20          → literal int
      text="Hello world"  → literal string
      text="Hi {name}"    → string with interpolation
      disabled={expr}     → expression
      on_click=fn_name    → bare identifier
      flex=1              → literal int
    """
    attrs = []
    # Tokenize attrs: handle quoted strings and {expr} as units
    i = 0
    s = attr_str.strip()
    while i < len(s):
        # Skip whitespace
        while i < len(s) and s[i].isspace():
            i += 1
        if i >= len(s):
            break
        # Read attr name (up to '=')
        name_start = i
        while i < len(s) and s[i] not in ('=', ' ', '\t', '\n', '>'):
            i += 1
        name = s[name_start:i].strip()
        if not name:
            i += 1
            continue
        if i >= len(s) or s[i] != '=':
            # Boolean attr with no value
            attrs.append(TemplateAttr(name=name, value='true', is_expr=False))
            continue
        i += 1  # consume '='
        if i >= len(s):
            break
        # Read value
        if s[i] == '"':
            # Quoted string
            i += 1
            val_start = i
            while i < len(s) and s[i] != '"':
                i += 1
            value = s[val_start:i]
            if i < len(s):
                i += 1  # consume closing "
            attrs.append(TemplateAttr(name=name, value=value, is_expr=False))
        elif s[i] == '{':
            # Expression
            i += 1
            depth = 1
            val_start = i
            while i < len(s) and depth > 0:
                if s[i] == '{':
                    depth += 1
                elif s[i] == '}':
                    depth -= 1
                i += 1
            value = s[val_start:i-1]
            attrs.append(TemplateAttr(name=name, value=value, is_expr=True))
        else:
            # Bare value (number or identifier)
            val_start = i
            while i < len(s) and not s[i].isspace():
                i += 1
            value = s[val_start:i]
            attrs.append(TemplateAttr(name=name, value=value, is_expr=False))
    return attrs


def parse_template_lines(lines: list) -> Optional[TemplateNode]:
    """Parse template lines into a TemplateNode tree.

    Uses a stack-based approach since lines already have brace-tracking
    stripped by read_block.
    """
    # Re-join and tokenize as XML-like tag soup
    src = '\n'.join(lines)
    root_nodes = _parse_tag_sequence(src)
    if not root_nodes:
        return None
    if len(root_nodes) == 1:
        return root_nodes[0]
    # Multiple roots — wrap in implicit vstack
    wrapper = TemplateNode(tag='vstack', attrs=[TemplateAttr('spacing','0',False)],
                           children=root_nodes, self_closing=False)
    return wrapper


def _parse_tag_sequence(src: str) -> list:
    """Parse a sequence of tags from src string, returning list of TemplateNode."""
    nodes = []
    pos = [0]

    def skip_ws():
        while pos[0] < len(src) and src[pos[0]] in ' \t\n\r':
            pos[0] += 1

    def parse_one() -> Optional[TemplateNode]:
        skip_ws()
        if pos[0] >= len(src):
            return None
        if src[pos[0]] != '<':
            # Text content — skip
            while pos[0] < len(src) and src[pos[0]] != '<':
                pos[0] += 1
            return None
        if src[pos[0]:pos[0]+2] == '</':
            # Closing tag — stop
            return None
        pos[0] += 1  # consume '<'
        # Read tag name
        name_start = pos[0]
        while pos[0] < len(src) and src[pos[0]] not in (' ', '\t', '\n', '/', '>'):
            pos[0] += 1
        tag_name = src[name_start:pos[0]]
        # Read attributes up to '>' or '/>'
        attr_start = pos[0]
        while pos[0] < len(src):
            c = src[pos[0]]
            if c == '/' and pos[0]+1 < len(src) and src[pos[0]+1] == '>':
                attr_str = src[attr_start:pos[0]]
                pos[0] += 2  # consume '/>'
                attrs = parse_template_attrs(attr_str)
                return TemplateNode(tag=tag_name, attrs=attrs, children=[], self_closing=True)
            elif c == '>':
                attr_str = src[attr_start:pos[0]]
                pos[0] += 1  # consume '>'
                attrs = parse_template_attrs(attr_str)
                # Parse children
                children = []
                while pos[0] < len(src):
                    skip_ws()
                    if pos[0] >= len(src):
                        break
                    if src[pos[0]:pos[0]+2] == '</':
                        # Consume closing tag
                        while pos[0] < len(src) and src[pos[0]] != '>':
                            pos[0] += 1
                        if pos[0] < len(src):
                            pos[0] += 1
                        break
                    child = parse_one()
                    if child is None and src[pos[0]:pos[0]+2] == '</':
                        while pos[0] < len(src) and src[pos[0]] != '>':
                            pos[0] += 1
                        if pos[0] < len(src):
                            pos[0] += 1
                        break
                    if child is not None:
                        children.append(child)
                return TemplateNode(tag=tag_name, attrs=attrs, children=children, self_closing=False)
            pos[0] += 1
        return None

    while pos[0] < len(src):
        node = parse_one()
        if node is not None:
            nodes.append(node)
        else:
            skip_ws()
            if pos[0] < len(src) and src[pos[0]] != '<':
                # Skip non-tag text
                while pos[0] < len(src) and src[pos[0]] != '<':
                    pos[0] += 1
            elif pos[0] < len(src) and src[pos[0]:pos[0]+2] == '</':
                break
            elif pos[0] < len(src) and src[pos[0]] == '<':
                # Could not parse — skip one char to avoid infinite loop
                pos[0] += 1

    return nodes


def parse_component_file(src: str) -> Component:
    """Top-level parser: extract one component { ... } block."""
    tok = Tokenizer(src)
    state_vars = []
    methods = []
    template_node = None
    component_name = 'App'

    while True:
        line = tok.next_line()
        if line is None:
            break
        # Match: component Name {
        m = re.match(r'^component\s+(\w+)\s*\{?\s*$', line)
        if m:
            component_name = m.group(1)
            # Now parse the component body
            # read_block will consume until matching }
            # but we need to parse subsections — do it manually
            _parse_component_body(tok, state_vars, methods, lambda n: None)
            # parse template via a local variable trick
            break

    # Re-parse with template capture
    state_vars.clear()
    methods.clear()
    tok2 = Tokenizer(src)
    template_holder = [None]

    def capture_template(n):
        template_holder[0] = n

    while True:
        line = tok2.next_line()
        if line is None:
            break
        m = re.match(r'^component\s+(\w+)\s*\{?\s*$', line)
        if m:
            component_name = m.group(1)
            _parse_component_body(tok2, state_vars, methods, capture_template)
            break

    return Component(
        name=component_name,
        state=state_vars,
        methods=methods,
        template=template_holder[0],
    )


def _parse_component_body(tok: Tokenizer, state_vars: list, methods: list, capture_template):
    """Parse the inside of component { ... }."""
    while True:
        line = tok.peek()
        if line is None:
            break
        if line == '}':
            tok.next_line()
            break
        line = tok.next_line()
        if line is None:
            break

        if re.match(r'^state\s*\{', line):
            block = tok.read_block()
            state_vars.extend(parse_state_block(block))

        elif re.match(r'^fn\s+', line):
            method = parse_method(tok, line)
            methods.append(method)

        elif re.match(r'^template\s*\{', line):
            block = tok.read_block()
            node = parse_template_lines(block)
            capture_template(node)

        elif line == '}':
            break
        # else skip props, imports, etc.


# ── Code generator ─────────────────────────────────────────────────────────────

class Codegen:
    def __init__(self, component: Component, vessel_path: str = "../../vessels/el-native/src/main.el"):
        self.comp = component
        self.vessel_path = vessel_path
        self._var_counter = 0
        self._widget_vars: list = []   # [(var_name, global_name)] for global widget handles
        self._lines: list = []

    def _fresh_var(self, hint: str = "w") -> str:
        self._var_counter += 1
        return f"{hint}_{self._var_counter}"

    def _emit(self, line: str = ""):
        self._lines.append(line)

    def _state_type(self, name: str) -> str:
        for sv in self.comp.state:
            if sv.name == name:
                return sv.type_
        return "Int"

    def _subst_state(self, expr: str) -> str:
        """Replace state.foo → g_foo in an expression."""
        return re.sub(r'\bstate\.(\w+)\b', lambda m: f"g_{m.group(1)}", expr)

    def _interpolate_string(self, text: str) -> str:
        """Convert "Hello {state.counter}" → str_concat("Hello ", int_to_str(g_counter)).

        Handles multiple interpolations via nested str_concat chains.
        Returns a valid el string expression.
        """
        # Find all {expr} segments
        parts = re.split(r'(\{[^}]+\})', text)
        if len(parts) == 1:
            # No interpolation
            return f'"{text}"'

        el_parts = []
        for part in parts:
            if not part:
                continue
            if part.startswith('{') and part.endswith('}'):
                inner = part[1:-1]
                inner = self._subst_state(inner)
                # Determine type of the expression for conversion
                # Heuristic: if it's a state variable reference, use known type
                sv_match = re.match(r'^g_(\w+)$', inner)
                if sv_match:
                    vname = sv_match.group(1)
                    t = self._state_type(vname)
                    if t == 'Int':
                        el_parts.append(f'int_to_str({inner})')
                    elif t == 'Float':
                        el_parts.append(f'float_to_str({inner})')
                    else:
                        el_parts.append(inner)
                elif re.search(r'\b(int_to_str|str_len)\b', inner):
                    # Already wrapped
                    el_parts.append(inner)
                else:
                    # Expression — try to wrap as int_to_str if it looks numeric
                    el_parts.append(f'int_to_str({inner})')
            else:
                el_parts.append(f'"{part}"')

        if len(el_parts) == 1:
            return el_parts[0]
        # Build nested str_concat
        result = el_parts[0]
        for p in el_parts[1:]:
            result = f'str_concat({result}, {p})'
        return result

    def _attr(self, attrs: list, name: str) -> Optional[TemplateAttr]:
        for a in attrs:
            if a.name == name:
                return a
        return None

    def _attr_int(self, attrs: list, name: str, default: int = 0) -> int:
        a = self._attr(attrs, name)
        if a is None:
            return default
        try:
            return int(a.value)
        except (ValueError, TypeError):
            return default

    def _emit_attr_calls(self, var: str, attrs: list, skip: set):
        """Emit widget property calls for all attributes not in skip."""
        for a in attrs:
            if a.name in skip:
                continue
            if a.name == 'padding':
                self._emit(f"    widget_set_padding_all({var}, {a.value})")
            elif a.name == 'padding_x':
                py_val = self._attr_int(attrs, 'padding_y', 0)
                self._emit(f"    widget_set_padding_xy({var}, {a.value}, {py_val})")
            elif a.name == 'padding_y':
                pass  # handled with padding_x
            elif a.name == 'bg':
                val = a.value.strip('"')
                self._emit(f'    widget_set_bg_color_hex({var}, "{val}")')
            elif a.name == 'color':
                val = a.value.strip('"')
                self._emit(f'    widget_set_color_hex({var}, "{val}")')
            elif a.name == 'width':
                self._emit(f"    widget_set_width({var}, {a.value})")
            elif a.name == 'height':
                self._emit(f"    widget_set_height({var}, {a.value})")
            elif a.name == 'flex':
                self._emit(f"    widget_set_flex({var}, {a.value})")
            elif a.name == 'radius':
                self._emit(f"    widget_set_corner_radius({var}, {a.value})")
            elif a.name == 'font':
                self._emit(f'    widget_set_font({var}, "system", {a.value}, false)')
            elif a.name == 'font_bold':
                self._emit(f'    widget_set_font({var}, "system", {a.value}, true)')
            elif a.name == 'style':
                sv = a.value.strip('"')
                if sv == 'surface':
                    self._emit(f"    style_surface({var})")
                elif sv == 'button_primary':
                    self._emit(f"    style_button_primary({var})")
                elif sv == 'heading':
                    self._emit(f"    style_label_heading({var})")
                elif sv == 'body':
                    self._emit(f"    style_label_body({var})")
                elif sv == 'muted':
                    self._emit(f"    style_label_muted({var})")

    def _emit_node(self, node: TemplateNode, parent_var: Optional[str]) -> str:
        """Emit code for one template node. Returns the variable name."""
        tag = node.tag
        attrs = node.attrs

        if tag in ('vstack', 'hstack'):
            spacing = self._attr_int(attrs, 'spacing', 0)
            var = self._fresh_var(tag)
            self._emit(f"    let {var}: Int = {tag}({spacing})")
            self._emit_attr_calls(var, attrs, skip={'spacing'})
            # Emit children
            for child in node.children:
                child_var = self._emit_node(child, var)
                self._emit(f"    widget_add_child({var}, {child_var})")
            if parent_var:
                pass  # caller will add_child
            return var

        elif tag == 'zstack':
            var = self._fresh_var('zstack')
            self._emit(f"    let {var}: Int = zstack()")
            self._emit_attr_calls(var, attrs, skip=set())
            for child in node.children:
                child_var = self._emit_node(child, var)
                self._emit(f"    widget_add_child({var}, {child_var})")
            return var

        elif tag == 'scroll':
            var = self._fresh_var('scroll')
            self._emit(f"    let {var}: Int = scroll()")
            self._emit_attr_calls(var, attrs, skip=set())
            for child in node.children:
                child_var = self._emit_node(child, var)
                self._emit(f"    widget_add_child({var}, {child_var})")
            return var

        elif tag == 'label':
            text_attr = self._attr(attrs, 'text')
            raw_text = text_attr.value if text_attr else ""
            text_expr = self._interpolate_string(raw_text)
            var = self._fresh_var('lbl')
            self._emit(f"    let {var}: Int = label({text_expr})")
            self._emit_attr_calls(var, attrs, skip={'text'})
            return var

        elif tag == 'button':
            label_attr = self._attr(attrs, 'label')
            btn_label = label_attr.value if label_attr else ""
            var = self._fresh_var('btn')
            self._emit(f'    let {var}: Int = button("{btn_label}")')
            style_attr = self._attr(attrs, 'style')
            if style_attr is None:
                self._emit(f"    style_button_primary({var})")
            # Skip attrs handled explicitly below or that have their own emitters
            self._emit_attr_calls(var, attrs, skip={'label', 'on_click', 'disabled'})
            on_click = self._attr(attrs, 'on_click')
            if on_click:
                self._emit(f'    widget_on_click({var}, "{on_click.value}")')
            disabled_attr = self._attr(attrs, 'disabled')
            if disabled_attr:
                expr = self._subst_state(disabled_attr.value)
                self._emit(f"    widget_set_disabled({var}, {expr})")
            return var

        elif tag == 'text_field':
            ph_attr = self._attr(attrs, 'placeholder')
            placeholder = ph_attr.value if ph_attr else ""
            var = self._fresh_var('tf')
            self._emit(f'    let {var}: Int = text_field("{placeholder}")')
            self._emit_attr_calls(var, attrs, skip={'placeholder', 'on_change'})
            on_change = self._attr(attrs, 'on_change')
            if on_change:
                self._emit(f'    widget_on_change({var}, "{on_change.value}")')
            return var

        elif tag == 'text_area':
            ph_attr = self._attr(attrs, 'placeholder')
            placeholder = ph_attr.value if ph_attr else ""
            var = self._fresh_var('ta')
            self._emit(f'    let {var}: Int = text_area("{placeholder}")')
            self._emit_attr_calls(var, attrs, skip={'placeholder', 'on_change'})
            on_change = self._attr(attrs, 'on_change')
            if on_change:
                self._emit(f'    widget_on_change({var}, "{on_change.value}")')
            return var

        elif tag == 'image':
            src_attr = self._attr(attrs, 'src')
            src = src_attr.value if src_attr else ""
            var = self._fresh_var('img')
            self._emit(f'    let {var}: Int = image("{src}")')
            self._emit_attr_calls(var, attrs, skip={'src'})
            return var

        else:
            # Unknown tag — emit as a label with tag name for debugging
            var = self._fresh_var('unknown')
            self._emit(f'    let {var}: Int = label("[{tag}]")')
            return var

    def _emit_method(self, method: MethodDef):
        """Emit a component method as an el top-level function.

        Substitutes state.foo → g_foo in the body.
        Normalizes callback signature to (widget: Int, data: String) -> Void
        if the method has no params or incompatible params.
        """
        # Normalize: callbacks must be (widget: Int, data: String) -> Void
        # to be registered with widget_on_click / widget_on_change.
        # Methods with no params get the standard callback signature.
        if not method.params:
            params_str = "widget: Int, data: String"
        else:
            params_str = ', '.join(f"{n}: {t}" for n, t in method.params)

        self._emit(f"fn {method.name}({params_str}) -> {method.return_type} {{")
        for raw_line in method.body_lines:
            line = _strip_line_comment(raw_line).strip()
            if not line:
                self._emit("")
                continue
            # state.foo = expr  →  g_foo = expr
            line = re.sub(r'\bstate\.(\w+)\b', lambda m: f"g_{m.group(1)}", line)
            # After state mutation, emit widget_set_text for labels that
            # display this state (best-effort: only for simple g_foo assignments)
            self._emit(f"    {line}")
        self._emit("}")
        self._emit("")

    def generate(self) -> str:
        comp = self.comp
        self._emit(f"// Generated by el-ui native codegen — do not edit.")
        self._emit(f"// Source component: {comp.name}")
        self._emit("")
        self._emit(f'import "{self.vessel_path}"')
        self._emit("")

        # ── State globals ──────────────────────────────────────────────────────
        if comp.state:
            self._emit("// ── State ────────────────────────────────────────────────────────────")
            self._emit("")
            for sv in comp.state:
                self._emit(f"let g_{sv.name}: {sv.type_} = {sv.default}")
            self._emit("")

        # ── Methods ────────────────────────────────────────────────────────────
        if comp.methods:
            self._emit("// ── Callbacks ────────────────────────────────────────────────────────")
            self._emit("")
            for method in comp.methods:
                self._emit_method(method)

        # ── app_build ──────────────────────────────────────────────────────────
        build_fn = f"{comp.name[0].lower()}{comp.name[1:]}_build"
        self._emit("// ── Widget tree ───────────────────────────────────────────────────────")
        self._emit("")
        self._emit(f"fn {build_fn}(window: Int) -> Void {{")

        if comp.template:
            root_var = self._emit_node(comp.template, None)
            self._emit(f"    widget_add_child(window, {root_var})")
        else:
            self._emit("    // (no template)")

        self._emit("}")
        self._emit("")

        return '\n'.join(self._lines)


# ── CLI ────────────────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(
        description="el-ui native codegen: component DSL → el-native API calls"
    )
    parser.add_argument("input", help="Input .el component file")
    parser.add_argument("-o", "--output", help="Output file (default: stdout)")
    parser.add_argument(
        "--vessel-path",
        default="../../vessels/el-native/src/main.el",
        help="Import path for el-native vessel in the output file",
    )
    args = parser.parse_args()

    try:
        with open(args.input, 'r', encoding='utf-8') as f:
            src = f.read()
    except FileNotFoundError:
        print(f"Error: file not found: {args.input}", file=sys.stderr)
        sys.exit(1)

    try:
        component = parse_component_file(src)
    except ParseError as e:
        print(f"Parse error: {e}", file=sys.stderr)
        sys.exit(1)

    gen = Codegen(component, vessel_path=args.vessel_path)
    output = gen.generate()

    if args.output:
        with open(args.output, 'w', encoding='utf-8') as f:
            f.write(output)
        print(f"Written to {args.output}", file=sys.stderr)
    else:
        print(output)


if __name__ == "__main__":
    main()
