/*
 * Copyright © 2019 Atomist, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import * as d3 from "d3";
import {
    PlantedTree,
    SunburstLeaf,
    SunburstTree,
} from "../tree/sunburst";

const NameOfThisLibrary = "SunburstYo";

/**
 * Color palette for d3 to use
 */
const palette = [
    "#56b06f",
    "#3bbaa8",
    "#35848c",
    "#173d6d",
    "#846473",
    "#5F7186",
    "#0d560d",
    "#1f8045",
    "#173d48",
];

type SunburstTreeNode = d3.HierarchyNode<SunburstTree | SunburstLeaf>;

// tslint:disable

export function sunburst(workspaceId,
    data: any,
    pWidth,
    pHeight,
    options: {
        perLevelDataElementIds: string[],
        fieldsToDisplay: FieldToDisplay[]
    }) {
    const { perLevelDataElementIds, fieldsToDisplay } = options;
    const minDiameterInPixels = 100;

    const width = Math.max(pWidth || window.innerWidth, minDiameterInPixels),
        height = Math.max(pHeight || window.innerHeight, minDiameterInPixels),
        maxRadius = (Math.min(width, height) / 2) - 5;
    const viewBoxSide = maxRadius * 2 + 10;

    const x = d3.scaleLinear()
        .range([0, 2 * Math.PI])
        .clamp(true);

    const y = d3.scaleSqrt()
        .range([maxRadius * .1, maxRadius]);

    const chooseColorFromString = d3.scaleOrdinal(palette);

    const arc = d3.arc()
        .startAngle((d: any) => x(d.x0))
        .endAngle((d: any) => x(d.x1))
        .innerRadius((d: any) => Math.max(0, y(d.y0)))
        .outerRadius((d: any) => Math.max(0, y(d.y1)));

    const middleArcLine = d => {
        const halfPi = Math.PI / 2;
        const angles = [x(d.x0) - halfPi, x(d.x1) - halfPi];
        const r = Math.max(0, (y(d.y0) + y(d.y1)) / 2);

        const middleAngle = (angles[1] + angles[0]) / 2;
        const invertDirection = middleAngle > 0 && middleAngle < Math.PI; // On lower quadrants write text ccw
        if (invertDirection) {
            angles.reverse();
        }

        const path = d3.path();
        path.arc(0, 0, r, angles[0], angles[1], invertDirection);
        return path.toString();
    };

    const textFits = d => {
        const CHAR_SPACE = 6;

        const deltaAngle = x(d.x1) - x(d.x0);
        const r = Math.max(0, (y(d.y0) + y(d.y1)) / 2);
        const perimeter = r * deltaAngle;

        return d.data.name.length * CHAR_SPACE < perimeter;
    };

    const svg = d3.select("#putSvgHere").append("svg")
        .style("width", viewBoxSide + "px")
        .attr("viewBox", `${-viewBoxSide / 2} ${-viewBoxSide / 2} ${viewBoxSide} ${viewBoxSide}`)
        .on("click", focusOn); // Reset zoom on canvas click

    // now work with the data
    const d: PlantedTree = data;

    if (!d.tree || d.tree.children.length === 0) {
        alert("No data");
        return;
    }

    const root = d3.hierarchy<SunburstTree | SunburstLeaf>(d.tree);
    root.sum(d => (d as SunburstLeaf).size || 0); // sets a "value" property on each node

    const slice = svg.selectAll<d3.BaseType, SunburstTree | SunburstLeaf>("g.slice")
        .data(d3.partition<SunburstTree | SunburstLeaf>()(root).descendants());

    slice.exit().remove(); // does this remove any extraneous ones?

    const perLevelDataElements = perLevelDataElementIds.map(id => d3.select("#" + id)).filter(a => !!a);
    const additionalDataElement = d3.select("#additionalDataAboutWhatYouClicked");

    const newSlice = slice.enter()
        .append("g").attr("class", "slice")
        .on("click", d => {
            d3.event.stopPropagation();
            setFrozenLevelData(workspaceId, perLevelDataElements, d);
            focusOn(d);
        })
        .on("mouseover", (d: SunburstTreeNode) => {
            populatePerLevelData(perLevelDataElements, d);
            populateAdditionalData(additionalDataElement, fieldsToDisplay, d);
        });

    // This is the hover text
    newSlice.append("title")
        .text(d => d.data.name);

    newSlice.append("path")
        .attr("class", "main-arc")
        // I think this says, the last ring should use the same color as its parent
        .style("fill", (d: any) => d.data.color || chooseColorFromString((d.children ? d : d.parent).data.name))
        .attr("d", arc as any);

    newSlice.append("path")
        .attr("class", "hidden-arc")
        .attr("id", (_, i) => `hiddenArc${i}`)
        .attr("d", middleArcLine);

    const text = newSlice.append("text")
        .attr("display", d => textFits(d) ? null : "none");

    // Add white contour
    text.append("textPath")
        .attr("class", "textOutline")
        .attr("startOffset", "50%")
        .attr("xlink:href", (_, i) => `#hiddenArc${i}`)
        .text(d => d.data.name);

    text.append("textPath")
        .attr("startOffset", "50%")
        .attr("xlink:href", (_, i) => `#hiddenArc${i}`)
        .text(d => d.data.name);

    function focusOn(d = { x0: 0, x1: 1, y0: 0, y1: 1 }) {
        // Reset to top-level if no data point specified

        const transition = svg.transition()
            .duration(750)
            .tween("scale", () => {
                const xd = d3.interpolate(x.domain(), [d.x0, d.x1]),
                    yd = d3.interpolate(y.domain(), [d.y0, 1]);
                return t => {
                    x.domain(xd(t));
                    y.domain(yd(t));
                };
            });

        transition.selectAll("path.main-arc")
            .attrTween("d", d => () => arc(d as any));

        transition.selectAll("path.hidden-arc")
            .attrTween("d", d => () => middleArcLine(d));

        transition.selectAll("text")
            .attrTween("display", d => () => textFits(d) ? null : "none");

        moveStackToFront(d);

        function moveStackToFront(elD) {
            svg.selectAll<d3.BaseType, SunburstTreeNode>(".slice").filter(d => d === elD)
                .each(function (d) {
                    (this as any).parentNode.appendChild(this); // move all parents to the end of the line
                    if (d.parent) {
                        moveStackToFront(d.parent);
                    }
                });
        }
    }
}

function hasTags(data: SunburstTree | SunburstLeaf): data is (SunburstTree | SunburstLeaf) & { tags: Array<{ name: string }> } {
    return !!(data as any).tags
}

type FieldToDisplay = string;
function populateAdditionalData(
    additionalDataElement: d3.Selection<any, any, any, any> | undefined,
    usefulFields: FieldToDisplay[],
    d: SunburstTreeNode) {
    if (!additionalDataElement) {
        return;
    }
    let content = "";
    const data = d.data as any;
    console.log("Properties on data: " + Object.getOwnPropertyNames(data).join(","))
    if (hasTags(data)) {
        content = "Tags: <ul>" + data.tags.map(t => `<li>${t.name}</li>`).join("") + "</ul>";
    }
    usefulFields.forEach(fieldname => {
        if (data[fieldname] !== undefined) {
            content += `<br />${fieldname}: ${data[fieldname]}`
        }
    });
    additionalDataElement.html(content);
}

function populatePerLevelData(perLevelDataElements: d3.Selection<any, any, any, any>[], d: SunburstTreeNode) {
    const namesUpTree = [d.data.name];
    for (let place: any = d; place = place.parent; !!place) {
        namesUpTree.push(place.data.name);
    }
    const namesDownTree = namesUpTree.reverse();

    perLevelDataElements.forEach((e, i) => {
        if (e.attr("class") === "frozenLevelData") {
            return;
        }
        const value = namesDownTree[i] || "(various)";
        e.html(value);
    });

    if (namesUpTree.length < perLevelDataElements.length) {
        // if this isn't a leaf node, then
        // the value property on the node is a count of the leaves under it.
        // put that count in the last level data
        perLevelDataElements[perLevelDataElements.length - 1].html("(" + d.value + " of them)");
    }

}

function setFrozenLevelData(workspaceId, perLevelDataElements: d3.Selection<any, any, any, any>[], d: SunburstTreeNode) {
    const htmlUpTree = [formatLevelData(d.data)];
    for (let place: any = d; place = place.parent; !!place) {
        htmlUpTree.push(formatLevelData(place.data))
    }
    const htmlDownTree = htmlUpTree.reverse();

    const dataId = (d.data as any).id;
    const levelCountAbove = htmlUpTree.length;

    perLevelDataElements.forEach((e, i) => {
        const className = i >= levelCountAbove ? "unfrozenLevelData" : "frozenLevelData";
        e.attr("class", className);
        if (!dataId || i !== (levelCountAbove - 1)) {
            // no buttons
            e.html(htmlDownTree[i]);
            return;
        }
        e.html(htmlDownTree[i] + "<br/>" + htmlForSetIdeal(workspaceId, dataId) + htmlForNoteProblem(workspaceId, dataId));
    });
}

function formatLevelData(data: { name: string, url?: string, viewUrl?: string, tags?: Array<{ name: string }> }): string {
    console.log("tags: " + data.tags);
    const viewLink = data.viewUrl ? `<a href="${data.viewUrl}"><img src="/hexagonal-fruit-of-power.png" class="linkToInsightsImage" ></img></a>` : "";
    let tagList: string = "";
    if (data.tags) {
        tagList = "<ul>" + data.tags.map(t => `<li>${t.name}</li>`).join("") + "</ul>"
    }
    const nameDisplay = data.url ? `<a href="${data.url}">${data.name}</a>` : data.name;

    return nameDisplay + viewLink + tagList;
}

function htmlForSetIdeal(workspaceId, dataId) {
    return `<button id="setIdeal" onclick="${NameOfThisLibrary}.postSetIdeal('${workspaceId}','${dataId}')">
                Set as ideal
            </button><label for="setIdeal" id="setIdealLabel" class="nothingToSay">&nbsp;</label>`;
}

function htmlForNoteProblem(workspaceId, dataId) {
    return `<button id="noteProblem" onclick="${NameOfThisLibrary}.postNoteProblem('${workspaceId}','${dataId}')">
                Note as problem
            </button><label for="noteProblem" id="noteProblemLabel" class="nothingToSay">&nbsp;</label>`;
}

export function postSetIdeal(workspaceId: string, fingerprintId: string) {
    const postUrl = `../../api/v1/${workspaceId}/ideal/${encodeURIComponent(fingerprintId)}`;
    const labelElement = document.getElementById("setIdealLabel");
    fetch(postUrl, { method: "PUT" }).then(response => {
        if (response.ok) {
            labelElement.textContent = `Ideal set`;
            labelElement.setAttribute("class", "success");
            labelElement.setAttribute("display", "static");
        } else {
            labelElement.textContent = "Failed to set. consult the server logaments";
            labelElement.setAttribute("class", "error");
        }
    },
        e => {
            labelElement.textContent = "Network error";
            labelElement.setAttribute("class", "error");
        });
}

export function postNoteProblem(workspaceId: string, fingerprintId: string) {
    const postUrl = `../../api/v1/${workspaceId}/problem/${encodeURIComponent(fingerprintId)}`;
    const labelElement = document.getElementById("noteProblemLabel");
    fetch(postUrl, { method: "PUT" }).then(response => {
        if (response.ok) {
            labelElement.textContent = `Problem noted`;
            labelElement.setAttribute("class", "success");
            labelElement.setAttribute("display", "static");
        } else {
            labelElement.textContent = "Failed to set. consult the server logaments";
            labelElement.setAttribute("class", "error");
        }
    },
        e => {
            labelElement.textContent = "Network error";
            labelElement.setAttribute("class", "error");
        });
}
