MacroPrimitiveFactory.java

package com.varnernet.gerb4j.macro;

import com.varnernet.gerb4j.MacroDefinition;
import com.varnernet.gerb4j.MacroExpressionEvaluator;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

/**
 * Parses the raw primitive strings stored in a {@link MacroDefinition} into typed {@link
 * MacroPrimitive} objects given a fully-evaluated variable map.
 *
 * <p>This is the single implementation of macro-primitive parsing. It replaces the parallel logic
 * that previously existed in both {@code MacroApertureRenderer} (for rendering) and {@code
 * MacroPrimitiveParser} (for bounds). Each resulting {@link MacroPrimitive} knows its own bounds
 * <em>and</em> how to render itself.
 *
 * <p>Call {@link #parse(MacroDefinition, Map, List)} once at aperture-instantiation time (when the
 * AD command is processed) and store the result in the {@link
 * com.varnernet.gerb4j.TemplateAperture}.
 */
public final class MacroPrimitiveFactory {

    private static final Logger LOG = Logger.getLogger(MacroPrimitiveFactory.class.getName());

    private static final int THERMAL_PRIMITIVE_MIN_LENGTH = 7;
    private static final int CIRCLE_PRIMITIVE_MIN_LENGTH = 5;
    private static final int VECTOR_LINE_PRIMITIVE_MIN_LENGTH = 8;
    private static final int CENTER_LINE_PRIMITIVE_MIN_LENGTH = 7;
    private static final int OUTLINE_PRIMITIVE_MIN_LENGTH = 6;
    private static final int POLYGON_PRIMITIVE_MIN_LENGTH = 7;

    private MacroPrimitiveFactory() {
    }

    /**
     * Parse all primitives in {@code macro} using the supplied variable values.
     *
     * @param macro         The macro definition (holds raw primitive strings).
     * @param evaluatedVars Map of variable number → evaluated value (may be empty).
     * @param parameters    Raw parameter strings from the AD command (used as fallback when a variable
     *                      is not in {@code evaluatedVars}).
     * @return An unmodifiable list of resolved {@link MacroPrimitive} objects, in source order.
     * Comment primitives (type 0) are silently skipped.
     */
    public static List<MacroPrimitive> parse(
            final MacroDefinition macro, final Map<Integer, Double> evaluatedVars, final List<String> parameters) {
        if (macro == null) {
            return Collections.emptyList();
        }
        Map<Integer, Double> vars = evaluatedVars != null ? evaluatedVars : Collections.emptyMap();
        List<String> params = parameters != null ? parameters : Collections.emptyList();

        List<MacroPrimitive> result = new ArrayList<>();
        for (String raw : macro.getPrimitives()) {
            try {
                MacroPrimitive p = parsePrimitive(raw, vars, params);
                if (p != null) {
                    result.add(p);
                }
            } catch (Exception e) {
                LOG.warning("[MacroPrimitiveFactory] Skipping malformed primitive \""
                        + raw + "\": " + e.getMessage());
            }
        }
        return Collections.unmodifiableList(result);
    }

    // ── Primitive dispatch ─────────────────────────────────────────────────────

    private static MacroPrimitive parsePrimitive(
            final String raw, final Map<Integer, Double> vars, final List<String> params) {
        String rawInput = raw;
        if (rawInput.endsWith("*")) {
            rawInput = rawInput.substring(0, rawInput.length() - 1);
        }
        String[] parts = rawInput.split(",");
        if (parts.length < 2) {
            return null;
        }

        String type = parts[0].trim();
        return switch (type) {
            case "0" -> null; // comment primitive — skip
            case "1" -> parseCircle(parts, vars, params);
            case "20" -> parseVectorLine(parts, vars, params);
            case "21" -> parseCenterLine(parts, vars, params);
            case "4" -> parseOutline(parts, vars, params);
            case "5" -> parsePolygon(parts, vars, params);
            case "7" -> parseThermal(parts, vars, params);
            default -> null;
        };
    }

    // ── Per-type parsers ───────────────────────────────────────────────────────

    /**
     * Type 1: {@code 1,exposure,diameter,centerX,centerY[,rotation]}.
     *
     * @param p      the primitive parameters
     * @param vars   the variable values
     * @param params the macro parameters
     * @return the parsed circle macro primitive
     */
    private static MacroPrimitive parseCircle(
            final String[] p, final Map<Integer, Double> vars, final List<String> params) {
        if (p.length < CIRCLE_PRIMITIVE_MIN_LENGTH) {
            return null;
        }
        boolean exposed = val(p[1], vars, params) != 0.0;
        double diameter = val(p[2], vars, params);
        double centerX = val(p[3], vars, params);
        double centerY = val(p[4], vars, params);
        double rotation = p.length >= 6 ? val(p[5], vars, params) : 0.0;
        return new CircleMacroPrimitive(exposed, diameter, centerX, centerY, rotation);
    }

    /**
     * Type 20: {@code 20,exposure,width,startX,startY,endX,endY,rotation}.
     *
     * @param p      the primitive parameters
     * @param vars   the variable values
     * @param params the macro parameters
     * @return the parsed vector line macro primitive
     */
    private static MacroPrimitive parseVectorLine(
            final String[] p, final Map<Integer, Double> vars, final List<String> params) {
        if (p.length < VECTOR_LINE_PRIMITIVE_MIN_LENGTH) {
            return null;
        }
        boolean exposed = val(p[1], vars, params) != 0.0;
        double width = val(p[2], vars, params);
        double startX = val(p[3], vars, params);
        double startY = val(p[4], vars, params);
        double endX = val(p[5], vars, params);
        double endY = val(p[6], vars, params);
        double rotation = val(p[7], vars, params);
        return new VectorLineMacroPrimitive(exposed, width, startX, startY, endX, endY, rotation);
    }

    /**
     * Type 21: {@code 21,exposure,width,height,centerX,centerY,rotation}.
     *
     * @param p      the primitive parameters
     * @param vars   the variable values
     * @param params the macro parameters
     * @return the parsed center line macro primitive
     */
    private static MacroPrimitive parseCenterLine(
            final String[] p, final Map<Integer, Double> vars, final List<String> params) {
        if (p.length < CENTER_LINE_PRIMITIVE_MIN_LENGTH) {
            return null;
        }
        boolean exposed = val(p[1], vars, params) != 0.0;
        double width = val(p[2], vars, params);
        double height = val(p[3], vars, params);
        double centerX = val(p[4], vars, params);
        double centerY = val(p[5], vars, params);
        double rotation = val(p[6], vars, params);
        return new CenterLineMacroPrimitive(exposed, width, height, centerX, centerY, rotation);
    }

    /**
     * Type 4: {@code 4,exposure,n,x1,y1,x2,y2,...,xn+1,yn+1,rotation}. The n+1ᵗʰ coordinate pair
     * closes the outline back to the first vertex; the last element is always the rotation angle.
     *
     * @param p      the primitive parameters
     * @param vars   the variable values
     * @param params the macro parameters
     * @return the parsed outline macro primitive
     */
    private static MacroPrimitive parseOutline(
            final String[] p, final Map<Integer, Double> vars, final List<String> params) {
        if (p.length < OUTLINE_PRIMITIVE_MIN_LENGTH) {
            return null;
        }
        boolean exposed = val(p[1], vars, params) != 0.0;
        int numVertices = (int) val(p[2], vars, params);
        double rotation = val(p[p.length - 1], vars, params);

        // n+1 coordinate pairs start at index 3, each pair is (x, y)
        int maxVertices = numVertices + 1;
        double[] xs = new double[maxVertices];
        double[] ys = new double[maxVertices];
        for (int i = 0; i < maxVertices; i++) {
            int xi = 3 + i * 2;
            int yi = 4 + i * 2;
            // Leave room for the trailing rotation field
            if (xi >= p.length - 1 || yi >= p.length - 1) {
                break;
            }
            xs[i] = val(p[xi], vars, params);
            ys[i] = val(p[yi], vars, params);
        }
        return new OutlineMacroPrimitive(exposed, xs, ys, rotation);
    }

    /**
     * Type 5: {@code 5,exposure,numVertices,centerX,centerY,diameter,rotation}.
     *
     * @param p      the primitive parameters
     * @param vars   the variable values
     * @param params the macro parameters
     * @return the parsed polygon macro primitive
     */
    private static MacroPrimitive parsePolygon(
            final String[] p, final Map<Integer, Double> vars, final List<String> params) {
        if (p.length < POLYGON_PRIMITIVE_MIN_LENGTH) {
            return null;
        }
        boolean exposed = val(p[1], vars, params) != 0.0;
        int numVertices = (int) val(p[2], vars, params);
        double centerX = val(p[3], vars, params);
        double centerY = val(p[4], vars, params);
        double diameter = val(p[5], vars, params);
        double rotation = val(p[6], vars, params);
        return new PolygonMacroPrimitive(exposed, numVertices, centerX, centerY, diameter, rotation);
    }

    /**
     * Type 7: {@code 7,centerX,centerY,outerDiameter,innerDiameter,gapThickness,rotation}.
     *
     * @param p      the primitive parameters
     * @param vars   the variable values
     * @param params the macro parameters
     * @return the parsed thermal macro primitive
     */
    private static MacroPrimitive parseThermal(
            final String[] p, final Map<Integer, Double> vars, final List<String> params) {
        if (p.length < THERMAL_PRIMITIVE_MIN_LENGTH) {
            return null;
        }
        double centerX = val(p[1], vars, params);
        double centerY = val(p[2], vars, params);
        double outerDia = val(p[3], vars, params);
        double innerDia = val(p[4], vars, params);
        double gapThick = val(p[5], vars, params);
        double rotation = val(p[6], vars, params);
        return new ThermalMacroPrimitive(centerX, centerY, outerDia, innerDia, gapThick, rotation);
    }

    // ── Expression evaluation ──────────────────────────────────────────────────

    /**
     * Evaluate a macro expression string to a double value.
     *
     * <p>This method parses and evaluates arithmetic expressions in Gerber macro definitions,
     * supporting variables ($1, $2, etc.), basic arithmetic (+, -, *, /), and parentheses.
     *
     * <p>This is the single canonical implementation, replacing the duplicate {@code parseValue()}
     * methods that existed in {@code MacroApertureRenderer} and {@code MacroPrimitiveParser}.
     *
     * @param expr   the expression string to evaluate
     * @param vars   the variable map for $N references
     * @param params the parameter list for $1, $2, etc.
     * @return the evaluated double value
     */
    static double val(final String expr, final Map<Integer, Double> vars, final List<String> params) {
        String exprInput = expr.trim();
        // Fast path: plain decimal literal
        try {
            return Double.parseDouble(exprInput);
        } catch (NumberFormatException ignored) {
        }
        // Full expression evaluation with variable substitution
        return MacroExpressionEvaluator.DEFAULT.evaluateWithVariables(exprInput, params, vars);
    }
}