<div id="app"></div>

<script>

var N =
{
    ID:{
        Walk:0,
        Instance:0
    },

    Create(inMeta)
    {
        return {
            ID:{
                Walk:0,
                Instance:N.ID.Instance++
            },
            Meta:inMeta||{},
            Link:{}
        };
    },
    Connect(inNodeMajor, inNodeMinor, inKey)
    {
        N.Step(inNodeMajor, inKey, true, true).push(inNodeMinor);
        N.Step(inNodeMinor, inKey, false, true).push(inNodeMajor);
    },
    Disconnect(inNodeMajor, inNodeMinor, inKey)
    {
        let remove = (inArray, inMatch) => inArray.findIndex( (inMember, inIndex, inArray) => (inMember === inMatch) ? inArray.splice(inIndex, 1) : false );

        if(inNodeMinor === null)
        {
            inNodeMajor.Link[inKey].Set.forEach( inLoopNode => 
            {
                if(inLoopNode.Link[inKey].Get.length == 1)
                {
                    // we're about to delete the last get reference on a minor node, just purge the key entirely
                    delete inLoopNode.Link[inKey];
                }
                else
                {
                    remove( inLoopNode.Link[inKey].Get, inNodeMajor);
                }

            });
            // we just wiped out all set, if get is empty we can purge the key from major
            if(inNodeMajor.Link[inKey].Get.length == 0)
            {
                delete inNodeMajor.Link[inKey];
            }
            return;
        }

        if(inNodeMajor === null)
        {
            inNodeMinor.Link[inKey].Get.forEach( inLoopNode => 
            {
                if(inLoopNode.Link[inKey].Set.length == 1)
                {
                    delete inLoopNode.Link[inKey];
                }
                else
                {
                    remove( inLoopNode.Link[inKey].Set, inNodeMinor);
                }
            });
            // we just wiped out all get, if set is empty we can purge the key from minor
            if(inNodeMinor.Link[inKey].Set.length == 0)
            {
                delete inNodeMinor.Link[inKey];
            }
            return;
        }


        if(inNodeMajor.Link[inKey].Set.length == 1)
        {
            delete inNodeMajor.Link[inKey];
        }
        else
        {
            remove(inNodeMajor.Link[inKey].Set, inNodeMinor);
        }
        if(inNodeMinor.Link[inKey].Get.length == 1)
        {
            delete inNodeMinor.Link[inKey];
        }
        else
        {
            remove(inNodeMinor.Link[inKey].Get, inNodeMajor);
        }

    },
    Step(inNode, inKey, inForward, inForceCreate)
    {
        let connectionGroup = inNode.Link[inKey];
        if(!connectionGroup)
        {
            if(inForceCreate)
            {
                inNode.Link[inKey] = connectionGroup = {Get:[], Set:[]};
            }
            else
            {
                return false;
            }
        }
        return (inForward === undefined || inForward === true) ? connectionGroup.Set : connectionGroup.Get;
        
    },
    Walk(inIterator, inNode, inKey, inForward)
    {
        let array = N.Step(inNode, inKey, inForward);
        for(let i=0; i<array.length; i++)
        {
            let next = array[i];
            if(next.ID.Walk !== N.ID.Walk)
            {
                next.ID.Walk = N.ID.Walk;
                let results = inIterator(next);
                if(results !== false)
                {
                    N.Walk(inIterator, next, inKey, inForward);
                }
            }
        }
    },
    Path(inArray, inNode, inKey, inForward)
    {
        var current = inNode;
        var direction = inForward||true;
        for(let i=0; i<inArray.length; i++)
        {
            current = N.Step(current, inKey, direction)[inArray[i]];
        }
        return current;
    }
};
</script>

<script>
`
A     C
 \   / \
  \ /   \
   B     D
`
let a = N.Create({name:"A"});
let b = N.Create({name:"B"});
let c = N.Create({name:"C"});
let d = N.Create({name:"D"});
N.Connect(a, b, "parent");
N.Connect(c, b, "parent");
N.Connect(c, d, "parent");
N.Disconnect(a, null, "parent");
/*
console.log(a.Link);
console.log(b.Link);
console.log(c.Link);
console.log(d.Link);
*/
</script>

<script>
var Pivot = 
{
    Leaves:{},
    Root:{},
    
    Init(inRows)
    {
        Pivot.Leaves = inRows.map(r => N.Create({Row:r}));
        Pivot.Init = ()=>{};
    },
    Pivot(inParent, inColumnIndicies, inSumIndicies, inDepth)
    {
        //arguments:
        //    - a Node with leaf Nodes temporarily stored in its Meta.Leaves
        //        - where each leaf Node has a row of table data in it's Meta.Row
        //    - a list of columns to pivot on
        //    - a list of columns to sum
        //    - optional traversal depth, defaults to 0
        let depth = inDepth||0;
        let uniques = {};
        inParent.Meta.Leaves.forEach((inLeaf)=>
        {
            let row = inLeaf.Meta.Row; // shorthand for the raw "CSV" row in the leaf Node's Meta
            let value = row[inColumnIndicies[depth]]; // get the pivot column
            let match = uniques[value]; // check in the uniques list if this pivot column exists
            if(!match)
            {
                // if not, store a value under that key that will be the meta object for a new child
                match = uniques[value] = {
                    Label:value,
                    Row:inSumIndicies.map((inColumnIndex, inIndex, inArray) => row[inColumnIndex]), // create a Meta.Row also on the new child nodes to store summed totals
                    Leaves:[]
                };
                // grow a child off of the parent using the meta object
                N.Connect(inParent, N.Create(match), "Hierarchy");
            }
            else
            {
                // if a match does exist, sum the appropriate columns
                inSumIndicies.forEach((inColumnIndex, inIndex, inArray) => match.Row[inIndex] += row[inColumnIndex]);
            }
            // move the leaves into the child
            match.Leaves.push(inLeaf);
        });
        
        delete inParent.Meta.Leaves;
        var iterator;
        if(depth >= inColumnIndicies.length-1)
        {
            iterator = inChild =>
            {
                inChild.Meta.Leaves.forEach( inLeaf => N.Connect(inChild, inLeaf, "Hierarchy") );
                delete inChild.Meta.Leaves;
            }
        }
        else
        {
            iterator = child => Pivot.Pivot(child, inColumnIndicies, inSumIndicies, depth+1);
        }
        N.Step(inParent, "Hierarchy").forEach(iterator);
        return inParent;
    },
    Create(inColumnIndicies, inSumIndicies)
    {
        return Pivot.Pivot(N.Create({Leaves:Pivot.Leaves}), inColumnIndicies, inSumIndicies);
    },
    Modify(inNode)
    {
        N.ID.Walk++;

        let modifier = N.Create({});

        let leaves = [];
        let gatherUp = n => N.Connect(modifier, n, "ModifyUp");
        let gatherDown = n =>
        {
            N.Connect(modifier, n, "ModifyDown");
            N.Step(n, "Hierarchy").length == 0 ?  leaves.push(n) : null;
        };
        let gatherOut = n => N.Connect(modifier, n, "ModifyOut");

        N.Walk(gatherUp, inNode, "Hierarchy", false);
        N.Connect(modifier, inNode, "ModifyDown");
        N.Walk(gatherDown, inNode, "Hierarchy");
        leaves.forEach(leaf=>N.Walk(gatherOut, leaf, "Hierarchy", false));

        return modifier;
    },
    Unmodify(inNode)
    {
        N.Disconnect(inNode, null, "ModifyUp");
        N.Disconnect(inNode, null, "ModifyDown");
        N.Disconnect(inNode, null, "ModifyOut");
    }
};
</script>

<script type="module">

import {h, render, createContext} from 'https://cdn.skypack.dev/preact';
import {useReducer} from 'https://cdn.skypack.dev/preact/hooks';
import { css, cx } from 'https://cdn.skypack.dev/@emotion/css';

Pivot.Init([
    ["#1", "a", "long",  1],
    ["#2", "b", "long",  2],
    ["#3", "b", "short", 2],
    ["#4", "a", "long",  3],
    ["#5", "b", "short", 1],
    ["#6", "a", "short", 0],
    ["#7", "b", "short", 7],
]);
let pivotRoot1 = Pivot.Create([1, 2], [3]);
let pivotRoot2 = Pivot.Create([2, 1], [3]);
let pivots = [pivotRoot1, pivotRoot2];

let select = N.Path([0, 1], pivotRoot1, "Hierarchy");
let mod = Pivot.Modify(select);



let Store = createContext(null);
let StoreState = 
{
    count:7
};
let StoreReducer = (inState, inAction) =>
{

};

let ElNode = ({node}) =>
{
    var nodeChildren = N.Step(node, "Hierarchy");
    var children = [];
    var table = [];

    let styles = {
    };
    if(node.Link["ModifyOut"])
    {
        styles.backgroundColor = "lightgrey";
    }
    if(node.Link["ModifyDown"])
    {
        styles.backgroundColor = "yellow";
    }
    if(node.Link["ModifyUp"])
    {
        styles.backgroundColor = "lavender";
    }

    if(node.Meta.Row)
    {
        table = node.Meta.Row.map( cell => h("span", {style:{padding:"10px"}}, cell));
    }
    children =  [
        h("strong", {style:styles}, node.Meta.Label||"a node"),
        ...table,
        h(Store.Consumer, null, value=>
        {
            return value.get.count;
        })
    ];

    if(nodeChildren.length)
    {
        children = [
            ...children,
            ...nodeChildren.map( inChild => h(ElNode, {node:inChild}))
        ];
    }
    return h("div", {style:{padding:"10px"}}, children);
}
let ElPivot = ({pivot}) =>
{
    return h("div", {style:{display:"inline-block"}}, [
        h(ElNode, {node:pivot})
    ]);
}
let ElRoot = ({pivots}) =>
{
    let [get, set] = useReducer(StoreReducer, StoreState);

    return h(Store.Provider, {value:{get, set}}, [
        h("div", null, [
            h("h3", null, "tree view"),
            ...pivots.map(pivot=>h(ElPivot, {pivot}))
        ])
    ]);
};
render(h(ElRoot, {pivots}, null), document.querySelector("#app"));

</script>