//
// DUMP demo for the Euclidean Algorithm
// http://stackedboxes.org/2020/01/05/dump-of-unsorted-morsels-for-programmers/
// Code is licensed under the MIT license
//
// Copyright 2019-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.


const cellSize = 10;

const poleFillColor = "#326029";
const poleStrokeColor = "#2aff00";
const stickFillColor = "#813131";
const stickStrokeColor = "#ff0031";
const measureFillColor = "#ffe8e8";
const measureStrokeColor = "#c66a6a";
const excessColor = "#ebbad9";

const roomFillColor = poleFillColor;
const roomStrokeColor = poleStrokeColor;
const tileFillColor = stickFillColor;
const tileStrokeColor = stickStrokeColor;


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


function addExcessPattern(svg: SelBaseType): void {
    let patternGroup = svg.append("defs")
        .append("pattern")
            .attr("id", "excessPattern")
            .attr("width", 10)
            .attr("height", 10)
            .attr("patternUnits", "userSpaceOnUse")
        .append("g");

    patternGroup.append("rect")
            .attr("x", 4)
            .attr("y", -4)
            .attr("width", 2)
            .attr("height", 20)
            .attr("stroke", "transparent")
            .attr("transform", "rotate(45 5 5)")
            .attr("fill", excessColor);

    patternGroup.append("rect")
            .attr("x", -4)
            .attr("y", 4)
            .attr("width", 20)
            .attr("height", 2)
            .attr("stroke", "transparent")
            .attr("transform", "rotate(45 5 5)")
            .attr("fill", excessColor);
}

class MeasuresDiagram {
    private poleLength_ = 25;
    private stickLength_ = 7;

    private grid_: SelSVGG;
    private pole_: SelSVGRect;
    private stick_: SelSVGRect;
    private measures_: SelSVGG;
    private excess_: SelSVGG;
    private poleLengthInput_: SelBaseType;
    private stickLengthInput_: SelBaseType;
    private text_: SelBaseType;

    public constructor(containerID: string) {

        let svg = d3.select(`#${containerID} svg`);

        // Pattern
        addExcessPattern(svg);

        // Pole
        this.pole_ = svg.append("rect")
            .attr("fill", poleFillColor)
            .attr("stroke", poleStrokeColor)
            .attr("width", this.poleLength_ * cellSize)
            .attr("height", cellSize)
            .attr("transform", `translate(${cellSize}, ${cellSize})`);

        this.poleLengthInput_ = d3.select("#demo-measures-pole-length");
        this.poleLengthInput_.attr("value", this.poleLength_);
        this.poleLengthInput_.on("change", () => this.onPoleLengthInputChange());

        // Stick
        this.stick_ = svg.append("rect")
            .attr("fill", stickFillColor)
            .attr("stroke", stickStrokeColor)
            .attr("width", this.stickLength_ * cellSize)
            .attr("height", cellSize)
            .attr("transform", `translate(${cellSize}, ${3 * cellSize})`);

        this.stickLengthInput_ = d3.select("#demo-measures-stick-length");
        this.stickLengthInput_.attr("value", this.stickLength_);
        this.stickLengthInput_.on("change", () => this.onStickLengthInputChange());

        // Measures
        this.measures_ = svg.append("g");
        this.updateMeasures();

        // Excess
        this.excess_ = svg.append("g");
        this.updateExcess();

        // Grid
        this.grid_ = svg.append("g")
            .attr("fill", "transparent")
            .attr("stroke-width", 0.25)
            .attr("stroke", "gray")
            .attr("stroke-opacity", 0.5);
        this.makeGrid();

        // Text
        this.text_ = d3.select("#demo-measures-text");
        this.updateText();
    }

    private setPoleLength(length: number): void {
        this.poleLength_ = length;
        this.pole_.attr("width", this.poleLength_ * cellSize)
    }

    private setStickLength(length: number): void {
        this.stickLength_ = length;
        this.stick_.attr("width", this.stickLength_ * cellSize)
    }

    private onPoleLengthInputChange(): void {
        this.doLengthChange(this.poleLengthInput_, x => this.setPoleLength(x));
    }

    private onStickLengthInputChange(): void {
        this.doLengthChange(this.stickLengthInput_, x => this.setStickLength(x));
    }

    private doLengthChange(input: SelBaseType, setLen: (newVal: number) => void): void {
        const min = Number(input.attr("min"));
        const max = Number(input.attr("max"));
        const newValue = Number(d3.event.target.value);

        if (newValue >= min && newValue <= max) {
            setLen(newValue);
            this.updateMeasures();
            this.updateExcess();
            this.updateText();
        }
    }

    private updateMeasures(): void {
        const data = [];
        for (let i = 1; i < this.poleLength_ / this.stickLength_; ++i) {
            data.push(i);
        }

        let measures = this.measures_.selectAll("rect").data(data);
        measures.exit().remove();
        measures.enter().append("rect")
            .attr("height", cellSize)
            .attr("fill", measureFillColor)
            .attr("stroke", measureStrokeColor);

        measures = this.measures_.selectAll("rect").data(data);
        measures
            .attr("transform", (x: number) => `translate(${cellSize + x * this.stickLength_ * cellSize}, ${3 * cellSize})`)
            .attr("width", this.stickLength_ * cellSize);
    }

    private updateExcess(): void {
        const data: [number, number][] = [];

        let remainder = this.poleLength_ % this.stickLength_;

        if (remainder != 0) {
            let div = Math.trunc(this.poleLength_ / this.stickLength_);
            data.push([cellSize
                    + (this.stickLength_ * div * cellSize)
                    + (remainder * cellSize),
                (this.stickLength_ - remainder) * cellSize]);
        }

        let excess = this.excess_.selectAll("rect").data(data);
        excess.exit().remove();
        excess.enter().append("rect")
            .attr("height", cellSize)
            .attr("fill", "url(#excessPattern)");

        excess = this.excess_.selectAll("rect").data(data);
        excess
            .attr("transform", (d: [number, number]) => `translate(${d[0]}, ${3 * cellSize})`)
            .attr("width", (d: [number,number]) => d[1]);
    }

    private makeGrid(): void {
        for (let x = 0; x < 35; ++x) {
            for (let y = 0; y < 5; ++y) {
                this.grid_.append("rect")
                    .attr("transform", `translate(${x * cellSize}, ${y * cellSize})`)
                    .attr("width", cellSize)
                    .attr("height", cellSize);
            }
        }
    }

    private updateText(): void {
        if (this.poleLength_ % this.stickLength_ === 0) {
            this.text_.text(`✔️ Yes, a stick of length ${this.stickLength_} measures a pole of length ${this.poleLength_}.
            In other words, ${this.stickLength_} is a divisor of ${this.poleLength_}.`);
        } else {
            this.text_.text(`❌ No, a stick of length ${this.stickLength_} does not measure a pole of length ${this.poleLength_}.
            In other words, ${this.stickLength_} is not a divisor of ${this.poleLength_}.`);
        }
    }

}



class TilesDiagram {
    private roomLength_ = 25;
    private roomWidth_ = 22;
    private tileSize_ = 5;

    private grid_: SelSVGG;
    private room_: SelSVGRect;
    private tile_: SelSVGRect;
    private text_: SelBaseType;
    private measures_: SelSVGG;
    private excess_: SelSVGG;

    private roomLengthInput_: SelBaseType;
    private roomWidthInput_: SelBaseType;
    private tileSizeInput_: SelBaseType;

    public constructor(containerID: string) {

        let svg = d3.select(`#${containerID} svg`);

        // Pattern
        addExcessPattern(svg);

        // Room
        this.room_ = svg.append("rect")
            .attr("fill", roomFillColor)
            .attr("stroke", roomStrokeColor)
            .attr("stroke-width", 4)
            .attr("width", this.roomWidth_ * cellSize)
            .attr("height", this.roomLength_ * cellSize)
            .attr("transform", `translate(${cellSize}, ${cellSize})`);

        this.roomLengthInput_ = d3.select("#demo-tiles-room-length");
        this.roomLengthInput_.attr("value", this.roomLength_);
        this.roomLengthInput_.on("change", () => this.onRoomLengthInputChange());

        this.roomWidthInput_ = d3.select("#demo-tiles-room-width");
        this.roomWidthInput_.attr("value", this.roomWidth_);
        this.roomWidthInput_.on("change", () => this.onRoomWidthInputChange());

        // Tile
        this.tile_ = svg.append("rect")
            .attr("fill", tileFillColor)
            .attr("stroke", tileStrokeColor)
            .attr("stroke-width", 2)
            .attr("width", this.tileSize_ * cellSize)
            .attr("height", this.tileSize_ * cellSize)
            .attr("transform", `translate(${cellSize}, ${cellSize})`);

        this.tileSizeInput_ = d3.select("#demo-tiles-tile-size");
        this.tileSizeInput_.attr("value", this.tileSize_);
        this.tileSizeInput_.on("change", () => this.onTileSizeInputChange());

        // Measures
        this.measures_ = svg.append("g");
        this.updateMeasures();

        // Excess
        this.excess_ = svg.append("g");
        this.updateExcess();

        // Grid
        this.grid_ = svg.append("g")
            .attr("fill", "transparent")
            .attr("stroke-width", 0.25)
            .attr("stroke", "gray")
            .attr("stroke-opacity", 0.5);
        this.makeGrid();

        // Text
        this.text_ = d3.select("#demo-tiles-text");
        this.updateText();
    }

    private setRoomLength(length: number): void {
        this.roomLength_ = length;
        this.room_.attr("height", this.roomLength_ * cellSize);
    }

    private setRoomWidth(width: number): void {
        this.roomWidth_ = width;
        this.room_.attr("width", this.roomWidth_ * cellSize);
    }

    private setTileSize(size: number): void {
        this.tileSize_ = size;
        this.tile_.attr("width", this.tileSize_ * cellSize)
            .attr("height", this.tileSize_ * cellSize);
    }

    private onRoomLengthInputChange(): void {
        this.doLengthChange(this.roomLengthInput_, x => this.setRoomLength(x));
    }

    private onRoomWidthInputChange(): void {
        this.doLengthChange(this.roomWidthInput_, x => this.setRoomWidth(x));
    }

    private onTileSizeInputChange(): void {
        this.doLengthChange(this.tileSizeInput_, x => this.setTileSize(x));
    }

    private doLengthChange(input: SelBaseType, setLen: (newVal: number) => void): void {
        const min = Number(input.attr("min"));
        const max = Number(input.attr("max"));
        const newValue = Number(d3.event.target.value);

        if (newValue >= min && newValue <= max) {
            setLen(newValue);
            this.updateMeasures();
            this.updateExcess();
            this.updateText();
        }
    }

    private updateMeasures(): void {
        const data: [number, number][] = [];
        for (let i = 0; i < this.roomWidth_ / this.tileSize_; ++i) {
            for (let j = 0; j < this.roomLength_ / this.tileSize_; ++j) {
                if (i !== 0 || j !== 0) data.push([i, j]);
            }
        }

        let measures = this.measures_.selectAll("rect").data(data);
        measures.exit().remove();
        measures.enter().append("rect")
            .attr("height", cellSize)
            .attr("fill-opacity", 0.0)
            .attr("stroke", measureStrokeColor)
            .attr("stroke-width", 1);

        measures = this.measures_.selectAll("rect").data(data);
        measures
            .attr("transform", ([x, y]) => `translate(${cellSize + x * this.tileSize_ * cellSize}, ${cellSize + y * this.tileSize_ * cellSize})`)
            .attr("width", this.tileSize_ * cellSize)
            .attr("height", this.tileSize_ * cellSize);
    }

    private updateExcess(): void {
        type rect = [number, number, number, number]; // X, Y, W, H

        const data: rect[] = [];

        let remainderX = this.roomWidth_ % this.tileSize_;
        let remainderY = this.roomLength_ % this.tileSize_;
        let divX = Math.trunc(this.roomWidth_ / this.tileSize_);
        let divY = Math.trunc(this.roomLength_ / this.tileSize_);

        let excessWidth = remainderX > 0 ? (this.tileSize_ - remainderX) * cellSize : 0;
        let excessLength = remainderY > 0 ? (this.tileSize_ - remainderY) * cellSize : 0;

        if (remainderX != 0) {
            data.push([
                cellSize
                + (this.tileSize_ * divX * cellSize)
                + (remainderX * cellSize),

                cellSize,

                excessWidth,

                (this.roomLength_ * cellSize) + excessLength
            ]);
        }

        if (remainderY != 0) {
            data.push([
                cellSize,

                cellSize
                + (this.tileSize_ * divY * cellSize)
                + (remainderY * cellSize),

                (this.roomWidth_ * cellSize) + excessWidth,

                excessLength
            ]);
        }

        let excess = this.excess_.selectAll("rect").data(data);
        excess.exit().remove();
        excess.enter().append("rect")
            .attr("fill", "url(#excessPattern)");

        excess = this.excess_.selectAll("rect").data(data);
        excess
            .attr("transform", (d: rect) => `translate(${d[0]}, ${d[1]})`)
            .attr("width", (d: rect) => d[2])
            .attr("height", (d: rect) => d[3]);
    }

    private makeGrid(): void {
        for (let x = 0; x < 32; ++x) {
            for (let y = 0; y < 32; ++y) {
                this.grid_.append("rect")
                    .attr("transform", `translate(${x * cellSize}, ${y * cellSize})`)
                    .attr("width", cellSize)
                    .attr("height", cellSize);
            }
        }
    }

    private updateText(): void {
        if (this.roomLength_ % this.tileSize_ === 0 && this.roomWidth_ % this.tileSize_ === 0) {
            this.text_.text(`✔️ Yes, tiles of size ${this.tileSize_} perfectly fill
                a ${this.roomLength_}×${this.roomWidth_} room. In other words, ${this.tileSize_}
                is a common divisor of ${this.roomLength_} and ${this.roomWidth_}.`);
        } else {
            this.text_.text(`❌ No, tiles of size ${this.tileSize_} do not perfectly fill
                a ${this.roomLength_}×${this.roomWidth_} room. In other words, ${this.tileSize_}
                is not a common divisor of ${this.roomLength_} and ${this.roomWidth_}.`);
        }
    }
}



class EuclideanAlgorithmDiagram {
    private func_: (a: number, b: number) => Promise<number>;
    private sbsFunc_: dump.StepByStepFunction | undefined = undefined;
    private funcName_: string;

    private aParam_ = 55;
    private bParam_ = 35;
    private a_ = 5;
    private b_ = 5;
    private step_ = 0;

    private grid_: SelSVGG;
    private rect_: SelSVGRect;
    private tile_: SelSVGRect;

    private text_: SelBaseType;

    private aInput_: SelBaseType;
    private bInput_: SelBaseType;

    private btnReset_: SelBaseType;
    private btnStep_: SelBaseType;


    public constructor(prefixID: string, func: (a: number, b: number) => Promise<number>) {
        this.funcName_ = prefixID;
        this.func_ = func;

        let svg = d3.select(`#${prefixID}-svg`);

        // Pattern
        addExcessPattern(svg);

        // Rectangle
        this.rect_ = svg.append("rect")
            .attr("fill", roomFillColor)
            .attr("stroke", roomStrokeColor)
            .attr("stroke-width", 6)
            .attr("width", this.bParam_ * cellSize)
            .attr("height", this.aParam_ * cellSize)
            .attr("transform", `translate(${cellSize}, ${cellSize})`);

        this.aInput_ = d3.select(`#${prefixID}-param-a`);
        this.aInput_.attr("value", this.aParam_);
        this.aInput_.on("change", () => this.onParamAInputChange());
        this.a_ = this.aParam_;

        this.bInput_ = d3.select(`#${prefixID}-param-b`);
        this.bInput_.attr("value", this.bParam_);
        this.bInput_.on("change", () => this.onParamBInputChange());
        this.b_ = this.bParam_;

        // Tile
        this.tile_ = svg.append("rect")
            .attr("fill", tileFillColor)
            .attr("stroke", tileStrokeColor)
            .attr("stroke-width", 2)
            .attr("width", this.a_ * cellSize)
            .attr("height", this.b_ * cellSize)
            .attr("transform", `translate(${cellSize}, ${cellSize})`);

        // Grid
        this.grid_ = svg.append("g")
            .attr("fill", "transparent")
            .attr("stroke-width", 0.25)
            .attr("stroke", "gray")
            .attr("stroke-opacity", 0.5);
        this.makeGrid();

        // Buttons
        this.btnReset_ = d3.select(`#${prefixID}-reset`);
        this.btnReset_.on("click", () => this.onReset());
        this.btnStep_ = d3.select(`#${prefixID}-step`);
        this.btnStep_.on("click", () => this.onStep());

        // Texts
        this.text_ = d3.select(`#${prefixID}-text`);

        // Initialize
        this.onReset();
    }

    private onReset(): void {
        this.step_ = 0;
        this.a_ = this.aParam_;
        this.b_ = this.bParam_;

        let initSBSFunc = () => {
            this.sbsFunc_ = new dump.StepByStepFunction(this.funcName_, async () => { await this.func_(this.a_, this.b_); } );
            this.sbsFunc_.data.set("a", this.a_);
            this.sbsFunc_.data.set("b", this.b_);
            this.sbsFunc_.data.set("step", this.step_);
            this.updateTexts();
            this.updateTile();
        }

        if (this.sbsFunc_ && this.sbsFunc_.isRunning() && !this.sbsFunc_.isFinished()) {
            this.sbsFunc_.abort(initSBSFunc);
        } else {
            initSBSFunc();
        }
    }

    private onStep(): void {
        if (this.sbsFunc_ == undefined) return;

        this.a_ = this.sbsFunc_.data.get("a");
        this.b_ = this.sbsFunc_.data.get("b");

        if (!this.sbsFunc_.isRunning()) {
            this.sbsFunc_.start();
            this.updateTexts();
            this.updateTile();
        } else if (!this.sbsFunc_.isFinished()) {
            this.sbsFunc_.step(() => {
                if (this.sbsFunc_ != undefined) {
                    this.a_ = this.sbsFunc_.data.get("a");
                    this.b_ = this.sbsFunc_.data.get("b");
                }
                ++this.step_;
                this.updateTexts();
                this.updateTile();
            });
        }
    }

    private setRectLength(length: number): void {
        this.aParam_ = length;
        this.rect_.attr("height", this.aParam_ * cellSize);
    }

    private setRectWidth(width: number): void {
        this.bParam_ = width;
        this.rect_.attr("width", this.bParam_ * cellSize);
    }

    private onParamAInputChange(): void {
        this.doRectSizeChange(this.aInput_, x => this.setRectLength(x));
    }

    private onParamBInputChange(): void {
        this.doRectSizeChange(this.bInput_, x => this.setRectWidth(x));
    }

    private doRectSizeChange(input: SelBaseType, setLen: (newVal: number) => void): void {
        const min = Number(input.attr("min"));
        const max = Number(input.attr("max"));
        const newValue = Number(d3.event.target.value);

        if (newValue >= min && newValue <= max) {
            setLen(newValue);
            this.updateTexts();
            this.onReset();
        }
    }

    private makeGrid(): void {
        for (let x = 0; x < 62; ++x) {
            for (let y = 0; y < 62; ++y) {
                this.grid_.append("rect")
                    .attr("transform", `translate(${x * cellSize}, ${y * cellSize})`)
                    .attr("width", cellSize)
                    .attr("height", cellSize);
            }
        }
    }

    private updateTexts(): void {
        var text = "";

        if (this.sbsFunc_ != undefined) {
            var a = this.sbsFunc_.data.get("a").toString();
            var b = this.sbsFunc_.data.get("b").toString();
            var step = this.step_;

            if (this.sbsFunc_.isFinished()) {
                text = `Finished in ${step} steps; a = ${a}, b = ${b}.`;
            } else if (!this.sbsFunc_.isRunning()) {
                text = `Not running. Click "Step" to start.`;
            } else {
                text = `Running... step ${step}, a = ${a}, b = ${b}.`;
            }
        }

        this.text_.text(text);
    }

    private updateTile(): void {
        this.tile_.attr("width", this.b_ * cellSize)
            .attr("height", this.a_ * cellSize);
    }
}


let measuresDiagram = new MeasuresDiagram("demo-measures");
let tilesDiagram = new TilesDiagram("demo-tiles");
let subtractiveDiagram = new EuclideanAlgorithmDiagram("demo-euclid-subtractive", subtractiveEuclideanAlgorithm);
let modernDiagram = new EuclideanAlgorithmDiagram("demo-euclid-modern", euclideanAlgorithm);
