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

<script src="papaparse.min.js"></script>

<!-- 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;
                //console.log("processing", next.Meta)
                let results = inIterator(next);
                if(results !== false)
                {
                    N.Walk(inIterator, next, inKey, inForward, inTerminal);
                }
                else
                {
                    //console.log("routine exited");
                }
            }
            else
            {
                //console.log("id collision");
            }
        }
    },
    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(inColumnTypes, inColumnNames, 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");
        }

        let numeric = (N.Step(Pivot.Schema, "sum")||[]).map(column=>column.Meta.Index);
        console.log(numeric);
        Pivot.Leaves = inRows.map(r =>
        {
            numeric.forEach(index => r[index] = parseFloat(r[index])||0);
            return 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:[],
                    Depth:depth
                };
                // 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);
        });
        
        // get the leaves out of the parent, at this point they have been re-distributed to the children
        delete inParent.Meta.Leaves;
        let iterator = () => {};
        if(depth >= inPivotIndicies.length-1)
        {
            iterator = inLastBranch =>
            {
                inLastBranch.Meta.Leaves.forEach( inLeaf =>
                {
                    let modifiers = [];
                    let collectModifier = n => modifiers.push(n);
                    let connectModifiers = n => modifiers.forEach(inModifier => N.Connect(inModifier, n, "ModifyOut", true));
                    
                    // collect modifiers effecting leaves
                    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 to parents, but with "check unique" enabled
                        console.log("walking modifiers up from", inLastBranch.Meta.Label);
                        N.ID.Walk++;
                        N.Walk(connectModifiers, inLastBranch, "Hierarchy", false);
                    }

                    // lastly connect the leaf to the branch
                    N.Connect(inLastBranch, inLeaf, "Leaf");

                });
                delete inLastBranch.Meta.Leaves;
                return false;
            }
        }
        else
        {
            iterator = child => {
                Pivot.Pivot(inRoot, child, inPivotIndicies, inSumIndicies, depth+1);
                return false;
            };
        }
        N.Walk(iterator, inParent, "Hierarchy");
        
        return inParent;
    },
    Create(inLabel, inPivotIndicies, inSumIndicies)
    {
        N.ID.Walk++;
        
        /*
        let sumColumns = (N.Step(Pivot.Schema, "sum")||[]).map(column=>column.Meta.Index);
        let labelColumns = (N.Step(Pivot.Schema, "label")||[]).map(column=>column.Meta.Index);
        */

        let pivotRoot = N.Create({Label:inLabel, 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]
    ]
);
*/

Papa.parse("./sample.csv",
{
    download:"true",
	complete: function(results)
    {
        let columnNames = results.data.shift();
        let columnTypes = ([...columnNames]).fill("hidden");

        columnTypes[29] = "sum";
        columnTypes[30] = "sum";
        columnTypes[31] = "sum";
        columnTypes[32] = "sum";
        columnTypes[33] = "sum";
        columnTypes[7 ] = "label";
        columnTypes[8 ] = "label";
        columnTypes[9 ] = "label";
        columnTypes[10] = "label";
        columnTypes[11] = "label";

        Pivot.Init(
            columnTypes,
            columnNames,
            results.data
        );
        Render();
	}
});

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(pivotColumnsUsed.map(column=>column.Meta.Label).join("|"), 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 ModificationsIcon = ({node}) =>
{
    let modsUp   = N.Step(node, "ModifyUp",   false)||[];
    let modsDown = N.Step(node, "ModifyDown", false)||[];
    let modsAt   = N.Step(node, "ModifyAt",   false)||[];
    let modsOut  = N.Step(node, "ModifyOut",  false)||[];

    var button = null;
    if(modsAt.length)
    {
        button = html`<button onClick=${e=>{Pivot.Unmodify(modsAt[0]); Render();}}>-</button>`;
    } 
    else
    {
        button = html`<button onClick=${e=>{Pivot.Modify(node); Render();}}>+</button>`;
    }

    let padding = 7;
    let icon = 0;
    let styles = css`
        position:relative;
        display:inline-block;
        vertical-align:middle;
        width:${padding*2 + icon}px;
        height:${padding*2 + icon}px;
        margin:${padding*2};
        .Icon
        {
            position:absolute;
            display:inline-block;
            width:${padding*2 + icon}px;
            height:${padding*2 + icon}px;
            text-align:center;
            font-size:9px;
            font-family:sans-serif;
            font-weight:900;
            line-height:${padding*2 + icon}px;

            &::after
            {
                content:" ";
                display:block;
                position:absolute;
                width:${icon}px;
                height:${icon}px;
                border:${padding}px solid transparent;
            }

            &.Down
            {
                left:0;
                bottom:100%;
                &::after
                {
                    top:100%;
                    border-top-color:green;
                    border-bottom:0px solid transparent;
                }
            }
            &.At
            {
                top:0;
                left:100%;
                &::after
                {
                    top:0;
                    right:100%;
                    border-right-color:red;
                    border-left:0px solid transparent;
                }
            }
            &.Up
            {
                left:0;
                top:100%;
                &::after
                {
                    bottom:100%;
                    border-bottom-color:orange;
                    border-top:0px solid transparent;
                }
            }

            &.Out
            {
                top:0;
                right:100%;
                &::after
                {
                    top:0;
                    left:100%;
                    border-left-color:grey;
                    border-right:0px solid transparent;
                }
            }
        }

    `;

    return html`
    <div class=${styles}>
        ${modsDown.length ? html`<div class="Icon Down">${modsDown.length}</div>` : null}
        ${modsAt.length ? html`<span class="Icon At">${modsAt.length}</span>` : null}
        ${modsUp.length ? html`<span class="Icon Up">${modsUp.length}</span>` : null}
        ${modsOut.length ? html`<span class="Icon Out">${modsOut.length}</span>` : null}
    </div>
    ${button}
    `;

};

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>`
        }
    });

    let stylesLeaf = css`
        background:#ddd;
        color:#333;
    `;

    return html`
    <tbody>
        <tr>
            <td>
                <strong class=${css`margin-left:${props.node.Meta.Depth*10}px;`}>${props.node.Meta.Label}</strong>
            </td>
            <td>
                <${ModificationsIcon} node=${props.node}><//>
            </td>
            ${displayCells}
        </tr>
        ${(N.Step(props.node, "Leaf")||[]).map(leaf =>
        {
            return html`
            <tr class=${stylesLeaf}>
                <td></td>
                <td><${ModificationsIcon} node=${leaf}><//></td>
                ${
                    leaf.Meta.Row.map(column=>h("td", null, column))
                }
            </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="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>