MacroDefinition.java

package com.varnernet.gerb4j;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
 * Represents a Gerber aperture macro definition (AM command). Stores the macro name, its primitive
 * definitions, and evaluated variables.
 *
 * <p>Example: %AMBOXR* 0 Rectangle with Rounded corners* $5=$1/2-$3* $6=-$3+$2/2*
 * 21,1,$1,$2-$7,0,0,$4* 1,1,$7,$5,$6,$4* *%
 *
 * <p>When instantiated: %ADD10BOXR,0.1X0.05X0.02X0.0*% Parameters: [0.1, 0.05, 0.02, 0.0]
 * Variables: $5, $6, $7 are evaluated with these parameters
 */
public class MacroDefinition {
    private final String name;
    private final List<String> primitives;
    private final Map<Integer, String> variableExpressions;
    private final MacroExpressionEvaluator expressionEvaluator;

    /**
     * Constructor with macro name.
     *
     * @param name the macro name
     */
    public MacroDefinition(final String name) {
        this(name, MacroExpressionEvaluator.DEFAULT);
    }

    /**
     * Constructor with dependency injection for expression evaluator. Allows testing with custom evaluators.
     *
     * @param name      the macro name
     * @param evaluator the expression evaluator
     */
    public MacroDefinition(final String name, final MacroExpressionEvaluator evaluator) {
        this.name = name;
        this.primitives = new ArrayList<>();
        this.variableExpressions = new HashMap<>();
        this.expressionEvaluator = evaluator;
    }

    /**
     * Gets the macro name (e.g., "BOXR").
     *
     * @return the macro name
     */
    public String getName() {
        return name;
    }

    /**
     * Gets all primitive definitions (raw strings from macro body).
     *
     * @return the list of primitive definitions
     */
    public List<String> getPrimitives() {
        return primitives;
    }

    /**
     * Adds a primitive definition to this macro. Examples: "21,1,$1,$2-$7,0,0,$4*", "1,1,$7,$5,$6,$4*".
     *
     * @param primitiveDefinition the primitive definition string
     */
    public void addPrimitive(final String primitiveDefinition) {
        if (primitiveDefinition != null && !primitiveDefinition.isEmpty()) {
            primitives.add(primitiveDefinition);
        }
    }

    /**
     * Registers a variable expression (e.g., $5=$1/2-$3).
     *
     * @param variableNumber the variable number (5 for $5)
     * @param expression     the expression (e.g., "$1/2-$3")
     */
    public void addVariableExpression(final int variableNumber, final String expression) {
        variableExpressions.put(variableNumber, expression);
    }

    /**
     * Gets the expression for a variable (e.g., get expression for $5).
     *
     * @param variableNumber the variable number
     * @return the expression string
     */
    public String getVariableExpression(final int variableNumber) {
        return variableExpressions.get(variableNumber);
    }

    /**
     * Gets all defined variables.
     *
     * @return map of variable expressions
     */
    public Map<Integer, String> getVariableExpressions() {
        return new HashMap<>(variableExpressions);
    }

    /**
     * Evaluates all variables with the given parameters, processing them in ascending variable-number
     * order so that a variable ($7) may safely reference an already-computed variable ($6).
     *
     * @param parameters the parameters provided in the AD command
     * @return map of variable number → evaluated value (includes all computed vars)
     */
    public Map<Integer, Double> evaluateVariables(final List<String> parameters) {
        Map<Integer, Double> evaluatedVariables = new HashMap<>();

        // Sort by variable number to ensure dependencies are resolved in order
        new TreeMap<>(variableExpressions)
                .forEach(
                        (varNumber, expression) -> {
                            try {
                                double value =
                                        expressionEvaluator.evaluateWithVariables(
                                                expression, parameters, evaluatedVariables);
                                evaluatedVariables.put(varNumber, value);
                            } catch (Exception e) {
                                throw new IllegalArgumentException(
                                        String.format(
                                                "Failed to evaluate $%d in macro %s: %s", varNumber, name, e.getMessage()),
                                        e);
                            }
                        });

        return evaluatedVariables;
    }
}