// This library provides WebAssembly bindings for the FreeCAD's geometric solver library planegcs.
// Copyright (C) 2023  Miroslav Šerý, Salusoft89 <miroslav.sery@salusoft89.cz>  

// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.

// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.

// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

import type { Constraint, ConstraintParamType } from "../planegcs_dist/constraints";
import { constraint_param_index } from "../planegcs_dist/constraint_param_index.js";
import { SketchIndex } from "./sketch_index.js";
import { emsc_vec_to_arr } from "./emsc_vectors.js";
import type {  SketchArc, SketchArcOfEllipse, SketchCircle, SketchEllipse, SketchLine, SketchPrimitive, SketchPoint, SketchParam, SketchHyperbola, SketchArcOfHyperbola, SketchParabola, SketchArcOfParabola } from "./sketch_primitive";
import { is_sketch_constraint, is_sketch_geometry } from "./sketch_primitive.js";
import { type GcsGeometry, type GcsSystem } from "../planegcs_dist/gcs_system.js";
import { Algorithm, Constraint_Alignment, SolveStatus, DebugMode } from "../planegcs_dist/enums.js";
import get_property_offset, { property_offsets } from "./geom_params.js";
import { oid } from "../planegcs_dist/id";

export class GcsWrapper { 
    gcs: GcsSystem;
    p_param_index: Map<oid, number> = new Map();
    sketch_index = new SketchIndex();
    // sketch param name -> index in gcs params
    private sketch_param_index: Map<string, number> = new Map(); 
    // nondriving constraint id -> list of its properties pushed as p-params (in order)
    private nondriving_constraint_params_order: Map<oid, string[]> = new Map(); 
    private enable_equal_optimization = false;

    get debug_mode() {
        return this.gcs.get_debug_mode() as DebugMode;
    }

    set debug_mode(mode: DebugMode) {
        this.gcs.set_debug_mode(mode);
    }

    get equal_optimization(): boolean {
        return this.enable_equal_optimization;
    }

    set equal_optimization(val: boolean) {
        this.enable_equal_optimization = val;
    }

    constructor(gcs: GcsSystem) {
        this.gcs = gcs;
    }

    destroy_gcs_module() {
        // only call before the deleting of this object
        this.gcs.clear_data();
        this.gcs.delete();
    }

    clear_data() {
        this.nondriving_constraint_params_order.clear();
        this.gcs.clear_data();
        this.p_param_index.clear();
        this.sketch_param_index.clear();
        this.sketch_index.clear();
    }
 
    // ------ Sketch -> GCS ------- (when building up a sketch)

    push_primitive(o: SketchPrimitive) {
        switch (o.type) {
            case 'point':
                this.push_point(o);
                break;
            case 'line':
                this.push_line(o);
                break;
            case 'circle':
                this.push_circle(o);
                break;
            case 'arc':
                this.push_arc(o);
                break;
            case 'ellipse':
                this.push_ellipse(o);
                break;
            case 'arc_of_ellipse':
                this.push_arc_of_ellipse(o);
                break;
            case 'hyperbola':
                this.push_hyperbola(o);
                break;
            case 'arc_of_hyperbola':
                this.push_arc_of_hyperbola(o);
                break;
            case 'parabola':
                this.push_parabola(o);
                break;
            case 'arc_of_parabola':
                this.push_arc_of_parabola(o);
                break;
            default:
                this.push_constraint(o);
        }

        if (this.sketch_index.has(o.id)) {
            throw new Error(`object with id ${o.id} already exists`);
        }

        this.sketch_index.set_primitive(o);
    }

    push_primitives_and_params(objects: (SketchPrimitive | SketchParam)[]) {
        for (const o of objects) {
            if (o.type === 'param') {
                this.push_sketch_param(o.name, o.value);
            } else {
                this.push_primitive(o);
            }
        }
    }

    solve(algorithm: Algorithm = Algorithm.DogLeg) {
        return this.gcs.solve_system(algorithm) as SolveStatus;
    }

    apply_solution() {
        this.gcs.apply_solution();
        for (const obj of this.sketch_index.get_primitives()) {
            this.pull_primitive(obj);
        }
    }

    set_convergence_threshold(threshold: number) {
        this.gcs.set_covergence_threshold(threshold);
    }

    get_convergence_threshold(): number {
        return this.gcs.get_convergence_threshold();
    }

    set_max_iterations(n: number) {
        this.gcs.set_max_iterations(n);
    }

    get_max_iterations(): number {
        return this.gcs.get_max_iterations();
    }

    get_gcs_params(): number[] {
        return emsc_vec_to_arr(this.gcs.get_p_params());
    }

    get_gcs_conflicting_constraints(): string[] {
        return emsc_vec_to_arr(this.gcs.get_conflicting()).map(
            (i) => this.sketch_index.get_id_by_index(i)
        );
    }

    get_gcs_redundant_constraints(): string[] {
        return emsc_vec_to_arr(this.gcs.get_redundant()).map(
            (i) => this.sketch_index.get_id_by_index(i)
        );
    }

    get_gcs_partially_redundant_constraints(): string[] {
        return emsc_vec_to_arr(this.gcs.get_partially_redundant()).map(
            (i) => this.sketch_index.get_id_by_index(i)
        );
    }

    has_gcs_conflicting_constraints(): boolean {
        return this.gcs.has_conflicting();
    }

    has_gcs_redundant_constraints(): boolean {
        return this.gcs.has_redundant();
    }

    has_gcs_partially_redundant_constraints(): boolean {
        return this.gcs.has_partially_redundant();
    }

    push_sketch_param(name: string, value: number, fixed = true): number {
        const pos = this.gcs.params_size();
        this.gcs.push_p_param(value, fixed);
        this.sketch_param_index.set(name, pos);
        return pos;
    }

    set_sketch_param(name: string, value: number) {
        const pos = this.sketch_param_index.get(name);
        if (pos === undefined) {
            throw new Error(`sketch param ${name} not found`);
        }
        this.gcs.set_p_param(pos, value, true);
    }

    get_sketch_param_value(name: string): number | undefined {
        const pos = this.sketch_param_index.get(name);
        return pos === undefined ? undefined : this.gcs.get_p_param(pos);
    }

    get_sketch_param_values(): Map<string, number> {
        const result = new Map<string, number>();
        for (const [name, pos] of this.sketch_param_index) {
            result.set(name, this.gcs.get_p_param(pos));
        }
        return result;
    }

    private push_p_params(id: oid, values: number[], fixed = false): number {
        const pos = this.gcs.params_size();
        for (const value of values) {
            this.gcs.push_p_param(value, fixed);
        }

        if (!this.p_param_index.has(id)) {
            this.p_param_index.set(id, pos);
        }

        return pos;
    }

    private push_point(p: SketchPoint) {
        if (this.p_param_index.has(p.id)) {
            return;
        }

        this.push_p_params(p.id, [p.x, p.y], p.fixed);
    }

    private push_line(l: SketchLine) {
        const p1 = this.sketch_index.get_sketch_point(l.p1_id);
        const p2 = this.sketch_index.get_sketch_point(l.p2_id);
        this.push_point(p1);
        this.push_point(p2);
    }

    private push_circle(c: SketchCircle) {
        const p = this.sketch_index.get_sketch_point(c.c_id);
        this.push_point(p);

        this.push_p_params(c.id, [c.radius], false);
    }

    private push_arc(a: SketchArc) {
        const center = this.sketch_index.get_sketch_point(a.c_id);
        this.push_point(center);
        
        const start = this.sketch_index.get_sketch_point(a.start_id);
        this.push_point(start);
        
        const end = this.sketch_index.get_sketch_point(a.end_id);
        this.push_point(end);

        this.push_p_params(a.id, [a.start_angle, a.end_angle, a.radius], false);
    }

    private push_ellipse(e: SketchEllipse) {
        const center = this.sketch_index.get_sketch_point(e.c_id);
        this.push_point(center);

        const focus1 = this.sketch_index.get_sketch_point(e.focus1_id);
        this.push_point(focus1);

        this.push_p_params(e.id, [e.radmin], false);
    }

    private push_hyperbola(h: SketchHyperbola) {
        const center = this.sketch_index.get_sketch_point(h.c_id);
        this.push_point(center);

        const focus1 = this.sketch_index.get_sketch_point(h.focus1_id);
        this.push_point(focus1);

        this.push_p_params(h.id, [h.radmin], false);
    }

    private push_arc_of_hyperbola(ah: SketchArcOfHyperbola) {
        const center = this.sketch_index.get_sketch_point(ah.c_id);
        this.push_point(center);

        const focus1 = this.sketch_index.get_sketch_point(ah.focus1_id);
        this.push_point(focus1);

        const start = this.sketch_index.get_sketch_point(ah.start_id);
        this.push_point(start);

        const end = this.sketch_index.get_sketch_point(ah.end_id);
        this.push_point(end);

        this.push_p_params(ah.id, [ah.start_angle, ah.end_angle, ah.radmin], false);
    }

    private push_parabola(p: SketchParabola) {
        const vertex = this.sketch_index.get_sketch_point(p.vertex_id);
        this.push_point(vertex);

        const focus1 = this.sketch_index.get_sketch_point(p.focus1_id);
        this.push_point(focus1);
    }

    private push_arc_of_parabola(ap: SketchArcOfParabola) {
        const vertex = this.sketch_index.get_sketch_point(ap.vertex_id);
        this.push_point(vertex);

        const focus1 = this.sketch_index.get_sketch_point(ap.focus1_id);
        this.push_point(focus1);

        const start = this.sketch_index.get_sketch_point(ap.start_id);
        this.push_point(start);

        const end = this.sketch_index.get_sketch_point(ap.end_id);
        this.push_point(end);

        this.push_p_params(ap.id, [ap.start_angle, ap.end_angle], false);
    }

    private push_arc_of_ellipse(ae: SketchArcOfEllipse) {
        const center = this.sketch_index.get_sketch_point(ae.c_id);
        this.push_point(center);

        const focus1 = this.sketch_index.get_sketch_point(ae.focus1_id);
        this.push_point(focus1);

        const start = this.sketch_index.get_sketch_point(ae.start_id);
        this.push_point(start);

        const end = this.sketch_index.get_sketch_point(ae.end_id);
        this.push_point(end);

        this.push_p_params(ae.id, [ae.start_angle, ae.end_angle, ae.radmin], false);
    }

    private sketch_primitive_to_gcs(o: SketchPrimitive) : GcsGeometry {
        switch (o.type) {
            case 'point': {
                const p_i = this.get_primitive_addr(o.id);
                return this.gcs.make_point(
                    p_i + property_offsets.point.x, p_i + property_offsets.point.y
                );
            }
            case 'line': {
                const p1_i = this.get_primitive_addr(o.p1_id);
                const p2_i = this.get_primitive_addr(o.p2_id);
                return this.gcs.make_line(
                    p1_i + property_offsets.point.x, p1_i + property_offsets.point.y,
                    p2_i + property_offsets.point.x, p2_i + property_offsets.point.y
                );
            }
            case 'circle': {
                const cp_i = this.get_primitive_addr(o.c_id);
                const circle_i = this.get_primitive_addr(o.id);
                return this.gcs.make_circle(
                    cp_i + property_offsets.point.x, cp_i + property_offsets.point.y,
                    circle_i + property_offsets.circle.radius);
            }
            case 'arc': {
                const c_i = this.get_primitive_addr(o.c_id);
                const start_i = this.get_primitive_addr(o.start_id);
                const end_i = this.get_primitive_addr(o.end_id);
                const a_i = this.get_primitive_addr(o.id);
                return this.gcs.make_arc(
                    c_i + property_offsets.point.x, c_i + property_offsets.point.y, 
                    start_i + property_offsets.point.x, start_i + property_offsets.point.y, 
                    end_i + property_offsets.point.x, end_i + property_offsets.point.y, 
                    a_i + property_offsets.arc.start_angle, a_i + property_offsets.arc.end_angle, a_i + property_offsets.arc.radius);
            }
            case 'ellipse': {
                const c_i = this.get_primitive_addr(o.c_id);
                const focus1_i = this.get_primitive_addr(o.focus1_id);
                const radmin_i = this.get_primitive_addr(o.id);
                return this.gcs.make_ellipse(
                    c_i + property_offsets.point.x, c_i + property_offsets.point.y,
                    focus1_i + property_offsets.point.x, focus1_i + property_offsets.point.y,
                    radmin_i + property_offsets.ellipse.radmin
                );
            }
            case 'arc_of_ellipse': {
                const c_i = this.get_primitive_addr(o.c_id);
                const focus1_i = this.get_primitive_addr(o.focus1_id);
                const start_i = this.get_primitive_addr(o.start_id);
                const end_i = this.get_primitive_addr(o.end_id);
                const a_i = this.get_primitive_addr(o.id);
                return this.gcs.make_arc_of_ellipse(
                    c_i + property_offsets.point.x, c_i + property_offsets.point.y,
                    focus1_i + property_offsets.point.x, focus1_i + property_offsets.point.y,
                    start_i + property_offsets.point.x, start_i + property_offsets.point.y,
                    end_i + property_offsets.point.x, end_i + property_offsets.point.y,
                    a_i + property_offsets.arc_of_ellipse.start_angle, a_i + property_offsets.arc_of_ellipse.end_angle, a_i + property_offsets.arc_of_ellipse.radmin
                );
            }
            case 'hyperbola': {
                const c_i = this.get_primitive_addr(o.c_id);
                const focus1_i = this.get_primitive_addr(o.focus1_id);
                const radmin_i = this.get_primitive_addr(o.id + property_offsets.hyperbola.radmin);
                return this.gcs.make_hyperbola(
                    c_i + property_offsets.point.x, c_i + property_offsets.point.y,
                    focus1_i + property_offsets.point.x, focus1_i + property_offsets.point.y,
                    radmin_i + property_offsets.hyperbola.radmin
                );
            }
            case 'arc_of_hyperbola': {
                const c_i = this.get_primitive_addr(o.c_id);
                const focus1_i = this.get_primitive_addr(o.focus1_id);
                const start_i = this.get_primitive_addr(o.start_id);
                const end_i = this.get_primitive_addr(o.end_id);
                const a_i = this.get_primitive_addr(o.id);
                return this.gcs.make_arc_of_hyperbola(
                    c_i + property_offsets.point.x, c_i + property_offsets.point.y,
                    focus1_i + property_offsets.point.x, focus1_i + property_offsets.point.y,
                    start_i + property_offsets.point.x, start_i + property_offsets.point.y,
                    end_i + property_offsets.point.x, end_i + property_offsets.point.y,
                    a_i + property_offsets.arc_of_hyperbola.start_angle, a_i + property_offsets.arc_of_hyperbola.end_angle, a_i + property_offsets.arc_of_hyperbola.radmin
                );
            }
            case 'parabola': {
                const vertex_i = this.get_primitive_addr(o.vertex_id);
                const focus1_i = this.get_primitive_addr(o.focus1_id);
                return this.gcs.make_parabola(
                    vertex_i + property_offsets.point.x, vertex_i + property_offsets.point.y,
                    focus1_i + property_offsets.point.x, focus1_i + property_offsets.point.y
                );
            }
            case 'arc_of_parabola': {
                const vertex_i = this.get_primitive_addr(o.vertex_id);
                const focus1_i = this.get_primitive_addr(o.focus1_id);
                const start_i = this.get_primitive_addr(o.start_id);
                const end_i = this.get_primitive_addr(o.end_id);
                const a_i = this.get_primitive_addr(o.id);
                return this.gcs.make_arc_of_parabola(
                    vertex_i + property_offsets.point.x, vertex_i + property_offsets.point.y,
                    focus1_i + property_offsets.point.x, focus1_i + property_offsets.point.y,
                    start_i + property_offsets.point.x, start_i + property_offsets.point.y,
                    end_i + property_offsets.point.x, end_i + property_offsets.point.y,
                    a_i + property_offsets.arc_of_parabola.start_angle, a_i + property_offsets.arc_of_parabola.end_angle
                );
            }
            default:
                throw new Error(`not-implemented object type: ${o.type}`);
            }
    }

    private push_constraint(c: Constraint) {
        const add_constraint_args: (string|number|boolean|GcsGeometry)[] = [];
        const deletable: GcsGeometry[] = [];

        const constraint_params = constraint_param_index[c.type];
        if (constraint_params === undefined) {
            throw new Error(`unknown constraint type: ${c.type}`);
        }

        let numeric_tag_id = -1;
        if (!c.temporary) {
            numeric_tag_id = this.sketch_index.counter() + 1;
        }

        for (const parameter of Object.keys(constraint_params)) {
            const type = constraint_params[parameter];
            if (type === undefined) {
                throw new Error(`unknown parameter type: ${type} in constraint ${c.type}`);
            }

            // parameters with default values
            if (parameter === 'tagId') {
                add_constraint_args.push(numeric_tag_id);
                continue;
            }
            if (parameter === 'driving') {
                // add the driving? value (true by default)
                add_constraint_args.push(c.driving ?? true);
                continue;
            }
            if (parameter === 'internalalignment' && c.type === 'equal') {
                add_constraint_args.push(c.internalalignment ?? Constraint_Alignment.NoInternalAlignment);
                continue;
            }

            
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            const val = (c as any)[parameter] as ConstraintParamType;
            const is_fixed = (c.driving ?? true);
            
            if (type === 'object_param_or_number') { // or string
                // properties of constraints that can be either: 
                // 1. number => push a new p-param
                // 2. string => reference a sketch parameter
                // 3. { o_id: '1', prop: 'x' } => reference a property of a sketch primitive

                if (typeof val === 'number') {
                    if (!c.driving) {
                        // register a parameter of a non-driving constraint, that can be later updated 
                        // by the pull_constraint function
                        const list = this.nondriving_constraint_params_order.get(c.id);
                        if (list === undefined) {
                            this.nondriving_constraint_params_order.set(c.id, [parameter]);
                        } else {
                            list.push(parameter);
                        }
                    }
                    const pos = this.push_p_params(c.id, [val], is_fixed);
                    add_constraint_args.push(pos);
                } else if (typeof val === 'string') {
                    // this is a sketch param
                    const param_addr = this.sketch_param_index.get(val);
                    if (param_addr === undefined) {
                        throw new Error(`couldn't parse object param: ${parameter} in constraint ${c.type}: unknown param ${val}`);
                    }
                    add_constraint_args.push(param_addr);
                } else if (typeof val === 'boolean') {
                    add_constraint_args.push(val);
                } else if (val !== undefined) {
                    const ref_primitive = this.sketch_index.get_primitive_or_fail(val.o_id);
                    if (!is_sketch_geometry(ref_primitive)) {
                        throw new Error(`Primitive #${val.o_id} (${ref_primitive.type}) is not supported to be referenced from a constraint.`);
                    }
                    const param_addr = this.get_primitive_addr(val.o_id) + get_property_offset(ref_primitive.type, val.prop);
                    add_constraint_args.push(param_addr);
                }
            } else if (type === 'object_id' && typeof val === 'string') {
                const obj = this.sketch_index.get_primitive_or_fail(val);
                const gcs_obj = this.sketch_primitive_to_gcs(obj);
                add_constraint_args.push(gcs_obj);
                deletable.push(gcs_obj);
            } else if (type === 'primitive_type' && (typeof val === 'number' || typeof val === 'boolean')) {
                add_constraint_args.push(val);
            }else {
                throw new Error(`unhandled parameter ${parameter} type: ${type}`);
            }
        }

        const c_name: string = c.type;
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        (this.gcs as any)[`add_constraint_${c_name}`](...add_constraint_args, c.scale ?? 1);

        // if something is set to be equal, then optimize this process by setting the parameter to the value directly
        if (this.enable_equal_optimization && c_name === 'equal') {
            const [param_1_addr, param_2_addr] = [add_constraint_args[0], add_constraint_args[1]];
            if (typeof param_1_addr === 'number' && typeof param_2_addr === 'number') {
                if (this.gcs.get_is_fixed(param_1_addr) && !this.gcs.get_is_fixed(param_2_addr)) {
                    this.gcs.set_p_param(param_2_addr, this.gcs.get_p_param(param_1_addr), false);
                } else if (this.gcs.get_is_fixed(param_2_addr) && !this.gcs.get_is_fixed(param_1_addr)) {
                    this.gcs.set_p_param(param_1_addr, this.gcs.get_p_param(param_2_addr), false);
                }
            }
        }

        // wasm-allocated objects must be manually deleted 
        for (const geom_shape of deletable) {
            geom_shape.delete();
        }
    }

    // delete_constraint_by_id(id: oid): boolean {
    //     if (id !== '-1') {
    //         const item = this.sketch_index.get_primitive(id);
    //         if (item !== undefined && !is_sketch_geometry(item)) {
    //             throw new Error(`object #${id} (${item.type}) is not a constraint (delete_constraint_by_id)`);
    //         }
    //     }

    //     this.gcs.clear_by_id(id);
    //     return this.sketch_index.delete_primitive(id);
    // }

    private get_primitive_addr(id: oid): number {
        const addr = this.p_param_index.get(id);
        if (addr === undefined) {
            throw new Error(`sketch object ${id} not found in p-params index`);
        }
        return addr;
    }

    // ------- GCS -> Sketch ------- (when retrieving a solution)

    private pull_primitive(p: SketchPrimitive) {
        if (this.p_param_index.has(p.id)) {
            if (p.type === 'point') {
                this.pull_point(p);
            } else if (p.type === 'line') {
                this.pull_line(p);
            } else if (p.type === 'arc') {
                this.pull_arc(p);
            } else if (p.type === 'circle') {
                this.pull_circle(p);
            } else if (p.type === 'ellipse') {
                this.pull_ellipse(p);
            } else if (p.type === 'arc_of_ellipse') {
                this.pull_arc_of_ellipse(p);
            } else if (p.type === 'hyperbola') {
                this.pull_hyperbola(p);
            } else if (p.type === 'arc_of_hyperbola') {
                this.pull_arc_of_hyperbola(p);
            } else if (p.type === 'parabola') {
                this.pull_parabola(p);
            } else if (p.type === 'arc_of_parabola') {
                this.pull_arc_of_parabola(p);
            } else if (is_sketch_constraint(p)) {
                this.pull_constraint(p);
            } else {
                // console.log(`${p.type}`);
                // todo: is this else branch necessary?
                this.sketch_index.set_primitive(p);
            }
        } else {
            // console.log(`object ${o.type} #${o.id} not found in params when retrieving solution`);
            this.sketch_index.set_primitive(p);
        }
    }

    private pull_point(p: SketchPoint) {
        const point_addr = this.get_primitive_addr(p.id);
        const point = {
            ...p,
            x: this.gcs.get_p_param(point_addr + property_offsets.point.x),
            y: this.gcs.get_p_param(point_addr + property_offsets.point.y),
        }
        this.sketch_index.set_primitive(point);
    }

    private pull_line(l: SketchLine) {
        this.sketch_index.set_primitive(l);
    }

    private pull_arc(a: SketchArc) {
        const addr = this.get_primitive_addr(a.id);
        this.sketch_index.set_primitive({
            ...a,
            start_angle: this.gcs.get_p_param(addr + property_offsets.arc.start_angle),
            end_angle: this.gcs.get_p_param(addr + property_offsets.arc.end_angle),
            radius: this.gcs.get_p_param(addr + property_offsets.arc.radius)
        });
    }

    private pull_circle(c: SketchCircle) {
        const addr = this.get_primitive_addr(c.id);

        this.sketch_index.set_primitive({
            ...c,
            radius: this.gcs.get_p_param(addr + property_offsets.circle.radius)
        });
    }

    private pull_ellipse(e: SketchEllipse) {
        const addr = this.get_primitive_addr(e.id);

        this.sketch_index.set_primitive({
            ...e,
            radmin: this.gcs.get_p_param(addr + property_offsets.ellipse.radmin),
        });
    }

    private pull_arc_of_ellipse(ae: SketchArcOfEllipse) {
        const addr = this.get_primitive_addr(ae.id);

        this.sketch_index.set_primitive({
            ...ae,
            start_angle: this.gcs.get_p_param(addr + property_offsets.arc_of_ellipse.start_angle),
            end_angle: this.gcs.get_p_param(addr + property_offsets.arc_of_ellipse.end_angle),
            radmin: this.gcs.get_p_param(addr + property_offsets.arc_of_ellipse.radmin),
        });
    }

    private pull_hyperbola(h: SketchHyperbola) {
        const addr = this.get_primitive_addr(h.id);

        this.sketch_index.set_primitive({
            ...h,
            radmin: this.gcs.get_p_param(addr + property_offsets.hyperbola.radmin),
        });
    }

    private pull_arc_of_hyperbola(ah: SketchArcOfHyperbola) {
        const addr = this.get_primitive_addr(ah.id);

        this.sketch_index.set_primitive({
            ...ah,
            start_angle: this.gcs.get_p_param(addr + property_offsets.arc_of_hyperbola.start_angle),
            end_angle: this.gcs.get_p_param(addr + property_offsets.arc_of_hyperbola.end_angle),
            radmin: this.gcs.get_p_param(addr + property_offsets.arc_of_hyperbola.radmin),
        });
    }

    private pull_parabola(p: SketchParabola) {
        this.sketch_index.set_primitive(p);
    }

    private pull_arc_of_parabola(ap: SketchArcOfParabola) {
        const addr = this.get_primitive_addr(ap.id);

        this.sketch_index.set_primitive({
            ...ap,
            start_angle: this.gcs.get_p_param(addr + property_offsets.arc_of_parabola.start_angle),
            end_angle: this.gcs.get_p_param(addr + property_offsets.arc_of_parabola.end_angle),
        });
    }

    private pull_constraint(c: Constraint) {
        // We don't need to pull driving constraints
        if(c.driving !== false) {
            return;
        }

        const constraint_addr = this.get_primitive_addr(c.id);

        const offsets = this.nondriving_constraint_params_order.get(c.id);
        if(!offsets) {
            console.warn(`No offsets for constraint type ${c.type}`)
            return
        }

        const constraint_copy = {...c};
        // Helper function to update a property of any constraint type (Equal, L2L, etc.)
        // preventing Typescript to complain about the type of the property (unable to assign a number to never)
        function update_property<T, K extends keyof T>(obj: T, key: K, value: T[K]) {
            obj[key] = value;
        }

        for (const [offset, constraint_property_name] of offsets.entries()) {
            const param = this.gcs.get_p_param(constraint_addr + offset);
            update_property(constraint_copy, constraint_property_name as keyof Constraint, param);
        }

        this.sketch_index.set_primitive(constraint_copy);
    }
}