/*
    See license.txt in the root of this project.
*/

# include "luametatex.h"

/*tex
    This is experimental code, and it might disappear. It relates to snapping using \LUA, something
    we have in \CONTEXT\ (where snapping went from \MKII, to \MKVI\ and eventually \MKXL, but in
    2025 we started experimenting with more native snapping support in the engine.
*/

int tex_snapping_needed(snapping_specification *specification)
{
    return specification && specification->method; 
}

void tex_snapping_reset(snapping_specification *specification)
{
    if (specification) {
        memset(specification, 0, sizeof(snapping_specification));
    }
}

int tex_snapping_content(halfword first, halfword last, snapping_specification *specification)
{
    int okay = 0;
    if (specification && specification->method) {
        int enforce = 0;
        int ignore = 0;
        while (first) {
            switch (node_type(first)) {
                case hlist_node:
                case vlist_node:
                    if (! tex_has_box_option(first, box_option_no_snapping)) {
                        if (! ignore && (enforce || (specification->method & snapping_method_list))) {
                            if (specification->method & snapping_method_threshold) {
                                if (box_height(first) <= specification->top) {
                                    box_height(first) = specification->height;
                                    okay = 1;
                                }
                                if (box_depth(first) <= specification->bottom) {
                                    box_depth(first) = specification->depth;
                                    okay = 1;
                                }
                            }
                        }
                    }
                    break;
                case rule_node:
                    if (! tex_has_rule_option(first, rule_option_no_snapping)) {
                        if (! ignore && (enforce || specification->method & snapping_method_rule)) {
                            if (specification->method & snapping_method_threshold) {
                                if (rule_height(first) != null_flag) {
                                    if (rule_height(first) <= specification->top) {
                                        rule_height(first) = specification->height;
                                        okay = 1;
                                    }
                                } else {
                                    /* running height rules adapt anyway */
                                }
                                if (rule_depth(first) != null_flag) {
                                    if (rule_depth(first) <= specification->bottom) {
                                        rule_depth(first) = specification->depth;
                                        okay = 1;
                                    }
                                } else {
                                    /* running depth rules adapt anyway */
                                }
                            }
                        }
                    }
                    break;
                case math_node:
                    {
                        switch (node_subtype(first)) {
                            case begin_inline_math:
                            case begin_broken_math:
                                if (tex_has_math_option(first, math_option_no_snapping)) {
                                    ignore  = 1;
                                    enforce = 0;
                                } else {
                                    ignore  = ! (specification->method & snapping_method_math);
                                    enforce = 1;
                                }
                                break;
                            case end_inline_math:
                            case end_broken_math:
                                ignore  = 0;
                                enforce = 0;
                                break;
                        }
                    }
                    break;
            }
            if (first == last) { 
                return okay;
            } else {
                first = node_next(first);
            }
        }
    }
    return okay;
}

int tex_snapping_indeed(halfword first, halfword last, snapping_specification *specification)
{
    int okay = 0;
    if (specification && specification->method) {
        int enforce = 0;
        int ignore = 0;
        while (first) {
            switch (node_type(first)) {
                case hlist_node:
                case vlist_node:
                    if (tex_has_box_option(first, box_option_no_snapping)) {
                        /* ignore this box */
                    } else if (! ignore && (enforce || (specification->method & snapping_method_list) || tex_has_box_option(first, box_option_snapping))) {
                        if (specification->method & snapping_method_threshold) {
                            if (box_height(first) <= specification->top && box_depth(first) <= specification->bottom) {
                                okay = 1;
                            }
                        }
                    } else if (box_height(first) > specification->height || box_depth(first) > specification->depth) {
                        return 0;
                    }
                    break;
                case glyph_node:
                 // if (tex_has_glyph_option(first, glyph_option_snapping)) {
                 //     if (! ignore && (enforce || specification->method & snapping_method_glyph)) {
                 //         if (specification->method & snapping_method_threshold) {
                 //             if (glyph_height(first) <= specification->top && glyph_depth(first) <= specification->bottom) {
                 //                 okay = 1;
                 //             }
                 //         }
                 //     }
                 // } else { 
                 //     if (tex_char_height_from_glyph(first) > specification->height || tex_char_depth_from_glyph(first) > specification->depth) {
                 //         return 0;
                 //     }
                 // }
                    break;
                case rule_node:
                    if (tex_has_rule_option(first, rule_option_no_snapping)) {
                        /* ignore this rule */
                    } else if (tex_has_rule_option(first, rule_option_snapping)) {
                        if (! ignore && (enforce || specification->method & snapping_method_rule)) {
                            /* running rule ht or dp adapts anyway */
                            if ((rule_height(first) != null_flag && rule_height(first) <= specification->top   )
                             && (rule_depth (first) != null_flag && rule_depth (first) <= specification->bottom)) {
                                    okay = 1;
                            }
                        }
                    } else if (rule_height(first) > specification->height || rule_depth(first) > specification->depth) {
                        return 0;
                    }
                    break;
                case math_node:
                    {
                        switch (node_subtype(first)) {
                            case begin_inline_math:
                            case begin_broken_math:
                                if (tex_has_math_option(first, math_option_snapping)) {
                                    ignore  = ! (specification->method & snapping_method_math);
                                    enforce = 1;
                                } else {
                                    ignore  = 1;
                                    enforce = 0;
                                }
                                break;
                            case end_inline_math:
                            case end_broken_math:
                                ignore  = 0;
                                enforce = 0;
                                break;
                        }
                    }
                    break;
            }
            if (first == last) { 
                return okay; 
            } else {
                first = node_next(first);
            }
        }
    }
    return okay; 
}

/*tex
    Here we have native snapping features, although they still assume control from the macro
    package end, maybe with a bit of \LUA\ help. Consider this experimental until it's used
    in \CONTEXT\ and documented there.

    We use an approach similar to par and balance passes, that is: we use specifications. For
    now we have one step but we might end up with more steps if we find a reason and variant
    approaches.
*/

/*tex

    There is a pitfall here. Because we have a macro language, we cannot be sure that when we
    have scanned a snapping then before we apply it, the related setting has been adapted. This
    means that we have to make a copy unless it is a constant one.

*/

halfword tex_snapping_scan(void)
{
    do {
        tex_get_x_token();
    } while (cur_cmd == spacer_cmd);
    switch (cur_cmd) {
        case specificationspec_cmd:
            {
                halfword snapping = eq_value(cur_cs);
                if (node_subtype(snapping) == line_snapping_code) {
                    return tex_copy_specification_node(snapping);
                }
            }
            break;
        case specification_cmd:
            if (cur_chr) {
                quarterword code = (quarterword) internal_specification_number(cur_chr);
                if (code == line_snapping_code) {
                    return tex_copy_specification_node(eq_value(cur_chr)); /* line_snapping_par */
                }
            }
            break;
    }
    tex_handle_error(
        back_error_type,
        "Missing or invalid snapping specification",
        "I expect to see a pre-defined snapping specification."
    );
    return null;
}

/*tex
    Because we want to trace we need a few more variables than optimal but the overhead can be
    neglected. The next helper is not that efficient, in the sense that it checks and sets up 
    conditions every time, which in a loop over a list iadd overhead. On the other hand, the 
    number of boxes that need snapping is normally not that large. 
*/

static inline scaled tex_aux_snap_dimen(scaled amount, halfword local_factor, halfword global_factor)
{
    if (amount) {
        /*tex
            We need to use the \TEX\ scaler and can't use double magic here! But when we
            go for an interval, then we can go double.
        */
        if (global_factor != scaling_factor) {
            amount = tex_xn_over_d_factor(amount, global_factor);
        }
        if (local_factor != scaling_factor) {
            amount = tex_xn_over_d_factor(amount, local_factor);
        }
    }
    return amount;
}

void tex_snapping_done(halfword *ht, halfword *dp, halfword snapping)
{
    if (snapping && specification_count(snapping) > 0) {
        int options = tex_get_line_snapping_options(snapping, 1);
        int factors = specification_factors(snapping);
        int global = specification_global(snapping);
        scaled amount = glue_amount(baseline_skip_par);
        scaled height = tex_get_line_snapping_height(snapping, 1);
        scaled depth  = tex_get_line_snapping_depth(snapping, 1);
        if (factors) {
            height = tex_aux_snap_dimen(amount, factors ? height : scaling_factor, global ? line_snapping_ht_factor_par : scaling_factor);
            depth  = tex_aux_snap_dimen(amount, factors ? depth  : scaling_factor, global ? line_snapping_dp_factor_par : scaling_factor);
        }
        if (height > 0 && depth > 0) {

    // if (keep && *ht < height && *dp < depth) {
    //      return;
    // }

            scaled tolerance = line_snapping_tolerance_par;
            scaled oldht = *ht;
            scaled olddp = *dp;
            scaled httolerance = tex_get_line_snapping_httolerance(snapping, 1);
            scaled dptolerance = tex_get_line_snapping_dptolerance(snapping, 1);
            scaled newht = height;
            scaled newdp = depth;
            scaled th, td;
            if (factors) {
                httolerance = tex_aux_snap_dimen(amount, factors ? httolerance : scaling_factor, global ? line_snapping_ht_factor_par : scaling_factor);
                dptolerance = tex_aux_snap_dimen(amount, factors ? dptolerance : scaling_factor, global ? line_snapping_dp_factor_par : scaling_factor);
            }
            th = oldht - httolerance - tolerance;
            td = olddp - dptolerance - tolerance;
            if (th < height) {
                *ht = newht;
            }
            if (td < depth) {
                *dp = newdp;
            }
            if (*ht > height || *dp > depth) {
                halfword step = tex_get_line_snapping_step(snapping, 1);
                int nh = 0;
                int nd = 0;
                if (step == 1 || step == 2) {
                    scaled ln = height + depth;
                    if (step == 1) {
                        while (newht < th) {
                            newht += ln;
                            nh++;
                        }
                        while (newdp < td) {
                            newdp += ln;
                            nd++;
                        }
                    } else {
                        while (newht < th) {
                            newht += ln/2;
                            nh++;
                        }
                        while (newdp < td) {
                            newdp += ln/2;
                            nd++;
                        }
                        if ((nh + nd) % 2) {
                            if (nh < nd) {
                                newht += ln/2;
                                nh++;
                            } else {
                                newdp += ln/2;
                                nd++;
                            }
                        } else {
                            /* we're fine */
                        }
                        if (ln % 2) {
                            /* We're off, is the next hack okay? */
                            newht += nh;
                            newdp += nd;
                        }
                    }
                } else {
                    newht = *ht;
                    newdp = *dp;
                }
                if (options & line_snapping_option_top) {
                 // if (options & line_snapping_option_line) {
                 // } else {
                        scaled delta = newht - height;
                        newdp = newdp + delta;
                        newht = height;
                 // }
                } else if (options & line_snapping_option_bottom) {
                 // if (options & line_snapping_option_line) {
                 // } else {
                       scaled delta = newdp - depth;
                        newht = newht + delta;
                        newdp = depth;
                 // }
                }
                *ht = newht;
                *dp = newdp;
                if (tracing_snapping_par > 0) {
                    tex_begin_diagnostic();
                    tex_print_format(
                        "%l[snapping: old (%p,%p), new (%p,%p), line (%p,%p), tolerance (%p,%p), step %i, applied (%i,%i)]",
                        oldht, olddp, newht, newdp,
                        height, depth, httolerance, dptolerance,
                        step, nh, nd
                    );
                    tex_end_diagnostic();
                }
            } else if (tracing_snapping_par > 0) {
                tex_begin_diagnostic();
                if (httolerance || dptolerance) {
                    tex_print_format(
                        "%l[snapping: old (%p,%p), new (%p,%p), line (%p,%p), tolerance (%p,%p)]",
                        oldht, olddp, newht, newdp,
                        height, depth, httolerance, dptolerance
                    );
                } else {
                    tex_print_format(
                        "%l[snapping: old (%p,%p), new (%p,%p), line (%p,%p)]",
                        oldht, olddp, newht, newdp,
                        height, depth
                    );
                }
                tex_end_diagnostic();
            }
        }
    }
}

void tex_snapping_line(halfword box, halfword snapping)
{
    if (! tex_has_box_option(box, box_option_no_snapping)) {
        if (! is_box_snapped_state(box)) {
         // tex_snapping_done(&(box_height(box)), &(box_depth(box)), snapping);
            scaled ht = box_height(box);
            scaled dp = box_depth(box);
            box_natural_height(box) = ht;
            box_natural_depth(box) = dp;
            if (tex_has_box_option(box, box_option_no_snapping)) {
                /* ignore */
            } else {
                tex_snapping_done(&ht, &dp, snapping);
                box_height(box) = ht;
                box_depth(box) = dp;
            }
            set_box_snapped_state(box);
        }
    }
}

halfword tex_snapping_rule(halfword rule, halfword snapping, quarterword subtype)
{
    if (! tex_has_rule_option(rule, rule_option_no_snapping)) {
        switch (node_subtype(rule)) {
            case normal_rule_subtype:
            case empty_rule_subtype:
                {
                    scaled oldwd = rule_width(rule);
                    scaled oldht = rule_height(rule);
                    scaled olddp = rule_depth(rule);
                    if (oldwd == null_flag || (oldht == null_flag && olddp == null_flag)) {
                        /* Definitely not as we pack (one can use uleaders). */
                    } else {
                        scaled newht = oldht;
                        scaled newdp = olddp;
                        if (oldht == null_flag) {
                            newht = 0;
                        }
                        if (olddp == null_flag) {
                            newdp = 0;
                        }
                        tex_snapping_done(&newht, &newdp, snapping);
                        if (newht != oldht || newdp != olddp) {
                            halfword prev = node_prev(rule);
                            halfword next = node_next(rule);
                            halfword list = tex_new_node(hlist_node, subtype);
                            /* we could pack twice when we have a nulkl_flag */
                            box_natural_height(list) = oldht;
                            box_natural_depth(list) = olddp;
                            /* */
                            node_prev(rule) = null;
                            node_next(rule) = null;
                            box_list(list) = rule;
                            tex_attach_attribute_list_copy(list, rule);
                            tex_try_couple_nodes(prev, list);
                            tex_try_couple_nodes(list, next);
                            box_width(list) = oldwd;
                            if (oldht != null_flag) {
                                box_height(list) = newht;
                            }
                            if (olddp != null_flag) {
                                box_depth(list) = newdp;
                            }
                            set_box_snapped_state(list);
                            return list;
                        }
                    }
                    break;
                }
        }
    }
    return rule;
}

halfword tex_snapping_list(halfword head, halfword tail, halfword snapping)
{
    halfword current = head;
    while (current) {
        int last = current == tail;
        switch (node_type(current)) {
            case hlist_node:
            case vlist_node:
                tex_snapping_line(current, snapping);
                break;
            case rule_node:
                {
                    int initial = current == head;
                    current = tex_snapping_rule(current, snapping, math_rule_list);
                    if (initial) {
                      head = current;
                    }
                    break;
                }
        }
        if (last) {
            break;
        } else {
            current = node_next(current);
        }
    }
    return head;
}
