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

<!-- N -->
<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, inUnique)
    {
        if(inUnique) // bail if the nodes are already connected
        {
            let check = N.Step(inNodeMajor, inKey, true);
            if(check)
            {
                if(check.indexOf(inNodeMinor) !== -1)
                {
                    return;
                }
            }
        }
        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 no specific child was passed
        if(inNodeMinor === null)
        {
            // get all the children
            let check = N.Step(inNodeMajor, inKey);
            if(!check){ return; }

            // go down to each child ...
            check.forEach( inNodeMinor => 
            {
                let connections = inNodeMinor.Link[inKey];
                remove( connections.Get, inNodeMajor); // ... and remove any reference to the parent

                // if after the remove operation, this child has no connections on inKey, scrub the key
                if(!connections.Set.length && !connections.Get.length)
                {
                    delete inNodeMinor.Link[inKey];
                }
            });

            // we just wiped out all outgoing connections to the parent, if incoming connections are empty too we can purge the key there as well
            if(inNodeMajor.Link[inKey].Get.length == 0)
            {
                delete inNodeMajor.Link[inKey];
            }
            return;
        }

        // if no specific parent was passed
        if(inNodeMajor === null)
        {
            // get all the parents
            let check = N.Step(inNodeMinor, inKey, false);
            if(!check){ return; }
            
            // go up to each parent ...
            check.forEach( inNodeMajor => 
            {
                let connections = inNodeMajor.Link[inKey];
                remove( connections.Set, inNodeMinor); // ... and remove any reference to the child
                
                // if after the remove operation, this parent has no connections on inKey, scrub the key
                if( !connections.Set.length && !connections.Get.length )
                {
                    delete inNodeMajor.Link[inKey];
                }
            });

            // we just wiped out all incoming connections to the child, if outgoing connections are empty too we can purge the key there as well
            if(inNodeMinor.Link[inKey].Set.length == 0)
            {
                delete inNodeMinor.Link[inKey];
            }
            return;
        }

        // if a specific parent and child were passed
        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 === true)
            {
                inNode.Link[inKey] = connectionGroup = {Get:[], Set:[]};
            }
            else
            {
                return false;
            }
        }
        return (inForward === undefined || inForward === true) ? connectionGroup.Set : connectionGroup.Get;
        
    },
    Walk(inIterator, inNode, inKey, inForward, inTerminal)
    {
        let array = N.Step(inNode, inKey, inForward);

        if(!array.length && inTerminal)
        {
            return inTerminal(inNode);
        }

        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, inTerminal);
                }
            }
        }
    },
    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>

<!-- Pivot -->
<script>
var Pivot = 
{
    Leaves:{},
    Root:N.Create({Label:"All Pivots"}),
    Schema:N.Create({Label:"Column Details"}),
    Proto:N.Create({Label:"User Form"}),
    Init(inColumnNames, inColumnTypes, inRows)
    {        
        for(let i=0; i<inColumnNames.length; i++)
        {
            let columnNode = N.Create({Label:inColumnNames[i], Index:i});
            N.Connect(Pivot.Schema, columnNode, inColumnTypes[i]);
            N.Connect(Pivot.Schema, columnNode, "all");
        }
        Pivot.Leaves = inRows.map(r => N.Create({Row:r}));
        Pivot.Init = ()=>{};
    },
    Pivot(inRoot, inParent, inPivotIndicies, 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 = {};
        let indexPivot = inPivotIndicies[depth]
        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[indexPivot]; // 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

                let clone = row.map(r=>null);
                inSumIndicies.forEach((inSumIndex, inIndex, inArray)=>
                {
                    clone[inSumIndex] = row[inSumIndex]
                });
                match = uniques[value] = {
                    Label:value,
                    Row:clone, 
                    IndexPivot:indexPivot,
                    IndexSum:inSumIndicies,
                    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((inSumIndex) => match.Row[inSumIndex] += row[inSumIndex]);
            }
            // move the leaves into the child
            match.Leaves.push(inLeaf);
        });
        
        delete inParent.Meta.Leaves;
        var iterator;
        if(depth >= inPivotIndicies.length-1)
        {
            iterator = inLastBranch =>
            {
                inLastBranch.Meta.Leaves.forEach( inLeaf =>
                {
                    // collect modifiers effecting leaves
                    let modifiers = [];
                    let collectModifier = n => modifiers.push(n);
                    let connectModifiers = n => modifiers.forEach(inModifier => N.Connect(inModifier, n, "ModifyOut", true));

                    N.Walk(collectModifier, inLeaf, "ModifyAt",   false);
                    N.Walk(collectModifier, inLeaf, "ModifyDown", false);

                    if(modifiers.length)
                    {
                        // apply them to the branch
                        inLastBranch.ID.Walk = N.ID.Walk;
                        connectModifiers(inLastBranch);

                        // also walk them up and connect, but with "check unique" enabled
                        N.Walk(connectModifiers, inLastBranch, "Hierarchy", false);
                    }

                    // lastly connect the leaf to the branch
                    N.Connect(inLastBranch, inLeaf, "Leaf");
                });
                delete inLastBranch.Meta.Leaves;
            }
        }
        else
        {
            iterator = child => Pivot.Pivot(inRoot, child, inPivotIndicies, inSumIndicies, depth+1);
        }
        N.Walk(iterator, inParent, "Hierarchy");
        return inParent;
    },
    Create(inPivotIndicies, inSumIndicies)
    {
        N.ID.Walk++;
        
        let columns = N.Step(Pivot.Schema, "all");
        let label = inPivotIndicies.map( inPivotIndex =>
        {
            return columns[inPivotIndex].Meta.Label;
        });

        let pivotRoot = N.Create({Label:label.join("|"), Leaves:Pivot.Leaves});
        N.Connect(Pivot.Root, pivotRoot, "Pivot");
        return Pivot.Pivot(pivotRoot, pivotRoot, inPivotIndicies, inSumIndicies);
    },
    Delete(inRoot)
    {
        // disconnect modifiers
        let check = N.Step(inRoot, "ModifyUp", false);
        if(check)
        {
            while(check.length>0)
            {
                Pivot.Unmodify(check[0]);
            }
        }

        // disconnect leaves from terminal branches
        N.Walk(()=>{}, inRoot, "Hierarchy", true, terminal=>{
            N.Disconnect(terminal, null, "Leaf");
        });

        // disconnect from app
        N.Disconnect(null, inRoot, "Pivot");
    },
    Modify(inNode)
    {
        let modified = N.Create({Label:"Modifier"});

        // traverse
        let gatherUp   = n => N.Connect(modified, n, "ModifyUp");
        let gatherDown = n => N.Connect(modified, n, "ModifyDown");
        let gatherOut  = n => N.Connect(modified, n, "ModifyOut");

        N.ID.Walk++;
        inNode.ID.Walk = N.ID.Walk;
        
        // at
        N.Connect(modified, inNode, "ModifyAt");
        
        // up
        N.Walk(gatherUp, inNode, "Hierarchy", false);

        // down 1
        N.Walk(gatherDown, inNode, "Hierarchy", true, terminal=>
        {
            // down 2
            // for each terminal node, step down into its leaves and gather down
            N.Walk(gatherDown, terminal, "Leaf", true, leaf=>
            {
                // out 1
                // walk back up on the leaf connections on other trees
                N.Walk(gatherOut, leaf, "Leaf", false, terminal=>
                {   
                    // out 2
                    // and continueup the hierarchy
                    N.Walk(gatherOut, terminal, "Hierarchy", false);
                });
            });
        });

        return modified;
    },
    Unmodify(inModifier)
    {
        N.Disconnect(inModifier, null, "ModifyUp");
        N.Disconnect(inModifier, null, "ModifyDown");
        N.Disconnect(inModifier, null, "ModifyOut");
        N.Disconnect(inModifier, null, "ModifyAt");
        N.Disconnect(null, inModifier, "Modifier");
    }
};
</script>

<!-- rendering -->
<script type="module">

Pivot.Init(
    ["id", "type-a", "type-b", "count", "extra"],
    ["label", "label", "label", "sum", "sum"],
    [
    ["#1", "a", "long",  1, 4],
    ["#2", "b", "long",  2, 4],
    ["#3", "b", "short", 2, 4],
    ["#4", "a", "long",  3, 4],
    ["#5", "b", "short", 1, 4],
    ["#6", "a", "short", 0, 4],
    ["#7", "b", "short", 7, 4]
    ]
);

import { h, render, createContext, Fragment } from 'https://cdn.skypack.dev/preact';
import { useReducer, useState } from 'https://cdn.skypack.dev/preact/hooks';
import { css, cx } from 'https://cdn.skypack.dev/@emotion/css';
import htm from 'https://unpkg.com/htm?module';
const html = htm.bind(h);

let PivotForm = props =>
{
    let styles = css`
        position:realtive;
        box-sizing: border-box;
        padding: 10px;
        color:black;
        font-family:sans-serif;

        .Title
        {
            font-size:24px;
            font-weight:100;
        }

        .Section
        {
            padding:10px 0 10px 0;

            .Heading
            {
                display:inline-block;
                color:#666;
                font-family:sans-serif;
                font-size:12px;
                font-weight:900;
                text-transform:uppercase;
            }
            .Group
            {
                display:inline-block;
                padding:5px;
                border-radius:5px;
                margin:3px;
                background:rgba(0, 0, 0, 0.3)
            }
        }
    `;


    let pivotColumns = N.Step(Pivot.Schema, "label")||[];
    let pivotColumnsUsed = N.Step(Pivot.Proto, "used-pivot")||[];

    let sumColumns = N.Step(Pivot.Schema, "sum")||[];
    //let sumColumnsUsed = N.Step(Pivot.Proto, "used-sum")||[];

    let indiciesPivot = pivotColumnsUsed.map(node=>node.Meta.Index);
    let indiciesSum = sumColumns.map(node=>node.Meta.Index);
    //let indiciesSum = sumColumnsUsed.map(node=>node.Meta.Index);
    
    let displayPivotsAll = html`
    <div class="Section">
        <div class="Heading">Available Columns</div>
        <div class="Group">
        ${pivotColumns.map( columnPivot =>
        {
            let attributes = {};
            if(N.Step(columnPivot, "used-pivot", false))
            {
                attributes.disabled = true;
            }
            else
            {
                attributes.onClick = e=>
                {
                    N.Connect(Pivot.Proto, columnPivot, "used-pivot");
                    Render();
                }
            }
            return html`<button ...${attributes}>${columnPivot.Meta.Label}</button>`;
        })}
        </div>
    </div>
    `;

    let displayPivotsPending = null;
    if(pivotColumnsUsed.length)
    {
        displayPivotsPending  = html`
        <div class="Section">
            <div class="Heading">Pending Pivot</div>
            <div class="Group">
                ${pivotColumnsUsed.map(columnPivot=>html`
                <button onClick=${e=>{N.Disconnect(Pivot.Proto, columnPivot, "used-pivot");Render();}}>
                    ${columnPivot.Meta.Label}
                </button>
                `)}
            </div>

            <button onClick=${e=>{
                N.Disconnect(Pivot.Proto, null, "used-pivot");
                N.Disconnect(Pivot.Proto, null, "used-sum");
                Pivot.Create(indiciesPivot, indiciesSum);
                Render();
            }}>Create</button>
        </div>
        `;
    }

    return html`
    <div class=${styles}>
        <div class="Title">Create New Pivot</div>
        ${displayPivotsAll}
        ${displayPivotsPending}
    </div>
    `;
}

let Section = props =>
{
    let styles = css`
        .Heading
        {
            padding:6px 0 6px 0;
            color:#666;
            font-weight:900;
            font-size:12px;
            text-transform:uppercase;
            cursor:pointer;

            span
            {
                display:inline-block;
                width:20px;
                height:20px;
                margin-right:10px;
                border-radius:20px;
                background:black;
                color:white;
                text-align:center;
            }
        }
        .Heading:hover
        {
            color:black;
        }
        .Body
        {
            position:relative;
            padding:10px 0 20px 30px;
            &::before
            {
                content: " ";
                display:block;
                position:absolute;
                top:-8px;
                left:8px;
                width:3px;
                height:100%;
                background:black;
            }
        }
    `;

    let [openGet, openSet] = useState(false);
    return html`
        <div class=${styles}>
            <div class="Heading" onClick=${e=>openSet(!openGet)}>
                <span>${openGet ? `−` : `+`}</span>
                ${props.label}
            </div>
            ${ openGet ? html`<div class="Body">${props.children}</div>` : null }
        </div>
    `;
}

let Modifier = props =>
{
    let refNode = N.Step(props.modifier, "ModifyAt")[0];

    let handleDelete = () =>
    {
        Pivot.Unmodify(props.modifier);
        Render();
    };

    let displayFields = [];
    N.Walk(node=>{
        displayFields.push(html`<input type='number'/>`)
    }, Pivot.Schema, "sum");


    return html`
    <div>
        <div>${refNode.Meta.Label}</div>
        <button onClick=${handleDelete}>Delete</button>
    </div>
    `;
}

let PivotBranch = props =>
{
    let row = props.node.Meta.Row;
    let displayCells = row.map(column=>h("td", null, column));
    let displayCellsModify = row.map(column=>false);
    props.node.Meta.IndexSum.forEach(i=>
    {
        displayCellsModify[i] = html`<td><input type="number" value=${row[i]}/></td>`;
    });
    displayCellsModify.forEach((cell, i)=>
    {
        if(!cell)
        {
            displayCellsModify[i] = html`<td>${row[i]}</td>`
        }
    });

    return html`
    <tbody>
        <tr>
            <td colspan=${displayCells.length+1}><hr/></td>
        </tr>
        <tr>
            <td>
                <strong>${props.node.Meta.Label}</strong>
            </td>
            ${displayCells}
        </tr>
        <tr>
            <td colspan=${displayCells.length+1}>Modifications</td>
        </tr>
        <tr>
            <td>At</td>
            ${displayCellsModify}
        </tr>
        <tr>
            <td>Below</td>
            ${displayCells}
        </tr>
        <tr>
            <td>Above</td>
            ${displayCells}
        </tr>
        <tr>
            <td>Outside</td>
            ${displayCells}
        </tr>
        <tr>
            <td>Computed</td>
            ${displayCells}
        </tr>
        <tr>
            <td colspan=${displayCells.length+1}>Goals</td>
        </tr>
        <tr>
            <td>Max</td>
            ${displayCells}
        </tr>
        <tr>
            <td>Min</td>
            ${displayCells}
        </tr>
    </tbody>
    `;
};

let PivotRoot = ({pivot}) =>
{

    let stylesRoot = css`
        display:block;
        box-sizing:border-box;
        padding:15px;
        font-family:sans-serif;
    `;
    let stylesHeading = css`
        margin: 0 0 15px 0;
        font-weight:0;
        font-size:24px;
    `;

    let modifiers = N.Step(pivot, "ModifyUp", false) || [];
    let handleDelete = ()=>
    {
        Pivot.Delete(pivot);
        Render();
    };

    let rows = [];
    N.ID.Walk++;
    N.Walk(n=>rows.push(h(PivotBranch, {node:n}, null)), pivot, "Hierarchy");

    return html`
    <div class=${stylesRoot}>
        <div key="heading" class=${stylesHeading}>${pivot.Meta.Label}</div>
        <${Section} key="settings" label=${`Settings`}>
            <button onClick=${handleDelete}>Destroy Pivot</button>
        <//>
        <${Section} key="modifiers" label=${`Modifiers (${modifiers.length})`}>
            ${ !modifiers.length ? html`<em>No modifiers</em>` : modifiers.map( m => html`<${Modifier} modifier=${m}/>`)}
        <//>
        <${Section} key="tree" label=${"Tree"}>
            <table>
            ${rows}
            </table>
        <//>
    </div>
    `;
};

let ElRoot = props =>
{
    let pivots = N.Step(Pivot.Root, "Pivot")||[];
    return h("div", null, [
        h(PivotForm),
        pivots.map(pivot=>h(PivotRoot, {key:pivot.Meta.Label, pivot}))
    ])
};
const Render = () => render(h(ElRoot), document.querySelector("#app"));
Render();


</script>

<!-- testing area -->
<script>
    let ModificationPull = (inDAG) =>
    {
        let collect = inArray => n => { n.Meta.Tweak.forEach((t, i)=>{ inArray[n.Meta.Indicies[i]] += t; }); };
        let totalScale = [0, 0, 0, 0, 0, 0, 0, 0];
        let totalAdd = [0, 0, 0, 0, 0, 0, 0, 0];
    
        N.ID.Walk++;
        N.Walk(collect(totalScale), inDAG, "ModifyAt", false);
        N.Walk(collect(totalScale), inDAG, "ModifyDown", false);
        N.Walk(collect(totalAdd), inDAG, "ModifyUp", false);
        N.Walk(collect(totalAdd), inDAG, "ModifyOut", false);
    
        return [totalScale, totalAdd];
    };
    
    let dag1 = N.Create({Label:"dag-1"});
    let m0 = N.Create({Label:"m-0", Tweak:[-1, 2, 0.7], Indicies:[0, 1, 2]});
    let m1 = N.Create({Label:"m-1", Tweak:[0.1, 0.1, 0.1], Indicies:[1, 2, 3]});
    
    N.Connect(m0, dag1, "ModifyAt");
    N.Connect(m1, dag1, "ModifyDown");
    
    let results = ModificationPull(dag1);
    console.log(results);
    
</script>