// Copyright (c) Vitaliy Filippov, 2019+
// License: VNPL-1.1 (see README.md for details)

// Extract OSDs from the lowest affected tree level into a separate (flat) map
// to run PG optimisation on failure domains instead of individual OSDs
//
// node_list = same input as for index_tree()
// rules = [ level, operator, value ][][]
// returns { nodes: new_node_list, leaves: { new_folded_node_id: [ extracted_leaf_nodes... ] } }
function fold_failure_domains(node_list, rules)
{
    const interest = {};
    for (const level_rules of rules)
    {
        for (const rule of level_rules)
            interest[rule[0]] = true;
    }
    const max_numeric_id = node_list.reduce((a, c) => a < (0|c.id) ? (0|c.id) : a, 0);
    let next_id = max_numeric_id;
    const node_map = node_list.reduce((a, c) => { a[c.id||''] = c; return a; }, {});
    const old_ids_by_new = {};
    const extracted_nodes = {};
    let folded = true;
    while (folded)
    {
        const per_parent = {};
        for (const node_id in node_map)
        {
            const node = node_map[node_id];
            const p = node.parent || '';
            per_parent[p] = per_parent[p]||[];
            per_parent[p].push(node);
        }
        folded = false;
        for (const node_id in per_parent)
        {
            const fold_node = node_id !== '' && per_parent[node_id].length > 0 && per_parent[node_id].filter(child => per_parent[child.id||''] || interest[child.level]).length == 0;
            if (fold_node)
            {
                const old_node = node_map[node_id];
                const new_id = ++next_id;
                node_map[new_id] = {
                    ...old_node,
                    id: new_id,
                    name: node_id, // for use in murmur3 hashes
                    size: per_parent[node_id].reduce((a, c) => a + (Number(c.size)||0), 0),
                };
                delete node_map[node_id];
                old_ids_by_new[new_id] = node_id;
                extracted_nodes[new_id] = [];
                for (const child of per_parent[node_id])
                {
                    if (old_ids_by_new[child.id])
                    {
                        extracted_nodes[new_id].push(...extracted_nodes[child.id]);
                        delete extracted_nodes[child.id];
                    }
                    else
                        extracted_nodes[new_id].push(child);
                    delete node_map[child.id];
                }
                folded = true;
            }
        }
    }
    return { nodes: Object.values(node_map), leaves: extracted_nodes };
}

// Distribute PGs mapped to "folded" nodes to individual OSDs according to their weights
// folded_pgs = optimize_result.int_pgs before folding
// prev_pgs = optional previous PGs from optimize_change() input
// extracted_nodes = output from fold_failure_domains
function unfold_failure_domains(folded_pgs, prev_pgs, extracted_nodes)
{
    const maps = {};
    let found = false;
    for (const new_id in extracted_nodes)
    {
        const weights = {};
        for (const sub_node of extracted_nodes[new_id])
        {
            weights[sub_node.id] = sub_node.size;
        }
        maps[new_id] = { weights, prev: [], next: [], pos: 0 };
        found = true;
    }
    if (!found)
    {
        return folded_pgs;
    }
    for (let i = 0; i < folded_pgs.length; i++)
    {
        for (let j = 0; j < folded_pgs[i].length; j++)
        {
            if (maps[folded_pgs[i][j]])
            {
                maps[folded_pgs[i][j]].prev.push(prev_pgs && prev_pgs[i] && prev_pgs[i][j] || 0);
            }
        }
    }
    for (const new_id in maps)
    {
        maps[new_id].next = adjust_distribution(maps[new_id].weights, maps[new_id].prev);
    }
    const mapped_pgs = [];
    for (let i = 0; i < folded_pgs.length; i++)
    {
        mapped_pgs.push(folded_pgs[i].map(osd => (maps[osd] ? maps[osd].next[maps[osd].pos++] : osd)));
    }
    return mapped_pgs;
}

// Return the new array of items re-distributed as close as possible to weights in wanted_weights
// wanted_weights = { [key]: weight }
// cur_items = key[]
function adjust_distribution(wanted_weights, cur_items)
{
    const item_map = {};
    for (let i = 0; i < cur_items.length; i++)
    {
        const item = cur_items[i];
        item_map[item] = (item_map[item] || { target: 0, cur: [] });
        item_map[item].cur.push(i);
    }
    let total_weight = 0;
    for (const item in wanted_weights)
    {
        total_weight += Number(wanted_weights[item]) || 0;
    }
    for (const item in wanted_weights)
    {
        const weight = wanted_weights[item] / total_weight * cur_items.length;
        if (weight > 0)
        {
            item_map[item] = (item_map[item] || { target: 0, cur: [] });
            item_map[item].target = weight;
        }
    }
    const diff = (item) => (item_map[item].cur.length - item_map[item].target);
    const most_underweighted = Object.keys(item_map)
        .filter(item => item_map[item].target > 0)
        .sort((a, b) => diff(a) - diff(b));
    // Items with zero target weight MUST never be selected - remove them
    // and remap each of them to a most underweighted item
    for (const item in item_map)
    {
        if (!item_map[item].target)
        {
            const prev = item_map[item];
            delete item_map[item];
            for (const idx of prev.cur)
            {
                const move_to = most_underweighted[0];
                item_map[move_to].cur.push(idx);
                move_leftmost(most_underweighted, diff);
            }
        }
    }
    // Other over-weighted items are only moved if it improves the distribution
    while (most_underweighted.length > 1)
    {
        const first = most_underweighted[0];
        const last = most_underweighted[most_underweighted.length-1];
        const first_diff = diff(first);
        const last_diff = diff(last);
        if (Math.abs(first_diff+1)+Math.abs(last_diff-1) < Math.abs(first_diff)+Math.abs(last_diff))
        {
            item_map[first].cur.push(item_map[last].cur.pop());
            move_leftmost(most_underweighted, diff);
            move_rightmost(most_underweighted, diff);
        }
        else
        {
            break;
        }
    }
    const new_items = new Array(cur_items.length);
    for (const item in item_map)
    {
        for (const idx of item_map[item].cur)
        {
            new_items[idx] = item;
        }
    }
    return new_items;
}

function move_leftmost(sorted_array, diff)
{
    // Re-sort by moving the leftmost item to the right if it changes position
    const first = sorted_array[0];
    const new_diff = diff(first);
    let r = 0;
    while (r < sorted_array.length-1 && diff(sorted_array[r+1]) <= new_diff)
        r++;
    if (r > 0)
    {
        for (let i = 0; i < r; i++)
            sorted_array[i] = sorted_array[i+1];
        sorted_array[r] = first;
    }
}

function move_rightmost(sorted_array, diff)
{
    // Re-sort by moving the rightmost item to the left if it changes position
    const last = sorted_array[sorted_array.length-1];
    const new_diff = diff(last);
    let r = sorted_array.length-1;
    while (r > 0 && diff(sorted_array[r-1]) > new_diff)
        r--;
    if (r < sorted_array.length-1)
    {
        for (let i = sorted_array.length-1; i > r; i--)
            sorted_array[i] = sorted_array[i-1];
        sorted_array[r] = last;
    }
}

// map previous PGs to folded nodes
function fold_prev_pgs(pgs, extracted_nodes)
{
    const unmap = {};
    for (const new_id in extracted_nodes)
    {
        for (const sub_node of extracted_nodes[new_id])
        {
            unmap[sub_node.id] = new_id;
        }
    }
    const mapped_pgs = [];
    for (let i = 0; i < pgs.length; i++)
    {
        mapped_pgs.push(pgs[i].map(osd => (unmap[osd] || osd)));
    }
    return mapped_pgs;
}

module.exports = {
    fold_failure_domains,
    unfold_failure_domains,
    adjust_distribution,
    fold_prev_pgs,
};
