from bcad.binterpreter.scadlike_parser import ScadLikeParser, ParserError

import os
import json
import math
import random
import time
import traceback

from logging import debug, warning, critical
import logging
import signal


from bcad.binterpreter.events import ee, ep
from bcad.binterpreter.singleton import Singleton
from bcad.binterpreter.scl_writer import SCLSTLWriter, SCLSTEPWriter, SCLDXFWriter
from bcad.binterpreter.scl_util import unstringify, sign, is_var_set
from bcad.binterpreter.scl_context import V3, SCLProfile2, SCLExtrude, SCLRevol, SCLGroup, SCLUnion, SCLIntersection, SCLDifference, SCLProjection, SCLPart3, SCLLoft, SCLMinkowski, SCLOffset, SCLHull, get_inc_name
from bcad.binterpreter.args import parse_args
from bcad.binterpreter.rqq import replies, RP_SETTING, RP_LOG
from bcad.binterpreter.progress_helper import (
    StopExecution,
    init_progress, advance_progress, adjust_progress_total,
    check_cancel, reset_cancel, push_label, pop_label, set_progress_label,
)
from bcad.binterpreter.settings import Settings


debug_parser=False
debug_expr_en = False

TAG="scl "

def debug_expr(msg):
    if (debug_expr_en):
        debug(TAG+msg)

def scl_bool(val):
    if val is None:
        return False
    if isinstance(val, bool):
        return val
    if isinstance(val, (int, float)):
        return val != 0
    if isinstance(val, str):
        return val != "" and val.lower() != 'false'
    if isinstance(val, list):
        return len(val) > 0
    return True

def scl_str(val):
    if val is True:
        return "true"
    if val is False:
        return "false"
    if val is None:
        return "undef"
    if isinstance(val, float):
        if val.is_integer():
            return str(int(val))
        return str(val)
    return str(val)


def substitute_undef_with_defaults(val, default_val):
    if val is None:
        return default_val
    if isinstance(val, list) and isinstance(default_val, list):
        merged = []
        for i in range(len(val)):
            v = val[i]
            d = default_val[i] if i < len(default_val) else None
            if v is None:
                merged.append(d)
            elif isinstance(v, list) and isinstance(d, list):
                merged.append(substitute_undef_with_defaults(v, d))
            else:
                merged.append(v)
        return merged
    return val


def _raise_assert(args, line):
    if not args[0]:
        msg = args[1] if len(args) > 1 else "Assertion failed"
        raise AssertionError(scl_str(msg), line)
    return None

def _version():
    try:
        from bcad._version import HASH
        return ['bcad', HASH]
    except Exception:
        return ['bcad', 'dev']

def _echo_output(args, line):
    out = f"{line:>4}: " + ", ".join(scl_str(a) for a in args)
    if hasattr(Singleton, 'pipe'):
        Singleton.pipe.send(json.dumps({
            'rp': replies[RP_LOG],
            'args': ['output', out]
        }))
    else:
        print(out)
    return None


class UnknownVariableError(Exception):
    def __init__(self, varname, line):
        self.message = "Unknown variable %s at line %i"%(varname, line)

class UnknownFunctionError(Exception):
    def __init__(self, fname, line):
        self.message = "Unknown function %s at line %i"%(fname, line)

class UnknownModuleError(Exception):
    def __init__(self, fname, line):
        self.message = "Unknown module %s at line %i"%(fname, line)

class UnhandledCaseError(Exception):
    def __init__(self, message):
        self.message = "Unhandled case encountered: %s"%(message)

class AssertionError(Exception):
    def __init__(self, msg, line):
        self.message = msg
        self.line = line

class SyntaxError(Exception):
    def __init__(self, message, line):
        self.message = "Syntax error at line %i: %s"%(line, message)

square_module_definition = {'type': 'stat_module_definition', 'id': 'square', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'size', 'val': {'type': 'array_list', 'line': 0, 'val': [
        {'type': 'expr_number', 'val': 1.0, 'line': 0},
        {'type': 'expr_number', 'val': 1.0, 'line': 0}
    ]}},
    {'type': 'expr_assign', 'id': 'center', 'line': 0, 'val': {'type': 'expr_bool', 'line': 0, 'val': False}}
]}
circle_module_definition = {'type': 'stat_module_definition', 'id': 'circle', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'r', 'val': {'type': 'expr_number', 'line': 0, 'val': None}},
    {'type': 'expr_assign', 'id': 'd', 'val': {'type': 'expr_number', 'line': 0, 'val': None}}
]}
rotate_module_definition = {'type': 'stat_module_definition', 'id': 'rotate', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'v', 'val': {'type': 'array_list', 'line': 0, 'val': [
        {'type': 'expr_number', 'val': 0.0, 'line': 0},
        {'type': 'expr_number', 'val': 0.0, 'line': 0},
        {'type': 'expr_number', 'val': 0.0, 'line': 0}
    ]}}]}
polygon_module_definition = {'type': 'stat_module_definition', 'id': 'polygon', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'points', 'val': {'type': 'array_list', 'line': 0, 'val': []}},
    {'type': 'expr_assign', 'id': 'paths', 'val': {'type': 'array_list', 'line': 0, 'val': []}},
]}
translate_module_definition = {'type': 'stat_module_definition', 'id': 'translate', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'v', 'val': {'type': 'array_list', 'line': 0, 'val': [
        {'type': 'expr_number', 'val': 0.0, 'line': 0},
        {'type': 'expr_number', 'val': 0.0, 'line': 0},
        {'type': 'expr_number', 'val': 0.0, 'line': 0}
    ]}}]}
color_module_definition = {'type': 'stat_module_definition', 'id': 'color', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'v', 'val': {'type': 'expr_number', 'line': 0, 'val': None}},
    {'type': 'expr_assign', 'id': 'alpha', 'line': 0, 'val': {'type': 'expr_number', 'line': 0, 'val': 1.0}} # Дефолт: 1.0 (непрозрачный)
]}
profile_module_definition = {'type': 'stat_module_definition', 'id': 'profile', 'line': 0, 'args': []}
linear_extrude_module_definition = {'type': 'stat_module_definition', 'id': 'linear_extrude', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'height', 'val': {'type': 'expr_number', 'val': 1.0, 'line': 0}, 'line': 0},
    {'type': 'expr_assign', 'id': 'twist', 'val': {'type': 'expr_number', 'val': 0.0, 'line': 0}, 'line': 0}
]}
rotate_extrude_module_definition = {'type': 'stat_module_definition', 'id': 'rotate_extrude', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'angle', 'val': {'type': 'expr_number', 'val': 360.0, 'line': 0}, 'line': 0},
    {'type': 'expr_assign', 'id': 'convexity', 'val': {'type': 'expr_number', 'val': 10.0, 'line': 0}, 'line': 0}
]}
group_module_definition = {'type': 'stat_module_definition', 'id': 'group', 'line': 0, 'args': []}
union_module_definition = {'type': 'stat_module_definition', 'id': 'union', 'line': 0, 'args': []}
intersection_module_definition = {'type': 'stat_module_definition', 'id': 'intersection', 'line': 0, 'args': []}
difference_module_definition = {'type': 'stat_module_definition', 'id': 'difference', 'line': 0, 'args': []}
minkowski_module_definition = {'type': 'stat_module_definition', 'id': 'minkowski', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'color_blend', 'val': {'type': 'expr_bool', 'line': 0, 'val': False}},
]}
offset_module_definition = {'type': 'stat_module_definition', 'id': 'offset', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'r', 'val': {'type': 'expr_number', 'line': 0, 'val': None}},
    {'type': 'expr_assign', 'id': 'delta', 'val': {'type': 'expr_number', 'line': 0, 'val': None}},
    {'type': 'expr_assign', 'id': 'chamfer', 'val': {'type': 'expr_bool', 'line': 0, 'val': False}},
    {'type': 'expr_assign', 'id': 'color_blend', 'val': {'type': 'expr_bool', 'line': 0, 'val': False}},
]}
mirror_module_definition = {'type': 'stat_module_definition', 'id': 'mirror', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'v', 'val': {'type': 'array_list', 'line': 0, 'val': [
        {'type': 'expr_number', 'val': 0.0, 'line': 0},
        {'type': 'expr_number', 'val': 0.0, 'line': 0},
        {'type': 'expr_number', 'val': 0.0, 'line': 0}
    ]}}]}
scale_module_definition = {'type': 'stat_module_definition', 'id': 'scale', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'v', 'val': {'type': 'array_list', 'line': 0, 'val': [
        {'type': 'expr_number', 'val': 1.0, 'line': 0},
        {'type': 'expr_number', 'val': 1.0, 'line': 0},
        {'type': 'expr_number', 'val': 1.0, 'line': 0}
    ]}}]}
fillet_module_definition = {'type': 'stat_module_definition', 'id': 'fillet', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'r', 'val': {'type': 'expr_number', 'val': 1.0, 'line': 0}, 'line': 0}
]}
import_module_definition = {'type': 'stat_module_definition', 'id': 'import', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'filename', 'val': {'type': 'expr_string', 'val': "", 'line': 0}, 'line': 0}
]}
line_module_definition = {'type': 'stat_module_definition', 'id': 'line', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'start', 'val': {'type': 'array_list', 'line': 0, 'val': [
        {'type': 'expr_number', 'val': 0.0, 'line': 0},
        {'type': 'expr_number', 'val': 0.0, 'line': 0},
        {'type': 'expr_number', 'val': 0.0, 'line': 0}
    ]}},
    {'type': 'expr_assign', 'id': 'end', 'val': {'type': 'array_list', 'line': 0, 'val': [
        {'type': 'expr_number', 'val': 0.0, 'line': 0},
        {'type': 'expr_number', 'val': 0.0, 'line': 0},
        {'type': 'expr_number', 'val': 0.0, 'line': 0}
    ]}}
]}
move_module_definition = {'type': 'stat_module_definition', 'id': 'move', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'p', 'val': {'type': 'array_list', 'line': 0, 'val': [
        {'type': 'expr_number', 'val': 0.0, 'line': 0},
        {'type': 'expr_number', 'val': 0.0, 'line': 0}
    ]}}
]}
arc_module_definition = {'type': 'stat_module_definition', 'id': 'arc', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'center', 'val': {'type': 'array_list', 'line': 0, 'val': [
        {'type': 'expr_number', 'val': 0.0, 'line': 0},
        {'type': 'expr_number', 'val': 0.0, 'line': 0}
    ]}},
    {'type': 'expr_assign', 'id': 'angle', 'val': {'type': 'expr_number', 'val': 90.0, 'line': 0}}
]}
spline_module_definition = {'type': 'stat_module_definition', 'id': 'spline', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'points', 'val': {'type': 'array_list', 'line': 0, 'val': []}}
]}
close_module_definition = {'type': 'stat_module_definition', 'id': 'close', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'fillet', 'val': {'type': 'expr_number', 'val': None, 'line': 0}}
]}
cube_module_definition = {'type': 'stat_module_definition', 'id': 'cube', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'v', 'val': {'type': 'array_list', 'line': 0, 'val': [
        {'type': 'expr_number', 'val': 1.0, 'line': 0},
        {'type': 'expr_number', 'val': 1.0, 'line': 0},
        {'type': 'expr_number', 'val': 1.0, 'line': 0}
    ]}},
    {'type': 'expr_assign', 'id': 'center', 'line': 0, 'val': {'type': 'expr_bool', 'line': 0, 'val': False}}
]}
cylinder_module_definition = {'type': 'stat_module_definition', 'id': 'cylinder', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'r', 'val': {'type': 'expr_number', 'line': 0, 'val': None}},
    {'type': 'expr_assign', 'id': 'd', 'val': {'type': 'expr_number', 'line': 0, 'val': None}},
    {'type': 'expr_assign', 'id': 'r1', 'val': {'type': 'expr_number', 'line': 0, 'val': None}},
    {'type': 'expr_assign', 'id': 'd1', 'val': {'type': 'expr_number', 'line': 0, 'val': None}},
    {'type': 'expr_assign', 'id': 'r2', 'val': {'type': 'expr_number', 'line': 0, 'val': None}},
    {'type': 'expr_assign', 'id': 'd2', 'val': {'type': 'expr_number', 'line': 0, 'val': None}},
    {'type': 'expr_assign', 'id': 'h', 'val': {'type': 'expr_number', 'line': 0, 'val': 1.0}},
    {'type': 'expr_assign', 'id': 'center', 'line': 0, 'val': {'type': 'expr_bool', 'line': 0, 'val': False}},
    {'type': 'expr_assign', 'id': '$fn', 'val': {'type': 'expr_number', 'line': 0, 'val': 0}}
]}
sphere_module_definition = {'type': 'stat_module_definition', 'id': 'sphere', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'r', 'val': {'type': 'expr_number', 'line': 0, 'val': 1.0}},
    {'type': 'expr_assign', 'id': 'd', 'val': {'type': 'expr_number', 'line': 0, 'val': None}}
]}
polyhedron_module_definition = {'type': 'stat_module_definition', 'id': 'polyhedron', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'points', 'val': {'type': 'array_list', 'line': 0, 'val': []}},
    {'type': 'expr_assign', 'id': 'faces', 'val': {'type': 'array_list', 'line': 0, 'val': []}},
    {'type': 'expr_assign', 'id': 'triangles', 'val': {'type': 'array_list', 'line': 0, 'val': []}},
    {'type': 'expr_assign', 'id': 'convexity', 'val': {'type': 'expr_number', 'val': 1.0, 'line': 0}, 'line': 0}
]}
hull_module_definition = {'type': 'stat_module_definition', 'id': 'hull', 'line': 0, 'args': []}
projection_module_definition = {'type': 'stat_module_definition', 'id': 'projection', 'line': 0, 'args': []}
display_wireframe_module_definition = {'type': 'stat_module_definition', 'id': 'display_wireframe', 'line': 0, 'args': []}

sqrt_function_definition = {'type': 'stat_function_definition', 'id': 'sqrt', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'v', 'val': {'type': 'expr_number', 'val': 0.0, 'line': 0}, 'line': 0}
]}

sin_function_definition = {'type': 'stat_function_definition', 'id': 'sin', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'v', 'val': {'type': 'expr_number', 'val': 0.0, 'line': 0}, 'line': 0}
]}

cos_function_definition = {'type': 'stat_function_definition', 'id': 'cos', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'v', 'val': {'type': 'expr_number', 'val': 0.0, 'line': 0}, 'line': 0}
]}

tan_function_definition = {'type': 'stat_function_definition', 'id': 'tan', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'v', 'val': {'type': 'expr_number', 'val': 0.0, 'line': 0}, 'line': 0}
]}

reverse_function_definition = {'type': 'stat_function_definition', 'id': 'reverse', 'line': 0, 'args': [
    {'type': 'expr_assign', 'id': 'v', 'line': 0, 'val': {'type': 'array_list', 'val': [], 'line': 0}}
]}

concat_function_definition = {'type': 'stat_function_definition', 'id': 'concat', 'line': 0, 'args': [
]}
loft_module_definition = {'type': 'stat_module_definition', 'id': 'loft', 'line': 0, 'args': [{'type': 'expr_assign', 'id': 'solid', 'val': {'type': 'expr_bool', 'val': True, 'line': 0}, 'line': 0},
                                                                                              {'type': 'expr_assign', 'id': 'ruled', 'val': {'type': 'expr_bool', 'val': True, 'line': 0}, 'line': 0},
                                                                                              {'type': 'expr_assign', 'id': 'precision', 'val': {'type': 'expr_number', 'val': 0.000001, 'line': 0}, 'line': 0}]}

class SCLFrame:
    def __init__(self):
        self.variables = {}
        self.functions = {}
        self.modules = {}

    def get_variable_value(self, varname, line):
        if varname in self.variables:
            return self.variables[varname]
        raise UnknownVariableError(varname, line)

    def has_variable(self, varname, line):
        if varname in self.variables:
            return True
        return False

    def set_variable(self, varname, value, line):
        self.variables[varname] = value

    def get_function(self, fname, line):
        if fname in self.functions:
            return self.functions[fname]
        raise UnknownFunctionError(fname, line)

    def has_function(self, fname, line):
        if fname in self.functions:
            return True
        return False

    def set_function(self, fname, value, line):
        self.functions[fname] = value

    def get_module(self, mname, line):
        if mname in self.modules:
            return self.modules[mname]
        raise UnknownModuleError(mname, line)

    def has_module(self, mname, line):
        debug(TAG+'SCLFrame:%i:Check module %s existance'%(line, mname))
        if mname in self.modules:
            return True
        return False

    def set_module(self, mname, value, line):
        debug(TAG+'SCLFrame:%i:Setting module %s'%(line, mname))
        self.modules[mname] = value

def _rands(args):
    lo, hi, n = args[0], args[1], int(args[2])
    rng = random.Random(int(args[3]) if len(args) > 3 else None)
    return [rng.uniform(lo, hi) for _ in range(n)]

# builtin pure functions (no side effects, no block context)
# 'handler' signature: handler(self, args, node) -> value
# 'params' for future autocomplete: [(name, type), ...]
_builtin_funcs: dict[str, dict] = {
    'sqrt': {
        'handler': lambda self, args, node: math.sqrt(args[0]),
        'params': [('v', 'number')],
    },
    'abs': {
        'handler': lambda self, args, node: abs(args[0]),
        'params': [('v', 'number')],
    },
    'len': {
        'handler': lambda self, args, node: len(args[0]) if isinstance(args[0], (list, str)) else 1,
        'params': [('v', 'any')],
    },
    'min': {
        'handler': lambda self, args, node: min(args[0]) if len(args) == 1 and isinstance(args[0], list) else min(args),
        'params': [('varlist', 'any')],
    },
    'max': {
        'handler': lambda self, args, node: max(args[0]) if len(args) == 1 and isinstance(args[0], list) else max(args),
        'params': [('varlist', 'any')],
    },
    'round': {
        'handler': lambda self, args, node: round(args[0]),
        'params': [('v', 'number')],
    },
    'ceil': {
        'handler': lambda self, args, node: math.ceil(args[0]),
        'params': [('v', 'number')],
    },
    'floor': {
        'handler': lambda self, args, node: math.floor(args[0]),
        'params': [('v', 'number')],
    },
    'sin': {
        'handler': lambda self, args, node: math.sin(math.radians(args[0])),
        'params': [('v', 'number')],
    },
    'cos': {
        'handler': lambda self, args, node: math.cos(math.radians(args[0])),
        'params': [('v', 'number')],
    },
    'tan': {
        'handler': lambda self, args, node: math.tan(math.radians(args[0])),
        'params': [('v', 'number')],
    },
    'asin': {
        'handler': lambda self, args, node: math.degrees(math.asin(args[0])),
        'params': [('v', 'number')],
    },
    'acos': {
        'handler': lambda self, args, node: math.degrees(math.acos(args[0])),
        'params': [('v', 'number')],
    },
    'atan': {
        'handler': lambda self, args, node: math.degrees(math.atan(args[0])),
        'params': [('v', 'number')],
    },
    'atan2': {
        'handler': lambda self, args, node: math.degrees(math.atan2(args[0], args[1])),
        'params': [('y', 'number'), ('x', 'number')],
    },
    'pow': {
        'handler': lambda self, args, node: math.pow(args[0], args[1]),
        'params': [('x', 'number'), ('y', 'number')],
    },
    'exp': {
        'handler': lambda self, args, node: math.exp(args[0]),
        'params': [('v', 'number')],
    },
    'ln': {
        'handler': lambda self, args, node: math.log(args[0]),
        'params': [('v', 'number')],
    },
    'log': {
        'handler': lambda self, args, node: math.log10(args[0]),
        'params': [('v', 'number')],
    },
    'sign': {
        'handler': lambda self, args, node: sign(args[0]),
        'params': [('v', 'number')],
    },
    'str': {
        'handler': lambda self, args, node: "".join([scl_str(val) for val in args]),
        'params': [('varlist', 'any')],
    },
    'reverse': {
        'handler': lambda self, args, node: (lambda rv: (rv.reverse(), rv)[1])(args[0][:]),
        'params': [('v', 'list')],
    },
    'concat': {
        'handler': lambda self, args, node: [x for val in args for x in (val if isinstance(val, list) else [val])],
        'params': [('varlist', 'any')],
    },
    'norm': {
        'handler': lambda self, args, node: math.sqrt(sum(x*x for x in args[0])),
        'params': [('v', 'list')],
    },
    'cross': {
        'handler': lambda self, args, node: [
            args[0][1]*args[1][2] - args[0][2]*args[1][1],
            args[0][2]*args[1][0] - args[0][0]*args[1][2],
            args[0][0]*args[1][1] - args[0][1]*args[1][0],
        ],
        'params': [('a', 'list'), ('b', 'list')],
    },
    'rands': {
        'handler': lambda self, args, node: _rands(args),
        'params': [('lo', 'number'), ('hi', 'number'), ('n', 'number'), ('seed', 'number')],
    },
    'is_undef': {
        'handler': lambda self, args, node: args[0] is None,
        'params': [('v', 'any')],
    },
    'is_list': {
        'handler': lambda self, args, node: isinstance(args[0], list),
        'params': [('v', 'any')],
    },
    'is_num': {
        'handler': lambda self, args, node: isinstance(args[0], (int, float)),
        'params': [('v', 'any')],
    },
    'is_bool': {
        'handler': lambda self, args, node: isinstance(args[0], bool),
        'params': [('v', 'any')],
    },
    'is_string': {
        'handler': lambda self, args, node: isinstance(args[0], str),
        'params': [('v', 'any')],
    },
    'assert': {
        'handler': lambda self, args, node: (_raise_assert(args, node['line']) if not args[0] else None),
        'params': [('cond', 'bool'), ('msg', 'string')],
    },
    'echo': {
        'handler': lambda self, args, node: _echo_output(args, node['line']),
        'params': [('varlist', 'any')],
    },
    'version': {
        'handler': lambda self, args, node: _version(),
        'params': [],
    },
    'version_num': {
        'handler': lambda self, args, node: 0,
        'params': [],
    },
    'timestamp': {
        'handler': lambda self, args, node: time.monotonic(),
        'params': [],
    },
    'current_pos': {
        'handler': lambda self, args, node: _get_current_pos(self),
        'params': [],
    },
}

def _get_current_pos(self):
    ctx = self.active_context
    while ctx is not None:
        if isinstance(ctx, SCLProfile2):
            p = ctx.current_pos
            if p is not None:
                return [p.x(), p.y()]
            return None
        ctx = ctx.parent
    return None

# --- handler functions for _builtin_modules ---

def _handle_linear_extrude(self, node):
    sid, line, args, block = node['id'], node['line'], node['args'], node.get('block', [])
    self.push_stack()
    if debug_parser:
        self.parse_block(block)
    else:
        self.push_context(SCLExtrude, get_inc_name("linear_extrude"))
        self.parse_block(block)
        self.push_stack()
        self.parse_kwargs(sid, line, linear_extrude_module_definition['args'], args)
        ln = 1
        twist = 0.0
        top = self.stack[-1]
        if top.has_variable('height', line):
            ln = self.find_variable_value('height', line)
        if top.has_variable('twist', line):
            twist = self.find_variable_value('twist', line)
        debug(TAG+"Call builtin extrude(%f, twist=%f)" % (ln, twist))
        self.active_context.linear_extrude(ln, twist)
        self.pop_stack()
        self.pop_context()
    debug(TAG+"Leave linear extrude")
    self.pop_stack()

def _handle_rotate_extrude(self, node):
    sid, line, args, block = node['id'], node['line'], node['args'], node.get('block', [])
    self.push_stack()
    if debug_parser:
        self.parse_block(block)
    else:
        self.push_context(SCLRevol, get_inc_name("linear_extrude"))
        self.parse_block(block)
        self.push_stack()
        self.parse_kwargs(sid, line, rotate_extrude_module_definition['args'], args)
        angle = 360.0
        top = self.stack[-1]
        if top.has_variable('angle', line):
            angle = self.find_variable_value('angle', line)
        debug(TAG+"Call builtin rotate_extrude(%f)" % (angle,))
        self.active_context.rotate_extrude(angle)
        self.pop_stack()
        self.pop_context()
    debug(TAG+"Leave rotate_extrude")
    self.pop_stack()

def _handle_hull(self, node):
    self.push_stack()
    if debug_parser:
        self.parse_block(node.get('block', []))
    else:
        self.push_context(SCLHull, get_inc_name("hull"))
        self.parse_block(node.get('block', []))
        self.active_context.hull()
        self.pop_context()
    debug(TAG+"Leave hull")
    self.pop_stack()

def _handle_loft(self, node):
    sid, line, args, block = node['id'], node['line'], node['args'], node.get('block', [])
    self.push_stack()
    if debug_parser:
        self.parse_block(block)
    else:
        self.push_context(SCLLoft, get_inc_name("loft"))
        self.parse_block(block)
        self.push_stack()
        self.parse_kwargs(sid, line, loft_module_definition['args'], args)
        solid = True
        ruled = True
        precision = 0.000001
        top = self.stack[-1]
        if top.has_variable('solid', line):
            solid = self.find_variable_value('solid', line)
        if top.has_variable('ruled', line):
            ruled = self.find_variable_value('ruled', line)
        if top.has_variable('precision', line):
            precision = self.find_variable_value('precision', line)
        debug(TAG+"Call builtin loft(%s, %s, %f)" % (str(solid), str(ruled), precision))
        self.active_context.loft(solid, ruled, precision)
        self.pop_stack()
        self.pop_context()
    debug(TAG+"Leave loft")
    self.pop_stack()

def _handle_profile(self, node):
    debug(TAG+"Profile")
    self.push_stack()
    if debug_parser:
        self.parse_block(node.get('block', []))
    else:
        self.push_context(SCLPart3, get_inc_name("profile"))
        self.push_context(SCLProfile2, get_inc_name("profile2"))
        self.parse_block(node.get('block', []))
        self.pop_context()
        self.active_context.add_profile_shape()
        self.pop_context()
    debug(TAG+"Leave profile")
    self.pop_stack()

def _handle_union(self, node):
    self.push_stack()
    if debug_parser:
        self.parse_block(node.get('block', []))
    else:
        self.push_context(SCLUnion, get_inc_name("union"))
        self.parse_block(node.get('block', []))
        self.active_context.union()
        self.pop_context()
    debug(TAG+"Leave union")
    self.pop_stack()

def _handle_group(self, node):
    self.push_stack()
    if debug_parser:
        self.parse_block(node.get('block', []))
    else:
        self.push_context(SCLGroup, get_inc_name("group"))
        self.parse_block(node.get('block', []))
        self.active_context.group()
        self.pop_context()
    debug(TAG+"Leave group")
    self.pop_stack()

def _handle_intersection(self, node):
    self.push_stack()
    if debug_parser:
        self.parse_block(node.get('block', []))
    else:
        self.push_context(SCLIntersection, get_inc_name("intersection"))
        self.parse_block(node.get('block', []))
        self.active_context.intersection()
        self.pop_context()
    debug(TAG+"Leave intersection")
    self.pop_stack()

def _handle_minkowski(self, node):
    sid, line, args, block = node['id'], node['line'], node['args'], node.get('block', [])
    self.push_stack()
    if debug_parser:
        self.parse_block(block)
    else:
        self.push_context(SCLMinkowski, get_inc_name("minkowski"))
        self.parse_block(block)
        self.push_stack()
        self.parse_kwargs(sid, line, minkowski_module_definition['args'], args)
        top = self.stack[-1]
        color_blend = top.get_variable_value('color_blend', line) if top.has_variable('color_blend', line) else False
        self.active_context.minkowski(color_blend)
        self.pop_stack()
        self.pop_context()
    debug(TAG+"Leave minkowski")
    self.pop_stack()

def _handle_offset(self, node):
    sid, line, args, block = node['id'], node['line'], node['args'], node.get('block', [])
    self.push_stack()
    if debug_parser:
        self.parse_block(block)
    else:
        self.push_context(SCLOffset, get_inc_name("offset"))
        self.parse_block(block)
        self.push_stack()
        self.parse_kwargs(sid, line, offset_module_definition['args'], args)
        top = self.stack[-1]
        r = top.get_variable_value('r', line) if top.has_variable('r', line) else None
        delta = top.get_variable_value('delta', line) if top.has_variable('delta', line) else None
        chamfer = top.get_variable_value('chamfer', line) if top.has_variable('chamfer', line) else False
        color_blend = top.get_variable_value('color_blend', line) if top.has_variable('color_blend', line) else False
        self.active_context.offset(r, delta, chamfer, color_blend)
        self.pop_stack()
        self.pop_context()
    debug(TAG+"Leave offset")
    self.pop_stack()

def _handle_difference(self, node):
    self.push_stack()
    if debug_parser:
        self.parse_block(node.get('block', []))
    else:
        self.push_context(SCLDifference, get_inc_name("difference"))
        self.parse_block(node.get('block', []))
        self.active_context.difference()
        self.pop_context()
    debug(TAG+"Leave difference")
    self.pop_stack()

def _handle_projection(self, node):
    self.push_stack()
    if debug_parser:
        self.parse_block(node.get('block', []))
    else:
        self.push_context(SCLProjection, get_inc_name("projection"))
        self.parse_block(node.get('block', []))
        self.active_context.projection()
        self.pop_context()
    debug(TAG+"Leave projection")
    self.pop_stack()

def _handle_rotate(self, node):
    sid, line, args, block = node['id'], node['line'], node['args'], node.get('block', [])
    debug(TAG+"Rotate")
    self.push_stack()
    v = [0, 0, 0]
    a = None
    non_kw_args = [a for a in args if a['type'] != 'expr_assign']
    if len(non_kw_args) >= 2:
        a_val = self.parse_expr(non_kw_args[0])
        v_val = self.parse_expr(non_kw_args[1])
        if isinstance(v_val, list):
            for i in range(min(3, len(v_val))):
                v[i] = v_val[i] if v_val[i] is not None else 0.0
        else:
            v[2] = v_val if v_val else 0.0
        a = a_val
    else:
        self.parse_kwargs(sid, line, rotate_module_definition['args'], args)
        top = self.stack[-1]
        if top.has_variable('v', line):
            v_ = self.find_variable_value('v', line)
            if isinstance(v_, list):
                ln = min(3, len(v_))
                for i in range(ln):
                    v[i] = v_[i] if v_[i] is not None else 0.0
            else:
                v[2] = v_ if v_ else 0.0
    self.push_context(SCLPart3, get_inc_name("rotate"))
    if debug_parser:
        self.parse_block(block)
    else:
        self.parse_block(block)
        debug(TAG+"Call builtin rotate(%f,%f,%f)" % (v[0], v[1], v[2]))
        if a is not None:
            self.active_context.rotate(a=a, v=v)
        else:
            self.active_context.rotate(v[0], v[1], v[2])
    debug(TAG+"Leave rotate [%s]" % (self.active_context.name))
    self.pop_context()
    self.pop_stack()

def _handle_display_wireframe(self, node):
    sid, line, args, block = node['id'], node['line'], node['args'], node.get('block', [])
    debug(TAG+"Display wireframe")
    self.push_stack()
    self.push_context(SCLPart3, get_inc_name("display_wireframe"))
    if debug_parser:
        self.parse_block(block)
    else:
        self.parse_block(block)
        debug(TAG+"Call builtin display_wireframe()")
        self.active_context.display_wireframe()
    debug(TAG+"Leave display_wireframe [%s]" % (self.active_context.name))
    self.pop_context()
    self.pop_stack()

def _handle_translate(self, node):
    sid, line, args, block = node['id'], node['line'], node['args'], node.get('block', [])
    debug(TAG+"translate")
    self.push_stack()
    self.parse_kwargs(sid, line, translate_module_definition['args'], args)
    top = self.stack[-1]
    v = [0, 0, 0]
    if top.has_variable('v', line):
        v_ = self.find_variable_value('v', line)
        ln = len(v) if len(v) < len(v_) else len(v_)
        for i in range(ln):
            v[i] = v_[i]
    self.push_context(SCLPart3, get_inc_name("translate"))
    if debug_parser:
        self.parse_block(block)
    else:
        self.parse_block(block)
        debug(TAG+"Call builtin translate(%f,%f,%f) line: %i [%s]" % (v[0], v[1], v[2], line, self.active_context.name))
        self.active_context.translate(v[0], v[1], v[2])
    debug(TAG+"Leave translate [%s]" % (self.active_context.name))
    self.pop_context()
    self.pop_stack()

def _handle_color(self, node):
    sid, line, args, block = node['id'], node['line'], node['args'], node.get('block', [])
    debug(TAG+"color")
    self.push_stack()
    self.parse_kwargs(sid, line, color_module_definition['args'], args)
    top = self.stack[-1]
    v = [0.0, 0.0, 0.0]
    alpha = 1.0
    if top.has_variable('v', line):
        v = self.find_variable_value('v', line)
    if top.has_variable('alpha', line):
        alpha_val = self.find_variable_value('alpha', line)
        if is_var_set(alpha_val):
            alpha = float(alpha_val)
    if isinstance(v, str):
        color_packet = [v, alpha]
    elif isinstance(v, list):
        color_packet = list(v)
        if len(color_packet) < 4:
            color_packet.append(alpha)
        elif top.has_variable('alpha', line):
            color_packet[3] = alpha
    else:
        color_packet = [v, alpha]
    self.push_context(SCLPart3, get_inc_name("color"))
    if debug_parser:
        self.parse_block(block)
    else:
        self.parse_block(block)
        debug(TAG+"Call builtin color(%s) line: %i [%s]" % (str(color_packet), line, self.active_context.name))
        self.active_context.color(color_packet)
    debug(TAG+"Leave color [%s]" % (self.active_context.name))
    self.pop_context()
    self.pop_stack()

def _handle_mirror(self, node):
    sid, line, args, block = node['id'], node['line'], node['args'], node.get('block', [])
    debug(TAG+"Mirror")
    self.push_stack()
    self.parse_kwargs(sid, line, mirror_module_definition['args'], args)
    top = self.stack[-1]
    v = [0, 0, 0]
    if top.has_variable('v', line):
        v_ = self.find_variable_value('v', line)
        ln = len(v) if len(v) < len(v_) else len(v_)
        for i in range(ln):
            v[i] = v_[i]
    self.push_context(SCLPart3, get_inc_name("mirror"))
    if debug_parser:
        self.parse_block(block)
    else:
        debug(TAG+"Call builtin mirror(%f, %f, %f)" % (v[0], v[1], v[2]))
        self.parse_block(block)
        self.active_context.mirror(v[0], v[1], v[2])
    debug(TAG+"Leave mirror")
    self.pop_context()
    self.pop_stack()

def _handle_scale(self, node):
    sid, line, args, block = node['id'], node['line'], node['args'], node.get('block', [])
    debug(TAG+"Scale")
    self.push_stack()
    self.parse_kwargs(sid, line, scale_module_definition['args'], args)
    top = self.stack[-1]
    v = [1, 1, 1]
    if top.has_variable('v', line):
        v_ = self.find_variable_value('v', line)
        if isinstance(v_, (int, float)):
            v = [float(v_), float(v_), float(v_)]
        else:
            ln = len(v) if len(v) < len(v_) else len(v_)
            for i in range(ln):
                v[i] = v_[i]
    self.push_context(SCLPart3, get_inc_name("scale"))
    if debug_parser:
        self.parse_block(block)
    else:
        debug(TAG+"Call builtin scale(%f, %f, %f)" % (v[0], v[1], v[2]))
        self.parse_block(block)
        self.active_context.scale(v[0], v[1], v[2])
    debug(TAG+"Leave scale")
    self.pop_context()
    self.pop_stack()

def _handle_line(self, node):
    sid, line, args = node['id'], node['line'], node['args']
    self.push_stack()
    num_pos = sum(1 for a in args if a.get('type') != 'expr_assign')
    if num_pos == 1:
        val = self.parse_expr(args[0])
        e = [0, 0, 0]
        ln = min(len(e), len(val))
        for i in range(ln):
            e[i] = val[i]
        debug(TAG+"Call builtin line(%s)" % (e,))
        if not debug_parser:
            self.active_context.line(V3(e[0], e[1], e[2]))
    else:
        self.parse_kwargs(sid, line, line_module_definition['args'], args)
        top = self.stack[-1]
        st = [0, 0, 0]
        e = [0, 0, 0]
        if top.has_variable('start', line):
            st_ = self.find_variable_value('start', line)
            ln = len(st) if len(st) < len(st_) else len(st_)
            for i in range(ln):
                st[i] = st_[i]
        if top.has_variable('end', line):
            e_ = self.find_variable_value('end', line)
            ln = len(e) if len(e) < len(e_) else len(e_)
            for i in range(ln):
                e[i] = e_[i]
        debug(TAG+"Call builtin line(%s->%s)" % (st, e))
        if not debug_parser:
            self.active_context.line(V3(st[0], st[1], st[2]), V3(e[0], e[1], e[2]))
    self.pop_stack()

def _handle_move(self, node):
    sid, line, args = node['id'], node['line'], node['args']
    self.push_stack()
    self.parse_kwargs(sid, line, move_module_definition['args'], args)
    top = self.stack[-1]
    p = [0, 0]
    if top.has_variable('p', line):
        p_ = self.find_variable_value('p', line)
        ln = min(len(p), len(p_))
        for i in range(ln):
            p[i] = p_[i]
    debug(TAG+"Call builtin move(%s)" % (p,))
    if not debug_parser:
        self.active_context.move(V3(p[0], p[1], 0))
    self.pop_stack()

def _handle_arc(self, node):
    sid, line, args = node['id'], node['line'], node['args']
    self.push_stack()
    self.parse_kwargs(sid, line, arc_module_definition['args'], args)
    top = self.stack[-1]
    center = [0, 0]
    angle = 90.0
    if top.has_variable('center', line):
        c_ = self.find_variable_value('center', line)
        ln = min(len(center), len(c_))
        for i in range(ln):
            center[i] = c_[i]
    if top.has_variable('angle', line):
        angle = self.find_variable_value('angle', line)
    debug(TAG+"Call builtin arc(center=%s, angle=%g)" % (center, angle))
    if not debug_parser:
        self.active_context.arc(V3(center[0], center[1], 0), angle)
    self.pop_stack()

def _handle_spline(self, node):
    sid, line, args = node['id'], node['line'], node['args']
    self.push_stack()
    self.parse_kwargs(sid, line, spline_module_definition['args'], args)
    top = self.stack[-1]
    pts = []
    if top.has_variable('points', line):
        pts_ = self.find_variable_value('points', line)
        pts = [V3(p[0], p[1], 0) for p in pts_]
    debug(TAG+"Call builtin spline(%i points)" % (len(pts),))
    if not debug_parser:
        self.active_context.spline(pts)
    self.pop_stack()

def _handle_close(self, node):
    sid, line, args = node['id'], node['line'], node['args']
    self.push_stack()
    self.parse_kwargs(sid, line, close_module_definition['args'], args)
    top = self.stack[-1]
    fillet_r = None
    if top.has_variable('fillet', line):
        fillet_r = self.find_variable_value('fillet', line)
    debug(TAG+"Call builtin close(fillet=%s)" % (str(fillet_r),))
    fillet_val = fillet_r if is_var_set(fillet_r) else None
    if not debug_parser:
        self.active_context.close(fillet_val)
    self.pop_stack()

def _handle_fillet(self, node):
    sid, line, args = node['id'], node['line'], node['args']
    self.push_stack()
    self.parse_kwargs(sid, line, fillet_module_definition['args'], args)
    top = self.stack[-1]
    r = 1.0
    if top.has_variable('r', line):
        r = self.find_variable_value('r', line)
    debug(TAG+"Call builtin fillet(%f)" % (r,))
    if not debug_parser:
        self.active_context.fillet(r)
    self.pop_stack()

def _handle_polygon(self, node):
    sid, line, args = node['id'], node['line'], node['args']
    self.push_stack()
    self.parse_kwargs(sid, line, polygon_module_definition['args'], args)
    top = self.stack[-1]
    points = None
    paths = None
    if top.has_variable('points', line):
        points = self.find_variable_value('points', line)
    if top.has_variable('paths', line):
        paths = self.find_variable_value('paths', line)
    debug(TAG+"Call builtin polygon(%s, %s) line: %i" % (str(points), str(paths), line))
    if not debug_parser:
        self.push_context(SCLPart3, get_inc_name("part"))
        try:
            self.active_context.polygon(points, paths, sid, line)
        except BaseException as e:
            warning(str(e))
        self.pop_context()
    self.pop_stack()

def _handle_square(self, node):
    sid, line, args = node['id'], node['line'], node['args']
    self.push_stack()
    self.parse_kwargs(sid, line, square_module_definition['args'], args)
    top = self.stack[-1]
    size = [1.0, 1.0]
    center = False
    if top.has_variable('size', line):
        v_ = self.find_variable_value('size', line)
        if type(v_) is list:
            ln = min(len(size), len(v_))
            for i in range(ln):
                size[i] = v_[i]
        else:
            size = [v_, v_]
    if top.has_variable('center', line):
        center = self.find_variable_value('center', line)
    debug(TAG+"Call builtin square(size=%s, center=%s)" % (str(size), str(center)))
    if not debug_parser:
        x0, y0 = (0.0, 0.0) if not center else (-size[0]/2.0, -size[1]/2.0)
        x1, y1 = x0 + size[0], y0 + size[1]
        pts = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]]
        self.push_context(SCLPart3, get_inc_name("part"))
        self.active_context.polygon(pts, [0, 1, 2, 3], sid, line)
        self.pop_context()
    self.pop_stack()

def _handle_polyhedron(self, node):
    sid, line, args = node['id'], node['line'], node['args']
    self.push_stack()
    self.parse_kwargs(sid, line, polyhedron_module_definition['args'], args)
    top = self.stack[-1]
    points = []
    faces = []
    if top.has_variable('points', line):
        points = self.find_variable_value('points', line)
    if top.has_variable('faces', line):
        faces = self.find_variable_value('faces', line)
    elif top.has_variable('triangles', line):
        faces = self.find_variable_value('triangles', line)
    debug(TAG+"Call builtin polyhedron(points=%s, faces=%s)" % (str(points), str(faces)))
    if not debug_parser:
        self.active_context.polyhedron(points, faces, sid, line)
    self.pop_stack()

def _handle_cube(self, node):
    sid, line, args = node['id'], node['line'], node['args']
    self.push_stack()
    self.parse_kwargs(sid, line, cube_module_definition['args'], args)
    top = self.stack[-1]
    v = [1.0, 1.0, 1.0]
    center = False
    if top.has_variable('v', line):
        v_ = self.find_variable_value('v', line)
        if isinstance(v_, list):
            ln = min(len(v), len(v_))
            for i in range(ln):
                v[i] = v_[i]
        else:
            v = [v_, v_, v_]
    if top.has_variable('center', line):
        center = self.find_variable_value('center', line)
    debug(TAG+"Call builtin cube(%s, %s) line: %i" % (str(v), str(center), line))
    if not debug_parser:
        self.active_context.cube(V3(v[0], v[1], v[2]), center)
    self.pop_stack()

def _handle_cylinder(self, node):
    sid, line, args = node['id'], node['line'], node['args']
    self.push_stack()
    self.parse_kwargs(sid, line, cylinder_module_definition['args'], args)
    top = self.stack[-1]
    r = None
    d = None
    h = None
    r1 = None
    r2 = None
    d1 = None
    d2 = None
    center = False
    if top.has_variable('center', line):
        center = self.find_variable_value('center', line)
    if top.has_variable('r', line):
        r = self.find_variable_value('r', line)
    if top.has_variable('r1', line):
        r1 = self.find_variable_value('r1', line)
    if top.has_variable('r2', line):
        r2 = self.find_variable_value('r2', line)
    if top.has_variable('d', line):
        d = self.find_variable_value('d', line)
    if top.has_variable('d1', line):
        d1 = self.find_variable_value('d1', line)
    if top.has_variable('d2', line):
        d2 = self.find_variable_value('d2', line)
    if top.has_variable('h', line):
        h = self.find_variable_value('h', line)
    fn = self.find_variable_value('$fn', line)
    debug(TAG+"Call builtin cylinder(r=%s, d=%s, h=%s, center=%s, $fn=%s) line: %i" % (str(r), str(d), str(h), str(center), str(fn), line))
    if not debug_parser:
        self.active_context.cylinder(r, r1, r2, d, d1, d2, h, center, fn=fn)
    self.pop_stack()

def _handle_sphere(self, node):
    sid, line, args = node['id'], node['line'], node['args']
    self.push_stack()
    self.parse_kwargs(sid, line, sphere_module_definition['args'], args)
    top = self.stack[-1]
    r = None
    d = None
    if top.has_variable('r', line):
        r = self.find_variable_value('r', line)
    if top.has_variable('d', line):
        d = self.find_variable_value('d', line)
    debug(TAG+"Call builtin sphere(r=%s, d=%s) line: %i" % (str(r), str(d), line))
    if not debug_parser:
        self.active_context.sphere(r, d)
    self.pop_stack()

def _handle_circle(self, node):
    sid, line, args = node['id'], node['line'], node['args']
    self.push_stack()
    self.parse_kwargs(sid, line, circle_module_definition['args'], args)
    top = self.stack[-1]
    r = None
    d = None
    if top.has_variable('r', line):
        r = self.find_variable_value('r', line)
    if top.has_variable('d', line):
        d = self.find_variable_value('d', line)
    debug(TAG+"Call builtin circle(r=%s, d=%s) line: %i" % (str(r), str(d), line))
    if not debug_parser:
        self.push_context(SCLPart3, get_inc_name("part"))
        self.active_context.circle(r, d)
        self.pop_context()
    self.pop_stack()

def _handle_import(self, node):
    sid, line, args = node['id'], node['line'], node['args']
    self.push_stack()
    self.parse_kwargs(sid, line, import_module_definition['args'], args)
    top = self.stack[-1]
    path = ""
    if top.has_variable('filename', line):
        path = self.find_variable_value('filename', line)
    debug(TAG+"Call builtin import(%s) line: %i" % (str(path), line))
    if not debug_parser:
        base_paths = [os.getcwd(), os.path.dirname(os.path.abspath(self.path))]
        paths = [os.path.join(bp, path) for bp in base_paths]
        file_path = None
        for p in paths:
            if os.path.isfile(p):
                debug(TAG+"Using path %s" % (p,))
                file_path = p
                break
        if file_path is not None:
            self.active_context.import_file(file_path)
        else:
            warning("Failed to load file %s: file not found" % (path,))
    self.pop_stack()

def _handle_echo_stmt(self, node):
    args_val = [self.parse_expr(a) for a in node['args']]
    _echo_output(args_val, node['line'])

def _handle_assert_stmt(self, node):
    args_val = [self.parse_expr(a) for a in node['args']]
    _raise_assert(args_val, node['line'])

def _handle_children(self, node):
    sid, line, args = node['id'], node['line'], node['args']
    try:
        block = self.find_variable_value('$children_block', line)
    except UnknownVariableError:
        block = None
    if block is None:
        warning("%s:%i Warning: No children to render" % (self.path, line))
        return
    idx = None
    if args:
        idx_val = self.parse_expr(args[0])
        if isinstance(idx_val, list):
            idx = idx_val
        else:
            idx = int(idx_val)
    has_own_block = self.stack[-1].has_variable('$children_block', line)
    own_block = None
    if has_own_block:
        own_block = self.stack[-1].get_variable_value('$children_block', line)
        del self.stack[-1].variables['$children_block']
    try:
        if self._should_group():
            self.push_context(SCLGroup, get_inc_name("group"))
            try:
                _exec_children(self, block, idx)
            finally:
                self.active_context.group()
                self.pop_context()
        else:
            _exec_children(self, block, idx)
    finally:
        if has_own_block:
            self.stack[-1].set_variable('$children_block', own_block, line)

def _exec_children(scl, block, idx):
    if idx is None:
        for stmt in block:
            scl.parse_statement(stmt)
    elif isinstance(idx, list):
        for i in idx:
            i_int = int(i)
            if 0 <= i_int < len(block):
                scl.parse_statement(block[i_int])
    else:
        if 0 <= idx < len(block):
            scl.parse_statement(block[idx])

# 'handler' signature: handler(self, node) -> None
# 'params_def' for future autocomplete: reference to the *_module_definition dict
# 'has_block': True for modules that take a child block (CSG, transforms, profile)

_builtin_modules: dict[str, dict] = {
    'linear_extrude': {
        'handler': _handle_linear_extrude,
        'params_def': linear_extrude_module_definition,
        'has_block': True,
    },
    'rotate_extrude': {
        'handler': _handle_rotate_extrude,
        'params_def': rotate_extrude_module_definition,
        'has_block': True,
    },
    'group': {
        'handler': _handle_group,
        'params_def': group_module_definition,
        'has_block': True,
    },
    'hull': {
        'handler': _handle_hull,
        'params_def': hull_module_definition,
        'has_block': True,
    },
    'loft': {
        'handler': _handle_loft,
        'params_def': loft_module_definition,
        'has_block': True,
    },
    'profile': {
        'handler': _handle_profile,
        'params_def': profile_module_definition,
        'has_block': True,
    },
    'union': {
        'handler': _handle_union,
        'params_def': union_module_definition,
        'has_block': True,
    },
    'intersection': {
        'handler': _handle_intersection,
        'params_def': intersection_module_definition,
        'has_block': True,
    },
    'minkowski': {
        'handler': _handle_minkowski,
        'params_def': minkowski_module_definition,
        'has_block': True,
    },
    'offset': {
        'handler': _handle_offset,
        'params_def': offset_module_definition,
        'has_block': True,
    },
    'difference': {
        'handler': _handle_difference,
        'params_def': difference_module_definition,
        'has_block': True,
    },
    'projection': {
        'handler': _handle_projection,
        'params_def': projection_module_definition,
        'has_block': True,
    },
    'rotate': {
        'handler': _handle_rotate,
        'params_def': rotate_module_definition,
        'has_block': True,
    },
    'display_wireframe': {
        'handler': _handle_display_wireframe,
        'params_def': display_wireframe_module_definition,
        'has_block': True,
    },
    'translate': {
        'handler': _handle_translate,
        'params_def': translate_module_definition,
        'has_block': True,
    },
    'color': {
        'handler': _handle_color,
        'params_def': color_module_definition,
        'has_block': True,
    },
    'mirror': {
        'handler': _handle_mirror,
        'params_def': mirror_module_definition,
        'has_block': True,
    },
    'scale': {
        'handler': _handle_scale,
        'params_def': scale_module_definition,
        'has_block': True,
    },
    'line': {
        'handler': _handle_line,
        'params_def': line_module_definition,
        'has_block': False,
    },
    'move': {
        'handler': _handle_move,
        'params_def': move_module_definition,
        'has_block': False,
    },
    'arc': {
        'handler': _handle_arc,
        'params_def': arc_module_definition,
        'has_block': False,
    },
    'spline': {
        'handler': _handle_spline,
        'params_def': spline_module_definition,
        'has_block': False,
    },
    'close': {
        'handler': _handle_close,
        'params_def': close_module_definition,
        'has_block': False,
    },
    'fillet': {
        'handler': _handle_fillet,
        'params_def': fillet_module_definition,
        'has_block': False,
    },
    'polygon': {
        'handler': _handle_polygon,
        'params_def': polygon_module_definition,
        'has_block': False,
    },
    'square': {
        'handler': _handle_square,
        'params_def': square_module_definition,
        'has_block': False,
    },
    'polyhedron': {
        'handler': _handle_polyhedron,
        'params_def': polyhedron_module_definition,
        'has_block': False,
    },
    'cube': {
        'handler': _handle_cube,
        'params_def': cube_module_definition,
        'has_block': False,
    },
    'cylinder': {
        'handler': _handle_cylinder,
        'params_def': cylinder_module_definition,
        'has_block': False,
    },
    'sphere': {
        'handler': _handle_sphere,
        'params_def': sphere_module_definition,
        'has_block': False,
    },
    'circle': {
        'handler': _handle_circle,
        'params_def': circle_module_definition,
        'has_block': False,
    },
    'import': {
        'handler': _handle_import,
        'params_def': import_module_definition,
        'has_block': False,
    },
    'echo': {
        'handler': _handle_echo_stmt,
        'params_def': None,
        'has_block': False,
    },
    'assert': {
        'handler': _handle_assert_stmt,
        'params_def': None,
        'has_block': False,
    },
    'children': {
        'handler': _handle_children,
        'params_def': None,
        'has_block': False,
    },
}

class SCL:
    def __init__(self, data=None, path=None, output_path=None, verbose=3, extra={}):
        self.verbose = verbose
        if verbose == 0:
            logging.getLogger(None).setLevel(logging.CRITICAL)
        elif verbose == 1:
            logging.getLogger(None).setLevel(logging.ERROR)
        elif verbose == 2:
            logging.getLogger(None).setLevel(logging.WARNING)
        elif verbose == 3:
            logging.getLogger(None).setLevel(logging.INFO)
        elif verbose >= 4:
            logging.getLogger(None).setLevel(logging.DEBUG)
        # create formatter and add it to the handlers
        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
        ch = logging.StreamHandler()
        ch.setLevel(logging.DEBUG)
        ch.setFormatter(formatter)
        logging.getLogger(None).handlers = []
        logging.getLogger(None).addHandler(ch)
        debug(TAG+'SCL data: %s, path: %s'%(data, path))
        self.extra = extra
        self.output_path = output_path
        self.data = data
        if self.output_path is None:
            ep.push_event(ee.init_display, None)
            ep.process()
        self.stack = [SCLFrame()]
        self.init_global_constants()

        self.debug=False

        self.path = path
        self.objects = None

    def init_global_constants(self):
        """ Инициализирует глобальные константы OpenSCAD """
        root = self.stack[0]
        root.set_variable('PI', math.pi, 0)
        root.set_variable('$fn', 0.0, 0)   # 0.0 означает "не задан" (вычисляется через $fa/$fs)
        root.set_variable('$fa', 12.0, 0)  # Минимальный угол сегмента в градусах
        root.set_variable('$fs', 2.0, 0)   # Минимальный размер сегмента в мм
        root.set_variable('$fe', 0.5, 0)   # Максимальное отклонение хорды в мм

    @staticmethod
    def _find_root_stmt(block):
        """Находит самый глубокий модификатор ! (root) в block и его потомках.
        Возвращает только поддерево помеченного ! оператора,
        отбрасывая все родительские обёртки (translate, rotate, color и т.д.).
        Если ! не найден — возвращает None."""
        best = SCL._find_deepest_root(block, 0)
        return best[0] if best else None

    @staticmethod
    def _find_deepest_root(block, depth):
        """Внутренняя: ищет самый глубокий ! в block на заданной depth.
        Возвращает (inner_list, depth) или None."""
        best_node = None
        best_depth = -1

        for stmt in block:
            if stmt.get('type') == 'stat_modifier':
                inner = stmt.get('statement')
                if inner:
                    candidate = SCL._find_deepest_root([inner], depth + 1)
                    if candidate:
                        if candidate[1] > best_depth:
                            best_node = candidate[0]
                            best_depth = candidate[1]
                    elif stmt.get('id') == 'root':
                        best_node = [inner]
                        best_depth = depth

            if 'block' in stmt:
                candidate = SCL._find_deepest_root(stmt['block'], depth + 1)
                if candidate and candidate[1] > best_depth:
                    best_node = candidate[0]
                    best_depth = candidate[1]

        if best_node is not None:
            return (best_node, best_depth)
        return None

    def reload_file(self):
        self.stack = [SCLFrame()]
        self.init_global_constants()
        self.load_file()

    def reload_from_data(self, data):
        self.data = data
        self.reload_file()

    def load_file(self):
        debug(TAG+"Load file %s"%(self.path,))

        if self.path is None:
            if self.output_path is not None:
                warning("No file specified, starting empty")
            self.root = SCLPart3(None)
            run_parser = self.data is not None
        else:
            filename, ext = os.path.splitext(self.path)
            run_parser = True
            if ext == '.sck':
                self.root = SCLProfile2(None)
            elif ext in ('.scp', '.scad'):
                self.root = SCLPart3(None)
            elif ext in ('.stp', '.step', '.STP', '.STEP'):
                self.root = SCLPart3(None)
                self.root.import_file(self.path)
                run_parser = False
            else:
                warning("Unknown file type %s"%(ext,))
                self.root = SCLPart3(None)

        self.root.set_name("root")
        self.context = self.root
        self.active_context = self.context

        if run_parser:
            slp = None
            try:
                slp = ScadLikeParser(data=self.data, path=self.path, debug=self.debug)
            except ParserError as pe:
                critical("%s:%i Unknown expression: %s"%(self.path, pe.lineno, pe.message))
                if self.verbose >= 4:
                    traceback.print_exc()
            else:
                debug(TAG+json.dumps(slp.data, indent=2))
                root_ast = self._find_root_stmt(slp.data)
                if root_ast:
                    statements = list(root_ast)
                    side_effects = []
                    for s in slp.data:
                        if s.get('type') in ('stat_assign', 'stat_module_definition',
                                              'stat_function_definition', 'use', 'include'):
                            side_effects.append(s)
                    statements = side_effects + statements
                else:
                    statements = slp.data
                try:
                    reset_cancel()
                    init_progress(max(len(statements), 1))
                    for s in statements:
                        check_cancel()
                        self.parse_statement(s)
                        advance_progress()
                except StopExecution:
                    debug(TAG+"Execution cancelled")
                    self.root = SCLPart3(None)
                except AssertionError as ae:
                    critical("%s:%i %s" % (self.path, ae.line, ae.message))

        if self.output_path is not None:
            self.export_geometry(self.output_path)
            self.objects = None
        else:
            debug(TAG+"Display context")
            self.objects = self.context.display(None)
            debug(TAG+"Objects tree")
            debug(self.objects)

    def make_writer(self, filepath, fmt=None):
        EXT_TO_FMT = {'.step': 'step', '.stp': 'step', '.stl': 'stl', '.dxf': 'dxf'}
        FMT_TO_CLS = {'step': SCLSTEPWriter, 'stl': SCLSTLWriter, 'dxf': SCLDXFWriter}

        if fmt is None:
            ext = os.path.splitext(filepath)[1].lower()
            fmt = EXT_TO_FMT.get(ext)
            if fmt is None:
                critical(f"Unsupported output format: {filepath}")
                return None, filepath

        cls = FMT_TO_CLS.get(fmt)
        if cls is None:
            critical(f"Unsupported output format: {fmt}")
            return None, filepath

        _, ext = os.path.splitext(filepath)
        if ext.lower() not in cls.extensions:
            filepath += cls.extensions[0]

        writer = cls()
        if cls is SCLSTLWriter:
            fa = float(self.find_variable_value('$fa', 0) or 12.0)
            fe = float(self.find_variable_value('$fe', 0) or 0.5)
            writer.set_resolution(
                fe if fe > 0 else 0.5,
                math.radians(fa) if fa > 0 else 0.5,
            )
        return writer, filepath


    def export_geometry(self, filepath, fmt=None):
        writer, filepath = self.make_writer(filepath, fmt)
        if writer is None:
            return False
        self.context.display(writer)
        if not self.context.children:
            critical("No geometry to export")
            return False
        writer.Write(filepath)
        return True

    def push_context(self, ctx, name):
        n = ctx(self.active_context)
        n.set_name(name)
        self.active_context.add_child_context(n)
        self.active_context = n

    def pop_context(self):
        self.active_context = self.active_context.parent

    def _should_group(self):
        ctx = self.active_context
        while ctx is not None:
            if isinstance(ctx, SCLProfile2):
                return False
            ctx = ctx.parent
        return True

    def find_variable_value(self, varname, line, print_function=debug):
        val = None
        print_function(TAG+'SCL:%i:Looking for variable %s'%(line, varname,))

        for f in reversed(self.stack):
            try:
                val = f.get_variable_value(varname, line)
            except Exception:
                pass
            else:
                print_function("Found value %s"%(val,))
                return val
        raise UnknownVariableError(varname, line)


    def set_variable(self, varname, value, line):
        debug(TAG+'SCL:%i:Setting variable %s: %s'%(line, varname, value))
        top = self.stack[-1]
        top.set_variable(varname, value, line)

    def find_function(self, fname, line):
        val = None
        debug(TAG+'SCL:%i:Looking for function %s'%(line, fname,))

        for f in reversed(self.stack):
            try:
                val = f.get_function(fname, line)
            except Exception:
                pass
            else:
                debug(TAG+"Found function %s"%(fname,))
                return val
        raise UnknownFunctionError(fname, line)

    def set_function(self, fname, value, line):
        debug(TAG+'SCL:%i:Setting function %s'%(line, fname))
        top = self.stack[-1]
        top.set_function(fname, value, line)

    def find_module(self, mname, line):
        val = None
        debug(TAG+'SCL:%i:Looking for module %s'%(line, mname,))

        for f in reversed(self.stack):
            try:
                val = f.get_module(mname, line)
            except Exception:
                pass
            else:
                debug(TAG+"Found module %s"%(mname,))
                return val
        raise UnknownModuleError(mname, line)

    def set_module(self, mname, value, line):
        debug(TAG+'SCL:%i:Setting module %s'%(line, mname))
        top = self.stack[-1]
        top.set_module(mname, value, line)

    def find_callable(self, callable_name, line):
        cbl = None
        try:
            cbl = self.find_module(callable_name, line)
        except Exception:
            try:
                cbl = self.find_function(callable_name, line)
            except Exception:
                raise RuntimeError("No callable with name %s"%(callable_name), line)
        return cbl

    def push_stack(self):
        self.stack.append(SCLFrame())

    def pop_stack(self):
        self.stack.pop()

    def parse_expr(self, e):
        debug_expr('Parse expr %s'%(e,))
        if 'type' not in e:
            raise UnhandledCaseError('type is not set: %s'%(e,))
        t = e['type']
        debug_expr("%i:%s"%(e['line'],t))
        if t=='expr_number':
            return e['val']
        if t=='expr_string':
            return e['val']
        elif t=='expr_bool':
            return e['val']
        elif t=='expr_assign':
            val = self.parse_expr(e['val'])
            self.set_variable(e['id'], val, e['line'])
            return val
        elif t=='expr_binop':
            e1 = e['val'][0]
            e2 = e['val'][1]
            op = e['val'][2]
            e1num=self.parse_expr(e1)
            e2num=self.parse_expr(e2)
            debug_expr('%s %s %s'%(e1num, op, e2num))

            if e1num is None or e2num is None:
                if op == '==':
                    return (e1num is None and e2num is None)
                elif op == '!=':
                    return (e1num is None) != (e2num is None)
                else:
                    warning("%s:%i Warning: undefined operation (%s %s %s)" % (self.path, e['line'], scl_str(e1num), op, scl_str(e2num)))
                    return None
            # --------------------------------------------------------------------------------------------

            try:
                if op == '+':
                    return e1num+e2num
                elif op == '-':
                    return e1num-e2num
                elif op == '*':
                    return e1num*e2num
                elif op == '/':
                    return e1num/e2num
                elif op == '%':
                    return e1num%e2num
            except (TypeError, ZeroDivisionError):
                warning("%s:%i Warning: invalid operands %s %s %s" % (self.path, e['line'], scl_str(e1num), op, scl_str(e2num)))
                return None
            if op == '==':
                debug(TAG+"Compare %s == %s -> %s"%((e1num), str(e2num), str(e1num==e2num)))
                return e1num==e2num
            elif op == '!=':
                return e1num!=e2num
            elif op == '<':
                return e1num<e2num
            elif op == '>':
                return e1num>e2num
            elif op == '>=':
                return e1num>=e2num
            elif op == '<=':
                return e1num<=e2num
            elif op == '&&':
                return e1num and e2num
            elif op == '||':
                return e1num or e2num
            else:
                raise UnhandledCaseError('unknown operation %s at line %i'%(op,e['line']))
        elif e['type']=='expr_unop':
            ev=e['val'][0]
            op=e['val'][1] if len(e['val']) > 1 else '-'
            en=self.parse_expr(ev)

            if en is None:
                return None

            if op == '!':
                return not en
            if (isinstance(en, list)):
                try:
                    return [-v for v in en]
                except TypeError:
                    warning("%s:%i Warning: can't negate list with non-numeric elements" % (self.path, e['line']))
                    return None
            try:
                return -en
            except TypeError:
                warning("%s:%i Warning: can't negate %s" % (self.path, e['line'], scl_str(en)))
                return None
        elif t=='expr_terop':
            condition = e['val']['condition']
            true_branch = e['val']['true']
            false_branch = e['val']['false']
            condition=self.parse_expr(condition)
            result = None
            if scl_bool(condition):
                result=self.parse_expr(true_branch)
            else:
                result=self.parse_expr(false_branch)
            return result
        elif e['type']=='expr_id':
            ev = e['val']
            # --- ИСПРАВЛЕННЫЙ БЛОК: Неизвестная переменная возвращает undef ---
            try:
                return self.find_variable_value(ev, e['line'], debug_expr)
            except UnknownVariableError:
                warning("%s:%i Warning: Unknown variable '%s' evaluated to undef" % (self.path, e['line'], ev))
                return None
            # ------------------------------------------------------------------
        elif e['type']=='array_list':
            arr = []
            for ev in e['val']:
                arr.append(self.parse_expr(ev))
            return arr
        elif e['type']=='array_range':
            step = 1.0
            if 'step' in e['val']:
                step = self.parse_expr(e['val']['step'])
            start = self.parse_expr(e['val']['start'])
            end = self.parse_expr(e['val']['end'])
            if ((end>=start) and (step>0)) or ((end<=start) and (step<0)):
                v = start
                arr = [v]
                diff = v-end
                pdiff = diff
                while (True):
                    v+=step
                    diff = v-end
                    if (diff!=0) and (sign(diff)!=sign(pdiff)):
                        break
                    arr.append(v)
                    pdiff = diff
            else:
                arr = []
                
            debug(TAG+"arr: %s"%(str(arr),))
            return arr

        elif e['type']=='expr_call':
            self.push_stack()
            fid = e['id']
            args_val = [self.parse_expr(arg) for arg in e['args']]

            entry = _builtin_funcs.get(fid)
            if entry is not None:
                ret = entry['handler'](self, args_val, e)
            else:
                self.pop_stack()
                try:
                    cbl = self.find_function(e['id'], e['line'])
                except UnknownFunctionError:
                    warning("%s:%i Warning: Unknown function '%s' evaluated to undef" % (self.path, e['line'], e['id']))
                    return None
                self.push_stack()
                self.parse_kwargs(e['id'], e['line'], cbl['args'], e['args'])
                ret = None
                for expr_node in cbl['body']:
                    ret = self.parse_expr(expr_node)

            self.pop_stack()
            return ret

        elif e['type']=='array_access':
            if isinstance(e['id'], str):
                val = self.find_variable_value(e['id'], e['line'], debug_expr)
            else:
                val = self.parse_expr(e['id']) # Рекурсивный вызов для вложенных массивов

            idx = self.parse_expr(e['index'])

            # --- БЕЗОПАСНЫЙ ИНДЕКСНЫЙ ДОСТУП ---
            try:
                # В OpenSCAD индексировать можно как списки (list), так и строки (str) посимвольно
                if isinstance(val, (list, str)):
                    i = int(idx)
                    # Проверяем, укладывается ли индекс в границы массива (включая отрицательные индексы с конца)
                    if 0 <= i < len(val) or -len(val) <= i < 0:
                        return val[i]
                    else:
                        warning("%s:%i Warning: Array index %i out of bounds [0..%i]" % (self.path, e['line'], i, len(val)-1))
                        return None
                else:
                    warning("%s:%i Warning: Attempt to index a non-array variable (%s)" % (self.path, e['line'], scl_str(val)))
                    return None
            except (TypeError, ValueError, IndexError) as ex:
                warning("%s:%i Warning: Invalid array access: %s" % (self.path, e['line'], str(ex)))
                return None
            # ------------------------------------
        elif e['type']=='expr_array_for':
            counter_vals = self.parse_expr(e['counter_vals'])
            if isinstance(counter_vals, str):
                counter_vals = list(counter_vals)
            elif not isinstance(counter_vals, list):
                warning("%s:%i Warning: for range is not a list" % (self.path, e['line']))
                return []
            result = []
            body = e['body']
            counter = e['counter']
            flt = e.get('filter')
            else_body = e.get('else_body')
            # Раскрываем expr_array_for_filter, чтобы определить вложенность
            raw_body = body
            while isinstance(raw_body, dict) and raw_body.get('type') == 'expr_array_for_filter':
                raw_body = raw_body['body']
            is_nested = isinstance(raw_body, dict) and raw_body.get('type') == 'expr_array_for'
            for val in counter_vals:
                self.push_stack()
                top = self.stack[-1]
                top.set_variable(counter, val, e['line'])
                if flt is not None:
                    cond = self.parse_expr(flt)
                    if scl_bool(cond):
                        val = self.parse_expr(body)
                    elif else_body is not None:
                        val = self.parse_expr(else_body)
                    else:
                        val = None
                else:
                    val = self.parse_expr(body)
                if val is not None:
                    if is_nested:
                        result.extend(val)
                    else:
                        result.append(val)
                self.pop_stack()
            return result

        elif e['type']=='expr_array_for_filter':
            if scl_bool(self.parse_expr(e['filter'])):
                return self.parse_expr(e['body'])
            elif e.get('else_body') is not None:
                return self.parse_expr(e['else_body'])
            else:
                return None

        elif e['type']=='expr_let':
            self.push_stack()
            for var in e['vars']:
                if isinstance(var, dict) and var.get('type') == 'expr_assign':
                    val = self.parse_expr(var['val'])
                    top = self.stack[-1]
                    top.set_variable(var['id'], val, e['line'])
            ret = self.parse_expr(e['body'])
            self.pop_stack()
            return ret

        else:
            raise UnhandledCaseError('unknown expression type \'%s\''%(t,))
        raise UnhandledCaseError('unknown error')

    def check_kwargs(self, args):
        prev_arg = None
        debug(TAG+'args: %s'%(str(args),))
        for a in args:
            if prev_arg is not None:
                if (prev_arg['type'] == 'expr_assign') and (a['type'] != 'expr_assign'):
                    # Безопасно выводим предупреждение вместо падения
                    warning("%s:%i Warning: positional argument follows keyword argument" % (self.path, a.get('line', 0)))
            prev_arg = a

    def check_kwargs_definition(self, args_definition):
        prev_arg = None
        debug(TAG+"args definition: %s"%(str(args_definition), ))
        for a in args_definition:
            debug(TAG+"Checking argument %s"%(a,))
            if prev_arg is not None:
                if (prev_arg['type'] == 'expr_assign') and (a['type'] == 'expr_name'):
                    # Безопасно выводим предупреждение вместо падения
                    warning("%s:%i Warning: positional argument follows keyword argument in definition" % (self.path, a.get('line', 0)))
                prev_arg = a
            if (a['type'] != 'expr_assign') and (a['type'] != 'expr_id'):
                # Безопасно выводим предупреждение вместо падения
                warning("%s:%i Warning: illegal argument definition of type '%s'" % (self.path, a.get('line', 0), a['type']))
        self.check_kwargs(args_definition)

    def get_args_list(self, args_definition):
        def_vals = {}
        arg_list = []
        positional = 0
        debug(TAG+'args_deginition: %s'%(str(args_definition),))
        for a in args_definition:
            defval = None
            argid = None
            if (a['type']!='expr_id'):
                defval = self.parse_expr(a['val'])
                argid = a['id']
            else:
                positional += 1
                argid = a['val']
            def_vals[argid] = defval
            arg_list.append(argid)
        return def_vals, arg_list, positional


    def parse_kwargs(self, name, line, args_definition, args, print_function=debug):
        print_function('kwargs args: %s'%(str(args),))
        self.check_kwargs_definition(args_definition)
        self.check_kwargs(args)
        def_vals, arg_list, positional = self.get_args_list(args_definition)
        print_function('defvals: %s, arglist: %s, positional: %i'%(def_vals, arg_list, positional))

        # --- ЗАМЕНЯЕМ ЖЕСТКИЕ ИСКЛЮЧЕНИЯ НА БЕЗОПАСНЫЕ ПРЕДУПРЕЖДЕНИЯ (WARNINGS) ---
        if (positional > len(args)):
            warning("%s:%i Warning: %s takes %i positional arguments, but %i arguments supplied" % (self.path, line, name, positional, len(args)))
        if (len(args) > len(args_definition)):
            warning("%s:%i Warning: %s takes %i arguments, but %i arguments supplied" % (self.path, line, name, len(args_definition), len(args)))
        # --------------------------------------------------------------------------

        new_frame = self.stack.pop()

        evaluated_args = []
        position = 0
        for a in args:
            if a['type'] != 'expr_assign':
                # Позиционный аргумент: проверяем, не вышли ли мы за пределы сигнатуры
                if position < len(arg_list):
                    arg_id = arg_list[position]
                    val = self.parse_expr(a)
                else:
                    # Если передано больше позиционных аргументов, чем принимает модуль, тихо игнорируем их
                    warning("%s:%i Warning: Too many positional arguments passed to %s. Ignoring extra argument." % (self.path, a['line'] if 'line' in a else line, name))
                    position += 1
                    continue
            else:
                # Именованный аргумент
                arg_id = a['id']
                val = self.parse_expr(a['val'])

            # Проверяем, соответствует ли имя аргумента сигнатуре
            if arg_id not in arg_list:
                warning("%s:%i Warning: variable '%s' not specified as parameter in %s" % (self.path, a['line'] if 'line' in a else line, arg_id, name))

            evaluated_args.append((arg_id, val, a['line'] if 'line' in a else line))
            position += 1

        self.stack.append(new_frame)

        for arg_id, val, arg_line in evaluated_args:
            if arg_id in def_vals and def_vals[arg_id] is not None:
                val = substitute_undef_with_defaults(val, def_vals[arg_id])
            self.set_variable(arg_id, val, arg_line)

        print_function('kwargs args: %s'%(args,))

        top = self.stack[-1]
        if (def_vals is not None):
            for k,v in def_vals.items():
                if (not top.has_variable(k, line)):
                    self.set_variable(k, v, line)

    def parse_block(self, block):
        for statement in block:
            self.parse_statement(statement)

    def parse_statement(self, s, use=False):
        debug(TAG+"Parse statement %s"%(s,))
        if 'type' not in s:
            raise UnhandledCaseError('Statement type unknown: %s'%(s,))
        t = s['type']
        debug(TAG+"%i:%s"%(s['line'],s['type']))
        if t=='stat_modifier':
            if s['id'] == 'disable':
                return
            if s['id'] == 'root':
                if 'statement' in s:
                    self.parse_statement(s['statement'])
                return
        elif t=='stat_assign':
            self.parse_expr(s['val'])
        elif t=='stat_call':
            if use:
                debug(TAG+"Skip call because \"use\" is set")
            else:
                check_cancel()
                entry = _builtin_modules.get(s['id'])
                if entry is not None:
                    entry['handler'](self, s)
                else:
                    self.push_stack()

                    # --- ИСПРАВЛЕННЫЙ БЛОК: Игнорируем неизвестный модуль по стандарту OpenSCAD ---
                    try:
                        cbl = self.find_module(s['id'], s['line'])
                    except UnknownModuleError:
                        warning("%s:%i Warning: Ignoring unknown module call '%s'" % (self.path, s['line'], s['id']))
                        self.pop_stack() # Обязательно сбрасываем локальный фрейм
                        return           # Завершаем выполнение этого стейтмента и переходим к следующему
                    # -----------------------------------------------------------------------------

                    debug(TAG+'Found module %s'%(s['id'],))
                    self.parse_kwargs(s['id'], s['line'], cbl['args'], s['args'])

                    # --- Сохраняем children-блок для children() ---
                    if 'block' in s:
                        top = self.stack[-1]
                        top.set_variable('$children_block', s['block'], s['line'])
                        top.set_variable('$children', len(s['block']), s['line'])
                    # ------------------------------------------------

                    # --- ВАЖНО: Неявный Group для модулей ---
                    if self._should_group():
                        self.push_context(SCLGroup, get_inc_name("group"))
                        for statement in cbl['block']:
                            self.parse_statement(statement)
                        self.active_context.group()
                        self.pop_context()
                    else:
                        for statement in cbl['block']:
                            self.parse_statement(statement)
                    # ----------------------------------------

                    debug(TAG+'Module %s done'%(s['id']))
                    self.pop_stack()
        elif t=='stat_function_definition':
            fid = s['id']
            ln = s['line']
            self.set_function(fid, s, ln)
        elif t=='stat_module_definition':
            mid = s['id']
            ln = s['line']
            self.set_module(mid, s, ln)
        elif t=='stat_for':
            if use:
                debug(TAG+"Skip for because \"use\" is set")
            else:
                ctr_name = s['counter']
                counter_vals = self.parse_expr(s['counter_vals'])
                block = s['block']
                ln = s['line']

                # Неявный Union для геометрии цикла
                iterations = len(counter_vals)
                adjust_progress_total(iterations)
                push_label(f"for {ctr_name}")

                if self._should_group():
                    self.push_context(SCLGroup, get_inc_name("group"))

                    for c in counter_vals:
                        set_progress_label(f"for {ctr_name}={c}")
                        check_cancel()
                        # Строгий Scope для каждой итерации (OpenSCAD semantics)
                        self.push_stack()
                        top = self.stack[-1]
                        top.set_variable(ctr_name, c, ln)

                        for statement in block:
                            self.parse_statement(statement)

                        self.pop_stack() # Уничтожаем переменные итерации
                        advance_progress()

                    self.active_context.group()
                    self.pop_context()
                else:
                    for c in counter_vals:
                        set_progress_label(f"for {ctr_name}={c}")
                        check_cancel()
                        self.push_stack()
                        top = self.stack[-1]
                        top.set_variable(ctr_name, c, ln)

                        for statement in block:
                            self.parse_statement(statement)
                        self.pop_stack()
                        advance_progress()
                pop_label()

        elif t=='stat_let':
            if use:
                debug(TAG+"Skip let because \"use\" is set")
            else:
                self.push_stack()
                for var in s['vars']:
                    if isinstance(var, dict) and var.get('type') == 'expr_assign':
                        val = self.parse_expr(var['val'])
                        top = self.stack[-1]
                        top.set_variable(var['id'], val, s['line'])
                for statement in s['block']:
                    self.parse_statement(statement)
                self.pop_stack()

        elif t=='stat_if':
            if use:
                debug(TAG+"Skip if because \"use\" is set")
            else:
                cond = self.parse_expr(s['condition'])
                if_block = s['blocks'][0]
                else_block = None
                if len(s['blocks'])>1:
                    else_block = s['blocks'][1]
                debug(TAG+"Cond: %s"%(str(cond),))

                if scl_bool(cond):
                    self.push_stack()
                    for statement in if_block:
                        self.parse_statement(statement)
                    self.pop_stack()
                elif (else_block is not None):
                    self.push_stack()
                    for statement in else_block:
                        self.parse_statement(statement)
                    self.pop_stack()

        elif t=='use':
            path = s['path']
            filename, ext = os.path.splitext(path)
            base_paths = [os.getcwd(), os.path.dirname(os.path.abspath(self.path))]
            paths = [os.path.join(bp, path) for bp in base_paths]
            debug(TAG+'paths: %s'%(repr(paths),))
            file_found = False
            for p in paths:
                if os.path.isfile(p):
                    debug(TAG+"Using path %s"%(p,))
                    parser = None
                    old_path = self.path
                    self.path = p
                    if ext == '.sck':
                        self.push_context(SCLProfile2, get_inc_name("profile2"))
                    elif (ext == '.scp') or (ext == '.scad'):
                        self.push_context(SCLPart3, get_inc_name("part"))
                    else:
                        critical("%s:%i Unknown file type %s"%(self.path, s['line'], path,))

                    try:
                        parser = ScadLikeParser(data=None, path=p, debug=self.debug)

                        debug(TAG+json.dumps(parser.data, indent=2))
                        for s in parser.data:
                            self.parse_statement(s, use=True)

                    except ParserError as pe:
                        warning("%s:%i Use: unknown expression %s in file used at %s:%i"%(p, pe.lineno, pe.message, self.path, s['line']))
                        if (self.verbose>=4):
                            traceback.print_exc()
                    self.pop_context()
                    self.path=old_path
                    file_found = True
                    break
            if (not file_found):
                critical("%s:%i File %s not found"%(self.path, s['line'], path,))
        elif t=='include':
            path = s['path']
            base_paths = [os.getcwd(), os.path.dirname(os.path.abspath(self.path))]
            paths = [os.path.join(bp, path) for bp in base_paths]
            file_found = False
            for p in paths:
                if os.path.isfile(p):
                    debug(TAG+"Including path %s"%(path,))
                    parser = None
                    old_path = self.path
                    self.path = p
                    try:
                        parser = ScadLikeParser(data=None, path=p, debug=self.debug)

                        debug(TAG+json.dumps(parser.data, indent=2))
                        for s in parser.data:
                            self.parse_statement(s)

                    except ParserError as pe:
                        warning("%s:%i Include: unknown expression %s in file included at %s:%i"%(p, pe.lineno, pe.message, self.path, s['line']))
                        if (self.verbose>=4):
                            traceback.print_exc()
                    self.path=old_path
                    file_found = True
                    break
            if (not file_found):
                critical("%s:%i File %s not found"%(self.path, s['line'], path,))
        else:
            raise UnhandledCaseError('unknown statement \"%s\"'%(t,))

# ── Windows PyInstaller spawn support ─────────────────────────────
# These must be at module level (not inside if __name__=="__main__")
# so that multiprocessing spawn child processes (Windows) can resolve
# them by name when unpickling the Process object.
# ──────────────────────────────────────────────────────────────────

class IPCLogHandler(logging.Handler):
    _prefix = {
        'DEBUG':    'D: ',
        'INFO':     'I: ',
        'WARNING':  'W: ',
        'ERROR':    'E: ',
        'CRITICAL': 'C: ',
    }
    def emit(self, record):
        prefix = self._prefix.get(record.levelname, '')
        log_entry = prefix + record.getMessage()
        if hasattr(Singleton, 'pipe') and Singleton.pipe is not None:
            try:
                Singleton.pipe.send(json.dumps({
                    'rp': replies[RP_LOG],
                    'args': ['console', log_entry]
                }))
            except OSError:
                pass

def occt_proc(pipe, img, cmdline):
    logging.basicConfig(level=logging.DEBUG, format='%(message)s')
    args = parse_args(cmdline)

    signal.signal(signal.SIGINT, signal.SIG_IGN)
    Singleton.pipe = pipe
    Singleton.img = img

    Singleton.settings = Settings()

    Singleton.scl = SCL(path=args.file, output_path=args.output,
                        verbose=args.verbose, extra={})
    logging.getLogger().addHandler(IPCLogHandler())

    ep.push_event(ee.main_start, args)
    ep.process()

    bg1, bg2 = Singleton.settings.get_rgb_pair('display')
    if hasattr(Singleton, 'pipe') and Singleton.pipe is not None:
        Singleton.pipe.send(json.dumps({
            'rp': replies[RP_SETTING],
            'args': ['display', 'bg_gradient', f'{bg1[0]},{bg1[1]},{bg1[2]};{bg2[0]},{bg2[1]},{bg2[2]}'],
        }))

    if args.output is None and Singleton.sd is not None:
        Singleton.sd.start_display()

if __name__=="__main__":
    import sys

    import multiprocessing
    # freeze_support() must be called before any other multiprocessing code
    # when running as a frozen PyInstaller .exe on Windows:
    #   - with 'spawn' start method (the only option on Windows),
    #     child processes receive --multiprocessing-fork in sys.argv
    #   - freeze_support() intercepts those args in the child and hands
    #     control to multiprocessing.spawn.spawn_main()
    #   - on the parent (and on Linux) this is a no-op
    multiprocessing.freeze_support()
    try:
        if sys.platform != 'win32':
            # 'fork' is faster and avoids re-importing OCCT/OpenGL in
            # the child process; not available on Windows
            multiprocessing.set_start_method('fork', force=True)
    except RuntimeError:
        pass

    from bcad.binterpreter.glfw_display import RunGLFWDisplay

    RunGLFWDisplay(occt_proc, sys.argv[1:])


