import React = require("react");

export interface Vector2 {
    x: number,
    y: number
}

interface Box {
    min: Vector2;
    max: Vector2;
}

const paperColor = "#f4f4f4";

export type Stroke = Vector2[];

function mul(a: Vector2, b: number): Vector2 {
    return { x: a.x * b, y: a.y * b };
}

function add(a: Vector2, b: Vector2): Vector2 {
    return { x: a.x + b.x, y: a.y + b.y };
}
/**Resta 2 vectores */
function sub(a: Vector2, b: Vector2): Vector2 {
    return { x: a.x - b.x, y: a.y - b.y };
}

/**Producto punto de dos vectores */
function dot(a: Vector2, b: Vector2): number {
    return a.x * b.x + a.y * b.y;
}

/**Longitud de un vector */
function len(a: Vector2): number {
    return Math.sqrt(dot(a, a));
}

function min(a: Vector2, b: Vector2): Vector2 {
    return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y) };
}

function max(a: Vector2, b: Vector2): Vector2 {
    return { x: Math.max(a.x, b.x), y: Math.max(a.y, b.y) };
}

function boxFromPoints(points: Vector2[]): Box {
    return {
        min: points.reduce(min),
        max: points.reduce(max)
    }
}

function boxFromBoxes(boxes: Box[]): Box {
    return {
        min: boxes.map(x => x.min).reduce(min),
        max: boxes.map(x => x.max).reduce(max)
    };
}

function signatureWidth(deltaLen: number) {
    return Math.min(Math.round(0.5 + (15 / (deltaLen + 1))), 4);
}

/**Dibuja una linea de una firma */
function drawStroke2(c: CanvasRenderingContext2D, a: Vector2, b: Vector2) {
    const delta = sub(a, b);
    const deltaLen = len(delta);

    c.lineCap = "round";
    c.lineWidth = signatureWidth(deltaLen);
    c.beginPath();
    c.moveTo(b.x, b.y);
    c.lineTo(a.x, a.y);
    c.stroke();
}


/**Agrupa un arreglo todos los elementos contiguos que encajen entre si */
function groupByAdjacent<T>(items: T[], isAdjacentGroup: (a: T, b: T) => boolean): T[][] {
    if (items.length == 0)
        return [];

    const ret: T[][] = [];
    let current: T[] = [];
    for (var x of items) {
        if (ret.length == 0 && current.length == 0) {
            current.push(x);
        } else {
            const last = current[current.length - 1];
            if (isAdjacentGroup(last, x)) {
                current.push(x);
            } else {
                //Agregamos current:
                ret.push(current);
                current = [x];
            }
        }
    }
    ret.push(current);
    return ret;
}

/**Dibuja un trazo de una firma */
function drawStroke(c: CanvasRenderingContext2D, stroke: Stroke) {
    if (stroke.length < 2) return;

    //Por cada punto agrupamos los anchos:
    const widths = stroke
        .map((x, i, arr) => {
            const a = arr[i - 1];
            const b = x;
            const delta = a && sub(a, b);
            const deltaLen = a && len(delta);
            const width = a && signatureWidth(deltaLen);

            return { x: x.x, y: x.y, width: width };
        })
        .map((x, i, arr) => i == 0 ? Object.assign({}, x, { width: arr[1].width }) : x)
        ;

    //Agrupamos por ancho de linea:
    const groups = groupByAdjacent(widths, (a, b) => a.width == b.width).map(x => ({ width: x[0].width, points: x.map(y => y as Vector2) }));
    //A cada grupo le pegamos el ultimo punto del grupo anterior:
    const groups2 = groups.map((x, i, arr) => {
        if (i == 0)
            return x;
        else {
            const puntosAnt = arr[i - 1].points;
            const puntoAnt = puntosAnt[puntosAnt.length - 1];
            const nuevosPuntos = [puntoAnt].concat(x.points);

            return Object.assign({}, x, { points: nuevosPuntos })
        }
    });

    //Dibuja los grupos
    for (var st of groups2) {
        c.lineCap = "round";
        c.lineWidth = st.width;
        c.lineJoin = "round";
        c.beginPath();

        const points = st.points;
        c.moveTo(points[0].x, points[0].y);
        for (var i = 1; i < points.length; i++) {
            c.lineTo(points[i].x, points[i].y);
        }

        c.stroke();
    }
}



function drawStrokes(c: CanvasRenderingContext2D, strokes: Stroke[], target?: Box) {
    if (strokes.length == 0) return;
    if (target) {
        //Escalamos la caja para que encaje en target:
        const strokeBox = boxFromBoxes(strokes.map(boxFromPoints));

        const targetSize = sub(target.max, target.min);
        const strokeSize = sub(strokeBox.max, strokeBox.min);
        const scale = Math.min(targetSize.y / strokeSize.y, targetSize.x / strokeSize.x);

        const pretransform = (x: Vector2) => add(mul(sub(x, strokeBox.min), scale), target.min);
        const preTransformStrokeBox = { min: pretransform(strokeBox.min), max: pretransform(strokeBox.max) };

        const boxCenter = (box: Box) => mul(add(box.min, box.max), 0.5);

        const centerTranslate = sub(boxCenter(target), boxCenter(preTransformStrokeBox));
        const transform = (x: Vector2) => add(pretransform(x), centerTranslate);

        const newStroke = strokes.map(x => x.map(transform));

        drawStrokes(c, newStroke);
    } else {
        for (var s of strokes) {
            drawStroke(c, s);
        }
    }
}


function mouseToVector(source: Element, pageX: number, pageY: number): Vector2 {
    const bounding = source.getBoundingClientRect();
    const boundingMin = { x: bounding.left, y: bounding.top };
    const scroll = { x: window.scrollX, y: window.scrollY };
    const offset = add(boundingMin, scroll);

    const eventPage = { x: pageX, y: pageY };

    return sub(eventPage, offset);
}
class CanvasSignature {
    constructor(canvas: HTMLCanvasElement, onStroke: (stroke: Vector2[]) => void) {
        this.canvas = canvas;
        this.onStroke = onStroke;
        this.context = canvas.getContext("2d")!;

        this.addEventListeners();
    }

    private addEventListeners() {
        const c = this.canvas;
        c.addEventListener("mouseup", this.onMouseUp);
        c.addEventListener("touchend", this.onTouchEnd);
        c.addEventListener("touchstart", this.onTouchStart);

        c.addEventListener("mousemove", this.onMouseMove);
        c.addEventListener("touchmove", this.onTouchMove);
    }

    public removeEventListeners() {
        const c = this.canvas;
        c.removeEventListener("mouseup", this.onMouseUp);
        c.removeEventListener("touchend", this.onTouchEnd);
        c.removeEventListener("touchstart", this.onTouchStart);

        c.removeEventListener("mousemove", this.onMouseMove);
        c.removeEventListener("touchmove", this.onTouchMove);
    }

    onResize = () => {
        this.context = this.canvas.getContext("2d")!;
    }

    onStroke: (stroke: Vector2[]) => void;
    private canvas: HTMLCanvasElement;
    private context: CanvasRenderingContext2D;
    private touchDown: boolean;
    private lastDownPos: Vector2 | null = null;

    private currentStroke: Vector2[] = [];

    public isReadOnly: boolean = false;

    onTouchStart = () => {
        this.touchDown = true;
    }
    onTouchEnd = () => {
        this.touchDown = false;
        this.onMouseUp();
    }

    onTouchMove = (event: TouchEvent) => {
        const down = this.touchDown;
        const dibujando = down && !this.isReadOnly;
        if (dibujando) {
            //prevenir el scroll en el celular, ocasiona problemas con la firma en android y iphone
            event.preventDefault();
        }
        const touch = event.touches[0];
        const pos = mouseToVector(event.srcElement!, touch.pageX, touch.pageY);

        this.processMouseMove(down, pos);
    }

    onMouseMove = (event: MouseEvent) => {
        //Si el mouse esta presionado
        const down = event.buttons == 1;
        const pos = mouseToVector(event.srcElement!, event.pageX, event.pageY);

        this.processMouseMove(down, pos);
    }
    processMouseMove(down: boolean, pos: Vector2) {
        const lastPos = this.lastDownPos;

        if (down && !this.isReadOnly) {
            this.currentStroke.push(pos);
            if (lastPos) {
                drawStroke2(this.context, pos, lastPos);
            }

            this.lastDownPos = pos;
        } else {
            this.lastDownPos = null;
        }
    }

    onMouseUp = () => {
        if (this.currentStroke.length > 1) {
            this.onStroke(this.currentStroke);
        }
        this.currentStroke = [];
        this.lastDownPos = null;
    }
}



export interface SignaturePadProps {
    /**Firma que se va a mostrar */
    value: Stroke[];
    /**Se dispara cada vez que se cambia la firma */
    onChange?: (value: Stroke[]) => void;
    /**True para encajar la firma al tamaño del canvas*/
    zoomToFit?: boolean;
}

/**Elemento editor de una firma */
export class SignatureCanvas extends React.Component<SignaturePadProps, {}> {
    handleCanvas = (canvas: HTMLCanvasElement | null) => {
        if (canvas == null) {
            this.canvas = undefined;
            this.drawer.removeEventListeners();
            window.removeEventListener("resize", this.onResize);
        } else {
            this.canvas = canvas;
            this.setCanvasSize();
            this.drawer = new CanvasSignature(canvas, this.handleStroke);
            this.refresh(this.props);
            window.addEventListener("resize", this.onResize);
            this.onResize();
        }

    }

    componentWillUnmount() {
        window.removeEventListener("resize", this.onResize);
    }

    private drawer: CanvasSignature;

    onResize = () => {
        const doResize = () => {
            if (this.setCanvasSize()) {
                this.drawer.onResize();
            }
        };
        const times = [10, 50, 100, 200];
        doResize();
        for (var time of times) {
            setTimeout(doResize, time);
        }
    }

    componentWillReceiveProps(props: SignaturePadProps) {
        this.refresh(props);
    }

    handleStroke = (stroke: Stroke) => {
        if (this.props.onChange) {
            this.props.onChange(this.props.value.concat([stroke]))
            this.refresh(this.props);
        }
    }

    /**Dibuja los strokes */
    refresh(props: SignaturePadProps) {
        const strokes = props.value;
        if (!this.canvas) return;
        if (this.drawer)
            this.drawer.isReadOnly = props.onChange == null;
        //Limpia el canvas
        const context = this.canvas.getContext("2d")!;
        context.clearRect(0, 0, this.canvas.width, this.canvas.height);

        //Dibujamos los strokes:
        const margin = 10;
        const targetBox: Box = { min: { x: margin, y: margin }, max: { x: this.canvas.width - margin * 2, y: this.canvas.height - margin * 2 } };
        drawStrokes(context, strokes, this.props.zoomToFit ? targetBox : undefined);
    }

    canvas?: HTMLCanvasElement;
    private setCanvasSize(): boolean {
        if (this.canvas) {
            const newWidth = this.canvas.offsetWidth;
            const newHeight = this.canvas.offsetHeight;

            if (this.canvas.width != newWidth || this.canvas.height != newHeight) {
                this.canvas.width = newWidth;
                this.canvas.height = newHeight;

                this.refresh(this.props);
                return true;
            }
        }
        return false;
    }

    render() {
        return (
            <canvas
                ref={this.handleCanvas}
                style={{
                    background: paperColor,
                    width: "100%",
                    height: "100%"
                }} />
        );
    }
}
