//
// DUMP demo for the Box-Muller Transform
// http://stackedboxes.org/2020/01/05/dump-of-unsorted-morsels-for-programmers/
// Code is licensed under the MIT license
//
// Copyright 2020 Leandro Motta Barros
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

namespace BoxMullerTransform {

const VectorColor = "#0066ff";
const HistogramColor = VectorColor;
const ProjectionColor = "#3399ff";

type SelBaseType = d3.Selection<d3.BaseType, unknown, HTMLElement, any>;
type SelSVGG = d3.Selection<SVGGElement, unknown, HTMLElement, any>;

export class TransformDiagram {
    /// The offset between the grid and the histogram
    private histogramOffset_ = 0.25
    private gridMin_ = -3
    private gridMax_ = +3
    private gridSize_ = this.gridMax_ - this.gridMin_;

    private numHistogramBuckets_ = 31;
    private histogramBucketSize_ = this.gridSize_ / this.numHistogramBuckets_;

    private btnOne_: SelBaseType;
    private btnTen_: SelBaseType;
    private btnHundred_: SelBaseType;
    private btnClear_: SelBaseType;

    private svg_: SelSVGG;
    private grid_: SelSVGG;
    private vectors_: SelSVGG;
    private projections_: SelSVGG;
    private rightHistogram_: SelSVGG;
    private bottomHistogram_: SelSVGG;

    // Histogram data are pairs of (offset, numberOfElementsInBucket)
    private rightHistogramData_: [number, number][] = [];
    private bottomHistogramData_: [number, number][] = [];

    public constructor(containerID: string) {
        this.svg_ = d3.select(`#${containerID} svg`);

        // Defs (arrowheads)
        this.makeDefs();

        // Buttons
        this.btnOne_ = d3.select(`#${containerID}-btn-one`);
        this.btnOne_.on("click", () => this.onBtnOne());

        this.btnTen_ = d3.select(`#${containerID}-btn-ten`);
        this.btnTen_.on("click", () => this.onBtnTen());

        this.btnHundred_ = d3.select(`#${containerID}-btn-hundred`);
        this.btnHundred_.on("click", () => this.onBtnHundred());

        this.btnClear_ = d3.select(`#${containerID}-btn-clear`);
        this.btnClear_.on("click", () => this.onBtnClear());

        // Grid
        this.grid_ = this.svg_.append("g")
            .attr("fill", "transparent")
            .attr("stroke-width", 0.01)
            .attr("stroke", "gray")
        this.makeGrid();

        // Vectors
        this.vectors_ = this.svg_.append("g")
            .style("stroke-width", 0.025)
            .style("stroke", VectorColor)
            .style("fill", VectorColor)
            .style("marker-end", "url(#arrowhead)");

        // Projections
        this.projections_ = this.svg_.append("g")
            .style("stroke-width", 0)
            .style("fill", ProjectionColor);

        // Histograms
        this.rightHistogram_ = this.svg_.append("g")
            .style("stroke-width", 0)
            .style("fill", HistogramColor);
        this.initRightHistogram();

        this.bottomHistogram_ = this.svg_.append("g")
            .style("stroke-width", 0)
            .style("fill", HistogramColor);
        this.initBottomHistogram();
    }

    private onBtnOne() {
        this.doOneVector()
    }

    private onBtnTen() {
        for (let i = 0; i < 10; ++i) {
            this.doOneVector();
        }
    }

    private onBtnHundred() {
        for (let i = 0; i < 100; ++i) {
            this.doOneVector();
        }
        this.btnHundred_.property("disabled", false);
    }

    private onBtnClear() {
        this.initRightHistogram();
        this.initBottomHistogram();
    }

    // Draw one pair of numbers, draw the resulting vector, make sure the next
    // animation step (projection) runs at the right moment.
    private doOneVector() {
        let z0: number;
        let z1: number;

        do {
            let u0 = this.uRand01();
            let u1 = this.uRand01();
            [z0, z1] = boxMullerTransform(u0, u1);
        } while(!this.isWithinGrid(z0, z1));

        this.vectors_.append("line")
            .attr("x1", 0).attr("y1", 0)
            .attr("x2", 0).attr("y2", 0)
            .transition()
                .attr("x2", z0).attr("y2", z1)
                .duration(750)
                .on("end", (() => this.drawProjection(z0, z1)))
            .transition()
                .delay(250)
                .duration(250)
                .style("stroke", "transparent")
                .style("fill", "transparent")
                .remove();
    }

    private drawProjection(z0: number, z1: number) {
        this.projections_.append("circle")
            .attr("cx", z0).attr("cy", z1)
            .attr("r", 0.06)
            .transition()
                .attr("cx", this.gridMax_ + this.histogramOffset_).attr("cy", z1)
                .duration(750)
                .on("end", () => this.incrementRightHistogram(z1))
            .transition()
                .attr("r", 0.18)
                .style("fill", "transparent")
                .remove();

        this.projections_.append("circle")
            .attr("cx", z0).attr("cy", z1)
            .attr("r", 0.06)
            .transition()
                .attr("cx", z0).attr("cy", this.gridMax_ + this.histogramOffset_)
                .duration(750)
                .on("end", () => this.incrementBottomHistogram(z0))
            .transition()
                .attr("r", 0.18)
                .style("fill", "transparent")
                .remove();
    }

    private initRightHistogram() {
        this.rightHistogramData_ = [];
        for (let i = 0; i < this.numHistogramBuckets_; ++i) {
            this.rightHistogramData_.push([
                (this.gridMin_ + i * this.histogramBucketSize_),
                0]);
        }

        this.updateRightHistogram();
    }

    private updateRightHistogram() {
        let rects = this.rightHistogram_.selectAll("rect").data(this.rightHistogramData_);
        rects.exit().remove();

        const dx = this.gridMax_ + this.histogramOffset_;
        rects.enter().append("rect")
            .attr("transform", d => `translate(${dx}, ${d[0]})`)
            .attr("height", this.histogramBucketSize_);

        rects = this.rightHistogram_.selectAll("rect").data(this.rightHistogramData_);
        rects.attr("width", d => d[1] * 0.025);
    }

    private incrementRightHistogram(value: number) {
        ++this.rightHistogramData_[this.valueToBucket(value)][1];
        this.updateRightHistogram();
    }

    private initBottomHistogram() {
        this.bottomHistogramData_ = [];
        for (let i = 0; i < this.numHistogramBuckets_; ++i) {
            this.bottomHistogramData_.push([
                (this.gridMin_ + i * this.histogramBucketSize_),
                0]);
        }

        this.updateBottomHistogram();
    }

    private updateBottomHistogram() {
        let rects = this.bottomHistogram_.selectAll("rect").data(this.bottomHistogramData_);
        rects.exit().remove();

        const dy = this.gridMax_ + this.histogramOffset_;
        rects.enter().append("rect")
            .attr("transform", d => `translate(${d[0]}, ${dy})`)
            .attr("width", this.histogramBucketSize_);

        rects = this.bottomHistogram_.selectAll("rect").data(this.bottomHistogramData_);
        rects.attr("height", d => d[1] * 0.025);
    }

    private incrementBottomHistogram(value: number) {
        ++this.bottomHistogramData_[this.valueToBucket(value)][1];
        this.updateBottomHistogram();
    }

    private valueToBucket(value: number): number {
        const t1 = value - this.gridMin_;                     // [0, gridSize_]
        const t2 = t1 /this.gridSize_;                        // [0, 1]
        const t3 = Math.trunc(t2 * this.numHistogramBuckets_) // [0, numHistogramBuckets]
        return t3 < this.numHistogramBuckets_ ? t3 : this.numHistogramBuckets_;
    }

    private makeDefs() {
        let defs = this.svg_.append("defs");
        defs.append("marker")
            .attr("id", "arrowhead")
            .attr("markerHeight", 4.5)
            .attr("markerWidth", 4.5)
            .attr("markerUnits", "strokeWidth")
            .attr("orient", "auto")
            .attr("refX", 0)
            .attr("refY", 0)
            .attr("viewBox", "-6 -2.5 6 5")
            .attr("stroke", VectorColor)
            .attr("fill", VectorColor)
            .append("path")
                .attr("d", "M -6,-2.5 L 0,0 L -6,2.5 L -6,-2.5 Z");
    }

    private makeGrid(): void {
        const wholeInterval = 1.0
        const fractionalInterval = 0.2

        // Fractional lines
        for (let i = this.gridMin_ + fractionalInterval; i < this.gridMax_; i += fractionalInterval) {
            this.grid_.append("line")
                .attr("x1", `${this.gridMin_}`)
                .attr("y1", `${i}`)
                .attr("x2", `${this.gridMax_}`)
                .attr("y2", `${i}`)
                .attr("stroke-opacity", 0.15);
            this.grid_.append("line")
                .attr("x1", `${i}`)
                .attr("y1", `${this.gridMin_}`)
                .attr("x2", `${i}`)
                .attr("y2", `${this.gridMax_}`)
                .attr("stroke-opacity", 0.15);
        }

        // Whole lines
        for (let i = this.gridMin_ + wholeInterval; i < this.gridMax_; i += wholeInterval) {
            this.grid_.append("line")
                .attr("x1", `${this.gridMin_}`)
                .attr("y1", `${i}`)
                .attr("x2", `${this.gridMax_}`)
                .attr("y2", `${i}`)
                .attr("stroke-opacity", 0.5);
            this.grid_.append("line")
                .attr("x1", `${i}`)
                .attr("y1", `${this.gridMin_}`)
                .attr("x2", `${i}`)
                .attr("y2", `${this.gridMax_}`)
                .attr("stroke-opacity", 0.5);

        // Axes lines
        this.grid_.append("line")
            .attr("x1", `${this.gridMin_}`)
            .attr("y1", 0)
            .attr("x2", `${this.gridMax_}`)
            .attr("y2", 0)
            .attr("stroke-opacity", 0.75);

        this.grid_.append("line")
            .attr("x1", 0)
            .attr("y1", `${this.gridMin_}`)
            .attr("x2", 0)
            .attr("y2", `${this.gridMax_}`)
            .attr("stroke-opacity", 0.75);

        // Boxes for the histograms
        this.grid_.append("line")
            .attr("x1", `${this.gridMin_}`)
            .attr("y1", this.gridMax_ + this.histogramOffset_)
            .attr("x2", `${this.gridMax_}`)
            .attr("y2", this.gridMax_ + this.histogramOffset_)
            .attr("stroke-width", 0.02)
            .attr("stroke", "black")
            .attr("stroke-opacity", 0.75);

        this.grid_.append("line")
            .attr("x1", `${this.gridMin_}`)
            .attr("y1", this.gridMax_ + this.histogramOffset_ + 1.73)
            .attr("x2", `${this.gridMax_}`)
            .attr("y2", this.gridMax_ + this.histogramOffset_ + 1.73)
            .attr("stroke-width", 0.02)
            .attr("stroke-opacity", 0.5);

        this.grid_.append("line")
            .attr("x1", this.gridMax_ + this.histogramOffset_)
            .attr("y1", `${this.gridMin_}`)
            .attr("x2", this.gridMax_ + this.histogramOffset_)
            .attr("y2", `${this.gridMax_}`)
            .attr("stroke-width", 0.02)
            .attr("stroke", "black")
            .attr("stroke-opacity", 0.75);

        this.grid_.append("line")
            .attr("x1", this.gridMax_ + this.histogramOffset_ + 1.73)
            .attr("y1", `${this.gridMin_}`)
            .attr("x2", this.gridMax_ + this.histogramOffset_ + 1.73)
            .attr("y2", `${this.gridMax_}`)
            .attr("stroke-width", 0.02)
            .attr("stroke-opacity", 0.5);
        }
    }

    // Return random in (0, 1), taken from an uniform distribution.
    private uRand01(): number {
        let u: number;
        do {
            u = Math.random();
        } while (u <= 0.0 || u >= 1.0);

        return u;
    }

    private isWithinGrid(z0: number, z1: number): boolean {
        return z0 >= this.gridMin_ &&
            z0 <= this.gridMax_ &&
            z1 >= this.gridMin_ &&
            z1 <= this.gridMax_;
    }
} // class TransformDiagram

} // namespace BoxMullerTransform

let boxMullerDiagram = new BoxMullerTransform.TransformDiagram("demo-box-muller");
