class Layer {
    constructor(seed, id=0, parent_layer=undefined, is_ngminus=false) {
        this.parent_layer = parent_layer;
        this.is_ngminus = is_ngminus;

        this.id = id;

        this.points = new Decimal(0);

        this.upgrades = {};

        this.child_left = undefined;
        this.child_right = undefined;

        this.boost = new Decimal(1);

        this.points_name = "";

        if (parent_layer !== undefined) {
            if (this.is_ngminus) parent_layer.child_left = this;
            else parent_layer.child_right = this;

            if (this.is_ngminus) {
                this.upgrade_time = parent_layer.upgrade_time.mul(2);
                this.final_goal = parent_layer.final_goal.pow(1.2);
            }
            else {
                this.upgrade_time = parent_layer.upgrade_time.mul(1.5);
                this.final_goal = parent_layer.final_goal.pow(3);
            }

            this.name = parent_layer.name;
            if (this.name == "Original") this.name = "NG";
            if (this.is_ngminus) this.name += "-";
            else this.name += "+";

            this.depth = parent_layer.depth + 1;

            this.coord = 2 * parent_layer.coord;
            if (!this.is_ngminus) this.coord += 1;
            this.coord = this.coord % (2 ** 32);

            if (this.is_ngminus) this.color = mixColors(parent_layer.color, [72, 159, 181]);
            else this.color = mixColors(parent_layer.color, [214, 40, 40]);
        }
        else {
            this.upgrade_time = new Decimal(10);
            this.final_goal = new Decimal(1e10);
            this.name = "Original";
            this.depth = 0;
            this.coord = 0;
            this.color = [19, 138, 54];
        }

        this.left_branch = false;
        this.right_branch = false;

        this.rng = sfc32(this.depth, this.coord, seed, 0xDEADBEEF);
        for (let i = 0; i < 15; i++) this.rng();

        if (parent_layer != undefined) this.points_name = choose(ITY_WORDS, this.rng);

        this.el = document.createElement("div");
        this.el.className = "tree-node-container";
        if (parent_layer === undefined) {
            this.el.style.top = "0px";
            this.el.style.left = "0px";
            this.el.style.transform = "translate(-50%, -50%)";
            document.getElementById("tree").appendChild(this.el);
        } else {
            this.el.style.top = "15em";
            this.el.style.left = (this.is_ngminus ? "-" : "") + "10em";
            this.el.style.transform = "";
            parent_layer.el.appendChild(this.el);
        }

        this.nodeEl = document.createElement("div");
        this.nodeEl.onclick = () => this.selectLayer();
        this.nodeEl.className = "tree-node";
        this.nodeEl.style.backgroundColor = formAsRGB(this.color);
        this.el.appendChild(this.nodeEl);

        this.label = document.createElement("p");
        this.label.className = "node-text";
        this.label.innerText = parent_layer === undefined ? "OG" : this.points_name.slice(0, 3);
        this.nodeEl.appendChild(this.label);

        this.unlockReq = document.createElement("p");
        this.unlockReq.className = "unlock-req";
        this.unlockReq.innerText = `Get ${formatNumber(this.final_goal)} ${this.points_name ? this.points_name + " points" : "points"} to unlock`;
        this.el.appendChild(this.unlockReq);

        this.generateUpgrades();
        this.balanceUpgrades();
        this.screenUpdate();
    }

    generateUpgrade() {
        let type_probs = {
            "add": Object.keys(this.upgrades).length == 0 ? (this.depth == 0 ? 0 : 1) : 1 / Object.keys(this.upgrades).length,
            "mul": 0.5,
            "pow": Math.pow(Object.keys(this.upgrades).length, 2) / 100,
            "mul_log": Math.pow(Object.keys(this.upgrades).length, 0.5) / 10,
            "mul_pow": 0//Math.pow(Object.keys(this.upgrades).length, 1) / 50
        }
        let type = chooseDict(type_probs, this.rng);

        let target_probs = {
            "points": 10
        }
        if (type != "mul_log" && type != "mul_pow") {
            for (let key of Object.keys(this.upgrades)) {
                if (this.upgrades[key].type == type) continue;
                if (this.upgrades[key].type == "mul_log" || this.upgrades[key].type == "mul_pow" || this.upgrades[key].target != "points") continue;
                if (type == "mul" && this.upgrades[key].type != "pow") target_probs[key] = 1;
                if (type == "pow" && this.upgrades[key].type != "add") target_probs[key] = 2;
            }
        }
        let target = chooseDict(target_probs, this.rng);

        let upgrade = new Upgrade(this, this.depth + "_" + Object.keys(this.upgrades).length, type, 0, target, 0, this.rng);

        this.upgrades[upgrade.id] = upgrade;
    }

    generateUpgrades() {
        if (this.is_ngminus) for (let i = 0; i < 4; i++) this.generateUpgrade();
        if (this.parent_layer == undefined) for (let i = 0; i < 8; i++) this.generateUpgrade();
        else {
            for (let key of Object.keys(this.parent_layer.upgrades)) {
                this.upgrades[key] = new Upgrade(this, key, 0, 0, "points", 0);
                this.upgrades[key].copyUpgrade(this.parent_layer.upgrades[key]);
            }
            if (!this.is_ngminus) for (let i = 0; i < 4; i++) this.generateUpgrade();
        }
    }

    balanceUpgrades() {
        let upgrades_left = Object.keys(this.upgrades).length;
        let last_target = new Decimal(1);
        let inflation_precaution = 1;
        for (let key of Object.keys(this.upgrades)) {
            let separation_pow = upgrades_left;
            if (this.upgrades[key].type == "add") separation_pow = Math.pow(separation_pow, 1.9 + 0.2 * this.rng());
            if (this.upgrades[key].type == "mul") separation_pow = Math.pow(separation_pow, 1.15 + 0.2 * this.rng());
            if (this.upgrades[key].type == "pow") separation_pow = Math.pow(separation_pow, 0.65 + 0.2 * this.rng());
            if (this.upgrades[key].type == "mul_log") separation_pow = Math.pow(separation_pow, 0.9 + 0.2 * this.rng());
            if (this.upgrades[key].type == "mul_pow") separation_pow = Math.pow(separation_pow, 0.6 + 0.2 * this.rng());

            let base_production = this.calculateProduction(this.depth == 0 ? 1 : 0.1 / this.depth, last_target);
            let base_last_target = last_target.max(base_production.mul(this.upgrade_time)).min(this.final_goal.div(last_target).pow(1 / Math.pow(separation_pow, 0.25)).mul(last_target));

            let base_target = properPrecision(this.final_goal.div(base_last_target).pow(1 / separation_pow).mul(base_last_target).round().max(last_target.add(1)).min(this.final_goal), 1);

            let target_production = new Decimal(base_target);
            target_production = target_production.div(this.upgrade_time).max(base_production);

            this.upgrades[key].cost = new Decimal(last_target);

            //console.log(key + ", base: " + formatNumber(base_production));
            //console.log(key + ", target: " + formatNumber(target_production));

            if (this.upgrades[key].target != "points") {
                let root_upgrade = this.upgrades[key].target;
                while (this.upgrades[root_upgrade].target != "points") root_upgrade = this.upgrades[root_upgrade].target;
                base_production = new Decimal(this.depth == 0 ? 1 : 0.1 / this.depth);
                for (let key2 of Object.keys(this.upgrades)) {
                    if (this.upgrades[key2].target != "points") continue;
                    base_production = this.upgrades[key2].applyEffect(base_production, last_target);
                    if (key2 == root_upgrade) break;
                }
                for (let key2 of Object.keys(this.upgrades).reverse()) {
                    if (key2 == root_upgrade) break;
                    if (this.upgrades[key2].target != "points") continue;
                    target_production = this.upgrades[key2].applyReverseEffect(target_production, last_target);
                }
                if (this.upgrades[root_upgrade].type == "add") {
                    //console.log(key + " unraveling, base: " + formatNumber(base_production));
                    //console.log(key + " unraveling, target: " + formatNumber(target_production));
                    let other_prod = base_production.sub(this.upgrades[root_upgrade].applyEffect(1));
                    //console.log("other_prod: " + formatNumber(other_prod));
                    base_production = this.upgrades[root_upgrade].applyEffect(1);
                    target_production = target_production.sub(other_prod);
                }
                if (this.upgrades[root_upgrade].type == "mul") {
                    //console.log(key + " unraveling, base: " + formatNumber(base_production));
                    //console.log(key + " unraveling, target: " + formatNumber(target_production));
                    let other_prod = base_production.div(this.upgrades[root_upgrade].applyEffect(1));
                    //console.log("other_prod: " + formatNumber(other_prod));
                    base_production = this.upgrades[root_upgrade].applyEffect(1);
                    target_production = target_production.div(other_prod);
                }
                if (this.upgrades[root_upgrade].type == "pow") {
                    //console.log(key + " unraveling, base: " + formatNumber(base_production));
                    //console.log(key + " unraveling, target: " + formatNumber(target_production));
                    let other_prod = base_production.root(this.upgrades[root_upgrade].applyEffect(1)).max(2);
                    //console.log("other_prod: " + formatNumber(other_prod));
                    base_production = this.upgrades[root_upgrade].applyEffect(1);
                    target_production = target_production.log(other_prod);
                }

                //console.log(key + ", result base: " + formatNumber(base_production));
                //console.log(key + ", result target: " + formatNumber(target_production));
            }

            //if (this.upgrades[key].type == "add") console.log("base result: " + formatNumber(new Decimal(target_production.sub(base_production))));
            //if (this.upgrades[key].type == "mul") console.log("base result: " + formatNumber(new Decimal(target_production.div(base_production))));
            //if (this.upgrades[key].type == "pow") console.log("base result: " + formatNumber(new Decimal(target_production.log(base_production.max(2)))));
            //if (this.upgrades[key].type == "mul_log") console.log("base result: " + formatNumber(new Decimal(target_production.div(base_production).log(last_target.max(1).log10().max(2)))));

            this.upgrades[key].bought = true;

            if (this.upgrades[key].type == "add") this.upgrades[key].effect = properPrecision(new Decimal(target_production.sub(base_production).max(this.upgrades[key].target == "points" ? 1 : 0.001)), 0);
            if (this.upgrades[key].type == "mul") this.upgrades[key].effect = properPrecision(new Decimal(target_production.div(base_production).max(1.1)), 1);
            if (this.upgrades[key].type == "pow") this.upgrades[key].effect = properPrecision(new Decimal(target_production.log(base_production.max(2)).max(1.001)), 3);
            if (this.upgrades[key].type == "mul_log") this.upgrades[key].effect = new Decimal(target_production.div(base_production).log(last_target.max(1).log10().max(2)).max(0.1));
            if (this.upgrades[key].type == "mul_pow") {
                this.upgrades[key].effect = new Decimal(target_production.div(base_production).log(last_target.max(2)).max(0.001).min(inflation_precaution * 0.3));
                inflation_precaution -= this.upgrades[key].effect.toNumber();
            }

            last_target = new Decimal(base_target);
            upgrades_left -= 1;
        }

        for (let key of Object.keys(this.upgrades)) this.upgrades[key].bought = false;
    }

    calculateProduction(base=1, total=this.points, ignore_add=false) {
        let production = new Decimal(base);
        for (let key of Object.keys(this.upgrades)) {
            if (this.upgrades[key].target == "points" && (!ignore_add || this.upgrades[key].type != "add")) production = this.upgrades[key].applyEffect(production, total);
        }
        production = production.mul(this.boost);
        return production;
    }

    calculateReverseProduction(base=1, total=this.points, ignore_add=false) {
        let production = new Decimal(base);
        production = production.div(this.boost);
        for (let key of Object.keys(this.upgrades).reverse()) {
            if (this.upgrades[key].target == "points" && (!ignore_add || this.upgrades[key].type != "add")) production = this.upgrades[key].applyReverseEffect(production, total);
        }
        return production;
    }

    getBoostValue() {
        let boost = this.points.add(1).log(this.final_goal).pow(0.5).mul(3).add(1);
        // Softcaps
        if (boost.gt(10)) boost = boost.div(10).log10().add(1).mul(10);
        if (boost.gt(1000)) boost = boost.div(1000).log10().add(1).mul(1000);
        if (boost.gt(1e10)) boost = boost.div(1e10).log10().add(1).mul(1e10);

        return boost;
    }

    propagateBoost() {
        this.boost = new Decimal(1);
        if (this.child_left != undefined) this.child_left.propagateBoost();
        if (this.child_right != undefined) this.child_right.propagateBoost();
        if (this.parent_layer != undefined) this.parent_layer.boost = this.parent_layer.boost.mul(this.boost).mul(this.getBoostValue());
    }

    processTimedelta(delta) {
        this.points = this.points.add(this.calculateProduction(this.depth == 0 ? 1 : 0).mul(delta / 1000));
        if (this.right_branch) this.points = this.points.add(this.prestigeGain().mul(delta / 1000));
        if ((this.child_left == undefined || this.child_right == undefined) && this.points.gt(this.final_goal)) {
            player.layers.push(new Layer(player.seed, player.layers.length, this, true));
            player.layers.push(new Layer(player.seed, player.layers.length, this, false));
        }
    }

    screenUpdate() {
        this.unlockReq.style.visibility = this.child_left === undefined || this.child_right === undefined ? "" : "hidden";
        let purchaseAvailable = Object.values(this.upgrades).some(upg => !upg.bought && upg.canBuy()) ||
                                (this.parent_layer != undefined && this.child_left != undefined && this.child_left.points.gte(this.child_left.final_goal) && !this.left_branch) ||
                                (this.parent_layer != undefined && this.child_right != undefined && this.child_right.points.gte(this.child_right.final_goal) && !this.right_branch);
        let ascensionAvailable = Object.values(this.upgrades).some(upg => !upg.bought && !upg.canBuy() && this.points.add(this.prestigeGain()).gte(upg.cost));
        this.nodeEl.className = `tree-node${ascensionAvailable ? ' ascensionAvailable' : ''}${purchaseAvailable ? ' purchaseAvailable' : ''}`;
    }

    screenUpdateCurrent() {
        let layer_container = document.getElementById('layer_info');

        layer_container.style.setProperty("--color-layer", formAsRGB(this.color));

        layer_container.getElementsByClassName('type')[0].textContent = this.name;
        layer_container.getElementsByClassName('point-amount')[0].textContent = formatNumber(this.points, true, true);
        layer_container.getElementsByClassName('gain-amount')[0].textContent = formatNumber(this.calculateProduction(this.depth == 0 ? 1 : 0), true);

        layer_container.getElementsByClassName('boost-from-value')[0].textContent = formatNumber(this.boost, true);
        layer_container.getElementsByClassName('boost-to-value')[0].textContent = formatNumber(this.getBoostValue(), true);

        if (this.parent_layer == undefined) layer_container.getElementsByClassName('boost-to')[0].style.visibility = "hidden";
        else layer_container.getElementsByClassName('boost-to')[0].style.visibility = "";

        for (let element of layer_container.getElementsByClassName('point-name')) {
            if (this.points_name == "") element.textContent = "points";
            else element.textContent = this.points_name + " points";
        }

        for (let element of layer_container.getElementsByClassName('prev-point-name')) {
            if (this.parent_layer == undefined) element.textContent = "April fools";
            else if (this.parent_layer.points_name == "") element.textContent = "points";
            else element.textContent = this.parent_layer.points_name + " points";
        }

        if (this.parent_layer == undefined) layer_container.getElementsByClassName('prestige')[0].style.visibility = "hidden";
        else layer_container.getElementsByClassName('prestige')[0].style.visibility = "";

        if (this.parent_layer == undefined || this.child_left == undefined) document.getElementById("qol_left").style.visibility = "hidden";
        else {
            document.getElementById("qol_left").style.visibility = "";
            document.getElementById("qol_left").disabled = this.child_left.points.lt(this.child_left.final_goal);
            if (this.left_branch) document.getElementById("qol_left").classList.add("complete");
            else document.getElementById("qol_left").classList.remove("complete");
            layer_container.getElementsByClassName('left-child-req')[0].textContent = formatNumber(this.child_left.final_goal, true);
            layer_container.getElementsByClassName('left-child-name')[0].textContent = this.child_left.points_name + " points";
        }

        if (this.parent_layer == undefined || this.child_right == undefined) document.getElementById("qol_right").style.visibility = "hidden";
        else {
            document.getElementById("qol_right").style.visibility = "";
            document.getElementById("qol_right").disabled = this.child_right.points.lt(this.child_right.final_goal);
            if (this.right_branch) document.getElementById("qol_right").classList.add("complete");
            else document.getElementById("qol_right").classList.remove("complete");
            layer_container.getElementsByClassName('right-child-req')[0].textContent = formatNumber(this.child_right.final_goal, true);
            layer_container.getElementsByClassName('right-child-name')[0].textContent = this.child_right.points_name + " points";
        }

        if (this.canPrestige()) {
            layer_container.getElementsByClassName('prestige')[0].disabled = false;
            layer_container.getElementsByClassName('cannot-prestige')[0].style.display = "none";
            layer_container.getElementsByClassName('can-prestige')[0].style.display = "";
        }
        else {
            layer_container.getElementsByClassName('prestige')[0].disabled = true;
            layer_container.getElementsByClassName('cannot-prestige')[0].style.display = "";
            layer_container.getElementsByClassName('can-prestige')[0].style.display = "none";
        }

        for (let element of layer_container.getElementsByClassName('prestige-need')) {
            element.textContent = formatNumber(this.prestigeNeed().add(1), true, true);
        }

        if (this.prestigeGain().gt(100)) layer_container.getElementsByClassName('next-at')[0].style.display = "none";
        else layer_container.getElementsByClassName('next-at')[0].style.display = "";

        layer_container.getElementsByClassName('prestige-gain')[0].textContent = formatNumber(this.prestigeGain(), true, true);

        for (let key of Object.keys(this.upgrades)) {
            this.upgrades[key].screenUpdate();
        }
    }

    selectLayer(forceZoom, instant = false) {
        let layer_container = document.getElementById('layer_info');
        let upgrade_container = layer_container.getElementsByClassName('upgrades-list')[0];

        let upgrade_elements = "";
        for (let key of Object.keys(this.upgrades)) {
            upgrade_elements += '<button class="upgrade" id="' + key + '" onclick="player.current_layer.upgrades[this.id].buy()"><div class="content"><p class="upgrade-name">&nbsp;</p><p class="upgrade-desc">' + this.upgrades[key].getDescCode() + '</p><p class="divider"></p><p class="upgrade-cost">Cost: <span class="cost"></span> <span class="point-name"></span></p></div></button>';
        }

        upgrade_container.innerHTML = upgrade_elements;

        const shouldZoom = player.current_layer === this || player.singleclick;
        player.current_layer = this;

        if (shouldZoom || forceZoom === true) {
            const treeContainer = document.getElementById("tree-container").getBoundingClientRect();
            const zoom = Decimal.pow(2, this.depth).times(player.zoomModifier).toNumber();
            const nodeRect = this.el.getBoundingClientRect();
            const rootRect = player.layers[0].el.getBoundingClientRect();
            const x = (rootRect.x + rootRect.width / 2 - nodeRect.x - nodeRect.width / 2) / panzoom.getScale() + treeContainer.width / 2;
            const y = (rootRect.y + rootRect.height / 2 - nodeRect.y - nodeRect.height / 2) / panzoom.getScale() + treeContainer.height / 2 / zoom;
            panzoom.zoom(zoom, { animate: !instant && player.animations });
            panzoom.pan(x, y, { animate: !instant && player.animations });
        }

        screenUpdate();
    }

    canPrestige() {
        return this.prestigeGain().gt(0);
    }

    prestigeGain() {
        if (this.parent_layer == undefined) return new Decimal(0);

        return this.calculateProduction(this.parent_layer.points.max(1).log(this.parent_layer.final_goal).pow(Math.log2(Math.max(1, this.depth) + 1)), this.points, true).floor();
    }

    prestigeNeed() {
        if (this.parent_layer == undefined) return new Decimal(0);

        return this.parent_layer.final_goal.pow(this.calculateReverseProduction(this.prestigeGain().add(1), this.points, true).pow(1 / Math.log2(Math.max(1, this.depth) + 1)));
    }

    prestige() {
        this.points = this.points.add(this.prestigeGain()).round();

        if (this.parent_layer != undefined) this.parent_layer.reset();
        screenUpdate();
    }

    reset() {
        if (!this.left_branch) {
            for (let key of Object.keys(this.upgrades)) {
                this.upgrades[key].bought = false;
            }
        }
        this.points = new Decimal(0);
        //if (this.parent_layer != undefined) this.parent_layer.reset();
    }

    buyLeft() {
        if (this.child_left == undefined) return;
        if (this.child_left.points.lt(this.child_left.final_goal)) return;
        if (this.left_branch) return;
        this.left_branch = true;
        this.child_left.points = this.child_left.points.sub(this.child_left.final_goal);
    }

    buyRight() {
        if (this.child_right == undefined) return;
        if (this.child_right.points.lt(this.child_right.final_goal)) return;
        if (this.right_branch) return;
        this.right_branch = true;
        this.child_right.points = this.child_right.points.sub(this.child_right.final_goal);
    }

    save() {
        let data = [];
        data.push(this.id);
        if (this.parent_layer != undefined) data.push(this.parent_layer.id);
        else data.push(-1);
        data.push(this.is_ngminus);
        data.push([this.points.sign, this.points.layer, this.points.mag]);
        data.push([this.upgrade_time.sign, this.upgrade_time.layer, this.upgrade_time.mag]);
        data.push([this.final_goal.sign, this.final_goal.layer, this.final_goal.mag]);
        data.push(this.name);
        data.push(this.points_name);
        data.push(this.depth);
        data.push(this.color);

        let upgrade_data = [];
        for (let key of Object.keys(this.upgrades)) upgrade_data.push(this.upgrades[key].save());

        data.push(upgrade_data);

        data.push(this.left_branch);
        data.push(this.right_branch);
        return data;
    }

    load(player, data) {
        this.id = data[0];
        if (data[1] == -1) this.parent_layer = undefined;
        else this.parent_layer = player.layers[data[1]];
        this.is_ngminus = data[2];
        this.points.fromComponents(data[3][0], data[3][1], data[3][2]);
        this.upgrade_time.fromComponents(data[4][0], data[4][1], data[4][2]);
        this.final_goal.fromComponents(data[5][0], data[5][1], data[5][2]);
        this.name = data[6];
        this.points_name = data[7];
        this.depth = data[8];
        this.coord = 0;
        this.color = data[9];

        this.upgrades = {};
        for (let upg of data[10]) {
            this.upgrades[upg[0]] = new Upgrade();
            this.upgrades[upg[0]].load(this, upg);
        }

        if (data.length > 11) this.left_branch = data[11];
        if (data.length > 12) this.right_branch = data[12];

        this.child_left = undefined;
        this.child_right = undefined;

        if (this.parent_layer != undefined) {
            if (this.is_ngminus) this.parent_layer.child_left = this;
            else this.parent_layer.child_right = this;

            this.coord = 2 * this.parent_layer.coord;
            if (!this.is_ngminus) this.coord += 1;
            this.coord = this.coord % (2 ** 32);
        }

        this.nodeEl.style.backgroundColor = formAsRGB(this.color);
        if (this.parent_layer === undefined) {
            this.el.style.top = "0px";
            this.el.style.left = "0px";
            this.el.style.transform = "translate(-50%, -50%)";
            document.getElementById("tree").appendChild(this.el);
        } else {
            this.el.style.top = "15em";
            this.el.style.left = (this.is_ngminus ? "-" : "") + "10em";
            this.el.style.transform = "";
            this.parent_layer.el.appendChild(this.el);
        }

        this.label.innerText = this.parent_layer === undefined ? "OG" : this.points_name.slice(0, 3);

        this.unlockReq.innerText = `Get ${formatNumber(this.final_goal)} ${this.points_name ? this.points_name + " points" : "points"} to unlock`;
    }
};