"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RuleSet = exports.NTRuleStub = exports.Rule = void 0;
const bin_util_1 = require("@ot-builder/bin-util");
const errors_1 = require("@ot-builder/errors");
const CffInterp = require("../../../../interp/ir");
const operator_1 = require("../../../../interp/operator");
const interpreter_1 = require("../../../read/interpreter");
const encoder_1 = require("../../encoder");
const mir_1 = require("../../mir");
const callSizeCache = [];
function estimateCallSize(scEst) {
    if (callSizeCache[scEst])
        return callSizeCache[scEst];
    const bias = (0, interpreter_1.computeSubroutineBias)(scEst);
    const size = encoder_1.CharStringEncoder.measureOperator(operator_1.CharStringOperator.CallGSubr) +
        Math.max(encoder_1.CharStringEncoder.measureInt(-bias), encoder_1.CharStringEncoder.measureInt(0), encoder_1.CharStringEncoder.measureInt(scEst - bias));
    callSizeCache[scEst] = size;
    return size;
}
class Rule {
    constructor(parts) {
        this.parts = parts;
        // Stat flags and properties
        this.stated = false;
        this.subrId = -1;
        this.depth = 0;
        this.staticSize = 0;
        this.selfSize = 0;
        this.expandedSize = 0;
        this.refCount = 0;
        this.inputRefCount = 0;
        this.nonTerminalRefCount = new Map();
    }
    resetStat() {
        this.stated = false;
        this.refCount = 0;
    }
    // How many bytes can we save?
    // scEst: estimated subroutine index length
    // rc: Ref-count of this rule
    utility(limits, scEst, rc) {
        const callSize = estimateCallSize(scEst);
        const indexSize = scEst < 0x10 ? 1 : scEst < 0x1000 ? 2 : scEst < 0x100000 ? 3 : 4;
        return this.selfSize * rc - (indexSize + limits.retSize + this.selfSize + callSize * rc);
    }
}
exports.Rule = Rule;
class NTRuleStub {
    constructor(symbol, parts) {
        this.symbol = symbol;
        this.parts = parts;
    }
}
exports.NTRuleStub = NTRuleStub;
class RuleSet {
    constructor() {
        this.inputRules = [];
        this.nonTerminalRules = [];
    }
    statRule(rule, scEst) {
        let selfSize = rule.staticSize;
        let expandedSize = rule.staticSize;
        for (const ir of rule.parts) {
            switch (ir.type) {
                case mir_1.MirType.NonTerminal: {
                    const innerRule = this.nonTerminalRules[ir.id];
                    if (!innerRule.stated)
                        throw errors_1.Errors.Unreachable();
                    if (innerRule.subrId >= 0) {
                        selfSize += estimateCallSize(scEst);
                    }
                    else {
                        selfSize += innerRule.selfSize;
                    }
                    expandedSize += innerRule.expandedSize;
                    break;
                }
            }
        }
        rule.selfSize = selfSize;
        rule.expandedSize = expandedSize;
        rule.stated = true;
    }
    staticStatRule(rid, rule, isInput) {
        let staticSize = 0;
        for (const ir of rule.parts) {
            switch (ir.type) {
                case mir_1.MirType.Operand: {
                    const size = encoder_1.CharStringEncoder.measureOperand(ir.arg);
                    staticSize += size;
                    break;
                }
                case mir_1.MirType.Operator: {
                    const size = encoder_1.CharStringEncoder.measureOperator(ir.opCode, ir.flags);
                    staticSize += size;
                    break;
                }
                case mir_1.MirType.NonTerminal: {
                    const innerRule = this.nonTerminalRules[ir.id];
                    if (isInput) {
                        innerRule.inputRefCount += 1;
                    }
                    else {
                        innerRule.nonTerminalRefCount.set(rid, 1 + (innerRule.nonTerminalRefCount.get(rid) || 0));
                    }
                }
            }
        }
        rule.staticSize = staticSize;
    }
    staticStatRules() {
        for (let rid = 0; rid < this.nonTerminalRules.length; rid++) {
            this.staticStatRule(rid, this.nonTerminalRules[rid], false);
        }
        for (let rid = 0; rid < this.inputRules.length; rid++) {
            this.staticStatRule(rid, this.inputRules[rid], true);
        }
    }
    dpOptimize(limits, scEst) {
        let sid = 0;
        for (const rule of this.nonTerminalRules) {
            rule.subrId = -1;
            rule.refCount = 0;
            rule.depth = 0;
        }
        for (let rid = this.nonTerminalRules.length; rid-- > 0;) {
            const rule = this.nonTerminalRules[rid];
            let refCount = rule.inputRefCount, depth = 1;
            for (const [outerRid, crossReferences] of rule.nonTerminalRefCount) {
                if (crossReferences <= 0)
                    continue;
                const outerRule = this.nonTerminalRules[outerRid];
                if (outerRule.subrId >= 0) {
                    // Outer rule is a subroutine
                    refCount += crossReferences;
                    if (outerRule.depth + 1 > depth)
                        depth = outerRule.depth + 1;
                }
                else {
                    // Outer rule is inlined
                    refCount += outerRule.refCount * crossReferences;
                    if (outerRule.depth > depth)
                        depth = outerRule.depth;
                }
            }
            rule.refCount = refCount;
            rule.depth = depth;
            const utility = rule.utility(limits, scEst, refCount);
            if (sid < scEst * 2 &&
                sid < limits.maxSubrs &&
                depth < limits.maxRecursion &&
                utility >= 0) {
                rule.subrId = sid++;
            }
            else {
                rule.subrId = -1;
            }
        }
        return sid;
    }
    statRules(scEst) {
        for (const rule of this.inputRules)
            rule.resetStat();
        for (const rule of this.nonTerminalRules)
            rule.resetStat();
        for (const rule of this.nonTerminalRules)
            this.statRule(rule, scEst);
        for (const rule of this.inputRules)
            this.statRule(rule, scEst);
    }
    currentSidPlan() {
        const plan = [];
        for (let ix = 0; ix < this.nonTerminalRules.length; ix++) {
            plan[ix] = this.nonTerminalRules[ix].subrId;
        }
        return plan;
    }
    optimize(limits) {
        // Initialize with "no subroutines"
        for (const rule of this.nonTerminalRules)
            rule.subrId = -1;
        // Best SubrID plan so far
        let bestSize = 0x7fffffff;
        let bestSidPlan = this.currentSidPlan();
        // Estimated subroutine index size and do initial stat
        let srCount = limits.maxSubrs;
        this.staticStatRules();
        this.statRules(srCount);
        // Run optimization
        for (let round = 0; round < 8; round++) {
            // Do a DP optimize then update the rules' size
            srCount = this.dpOptimize(limits, srCount);
            this.statRules(srCount);
            // Get current blobs' size
            let currentSize = 0;
            for (const rule of this.inputRules) {
                currentSize += rule.selfSize;
            }
            for (const rule of this.nonTerminalRules) {
                if (rule.subrId >= 0)
                    currentSize += rule.selfSize;
            }
            // Do we have a better result?
            const currentPlan = this.currentSidPlan();
            if (currentSize < bestSize) {
                bestSize = currentSize;
                bestSidPlan = currentPlan;
            }
            else {
                // Stop when stabilized
                let stable = true;
                for (let rid = 0; rid < bestSidPlan.length; rid++) {
                    if (currentPlan[rid] !== bestSidPlan[rid])
                        stable = false;
                }
                if (stable)
                    break;
            }
        }
        // Apply the best plan we have
        for (let ix = 0; ix < this.nonTerminalRules.length; ix++) {
            this.nonTerminalRules[ix].subrId = bestSidPlan[ix];
        }
    }
    // Rule to MirSeq
    *compileRule(rule, scEst, end) {
        const bias = (0, interpreter_1.computeSubroutineBias)(scEst);
        for (const ir of rule.parts) {
            switch (ir.type) {
                case mir_1.MirType.Operand: {
                    yield CffInterp.operand(ir.arg);
                    break;
                }
                case mir_1.MirType.Operator: {
                    yield CffInterp.operator(ir.opCode, ir.flags);
                    break;
                }
                case mir_1.MirType.NonTerminal: {
                    const innerRule = this.nonTerminalRules[ir.id];
                    if (!innerRule)
                        throw errors_1.Errors.Unreachable();
                    if (innerRule.subrId >= 0) {
                        yield CffInterp.operand(innerRule.subrId - bias);
                        yield CffInterp.operator(operator_1.CharStringOperator.CallGSubr);
                    }
                    else {
                        yield* this.compileRule(innerRule, scEst, null);
                    }
                    break;
                }
            }
        }
        if (end)
            yield CffInterp.operator(end);
    }
    encodeRule(rule, scEst, end) {
        const frag = new bin_util_1.Frag();
        const encoder = new encoder_1.CharStringEncoder(frag);
        const irSeq = this.compileRule(rule, scEst, end);
        for (const ir of irSeq)
            encoder.push(ir);
        return bin_util_1.Frag.pack(frag);
    }
    compile(limits) {
        const subroutines = [];
        const charStrings = [];
        let scEst = 0;
        for (const rule of this.nonTerminalRules) {
            if (rule.subrId + 1 > scEst)
                scEst = rule.subrId + 1;
        }
        for (const rule of this.inputRules) {
            charStrings.push(this.encodeRule(rule, scEst, limits.endCharSize ? operator_1.CharStringOperator.EndChar : null));
        }
        for (const rule of this.nonTerminalRules) {
            if (rule.subrId < 0)
                continue;
            subroutines[rule.subrId] = this.encodeRule(rule, scEst, limits.retSize ? operator_1.CharStringOperator.Return : null);
        }
        return { subroutines, charStrings };
    }
    // Printing
    printRule(rule, header) {
        let s = header;
        for (const ir of rule.parts) {
            switch (ir.type) {
                case mir_1.MirType.Operand: {
                    s += `${ir.arg} `;
                    break;
                }
                case mir_1.MirType.Operator: {
                    s += `${operator_1.CharStringOperator[ir.opCode]}`;
                    if (ir.flags)
                        s += `[${ir.flags.join("")}]`;
                    s += " ";
                    break;
                }
                case mir_1.MirType.NonTerminal: {
                    const innerRule = this.nonTerminalRules[ir.id];
                    if (!innerRule)
                        throw errors_1.Errors.Unreachable();
                    if (innerRule.subrId >= 0)
                        s += `{${innerRule.subrId}} `;
                    else
                        s += `( ${this.printRule(innerRule, "")}) `;
                    break;
                }
            }
        }
        return s;
    }
    ruleHeaderStart(rule, header) {
        return (`${header} :: DE ${rule.depth} ` +
            `RC ${rule.refCount} SS ${rule.selfSize} XS ${rule.expandedSize} :: `);
    }
    printPlan() {
        let s = "";
        for (const [gid, rule] of this.inputRules.entries()) {
            s += this.printRule(rule, this.ruleHeaderStart(rule, `CharString${gid}`)) + "\n";
        }
        for (const [gid, rule] of this.nonTerminalRules.entries()) {
            if (rule.subrId < 0)
                continue;
            s += this.printRule(rule, this.ruleHeaderStart(rule, `Subr${rule.subrId}`)) + "\n";
        }
        return s;
    }
}
exports.RuleSet = RuleSet;
//# sourceMappingURL=rule-set.js.map