import React from 'react';
import PropTypes from 'prop-types';
import { createPointerEventListener, getPointFromPointerEvent } from 'Utils/Events/pointerEvent';
import { withResizeDetectWrapper } from 'Shared/HOCs/ResizeDetect/ResizeDetect';
import { rgbToHex } from 'App/Reducers/DesignArea/Selectors';
import Progress from 'App/Shared/Components/Progress/Progress';

function getColorAt(ctx, x, y) {
    const p = ctx.getImageData(x, y, 1, 1).data;
    return {
        rgba: { r: p[0], g: p[1], b: p[2], a: p[3] },
        hex: rgbToHex(p[0], p[1], p[2]),
    };
}

/**
 * Saves the imageData of the source context, and returns a function which can clear the
 * given area and restore the original data.
 * @param sourceCtx
 * @returns {function(*, *=, *=, *=, *=)}
 */
const createAreaClearer = sourceCtx => {
    let imageData;
    if (sourceCtx) {
        const { width, height } = sourceCtx.canvas;
        imageData = sourceCtx.getImageData(0, 0, width, height);
    }
    return (ctx, x, y, w, h) => {
        ctx.clearRect(x, y, w, h);
        imageData && ctx.putImageData(imageData, 0, 0, x, y, w + 1, h + 1);
    };
};

/**
 * By default the indicator is shown on the left side, but if the user moves the pointer close to it,
 * it jumps to the right side.
 * @param ctx
 * @param x
 * @param y
 * @returns {{size: number, offsetX: number, offsetY: number}}
 */
const getIndicatorPosition = (ctx, x, y) => {
    let offsetX = 10;
    let offsetY = 10;
    const size = 50;
    const tolerance = 50;
    if (x < offsetX + size + tolerance && y < offsetY + size + tolerance) {
        offsetX = ctx.canvas.width - offsetX - size;
    }
    return {
        size,
        offsetX,
        offsetY,
    };
};

/**
 * Draws the indicator on the given context depending on the mouse position (x, y).
 * It returns the dirty area as an array to clear: [x, y, size].
 * @param ctx
 * @param x
 * @param y
 * @returns {[number,number,number]}
 */
const drawIndicator = (ctx, x, y) => {
    const { offsetX, offsetY, size } = getIndicatorPosition(ctx, x, y);
    const lineWidth = 2;
    const shadowBlur = 1;
    ctx.fillStyle = `rgba(${Object.values(getColorAt(ctx, x, y).rgba)})`;
    ctx.fillRect(offsetX, offsetY, size, size);
    ctx.lineWidth = lineWidth;
    ctx.strokeStyle = 'white';
    ctx.shadowColor = 'black';
    ctx.shadowBlur = shadowBlur;
    ctx.strokeRect(offsetX, offsetY, size, size);
    const padding = lineWidth + shadowBlur;
    return [offsetX - padding, offsetY - padding, size + 2 * padding];
};

/**
 * Draws the marker circle on the given context (from the center x and y).
 * It returns the dirty area as an array to clear: [x, y, size].
 * @param ctx
 * @param x
 * @param y
 * @returns {[number,number,number]}
 */
const drawMarker = (ctx, x, y) => {
    const lineWidth = 2;
    const shadowBlur = 1;
    const size = 5;
    const posOffset = shadowBlur / 2;
    ctx.strokeStyle = 'white';
    ctx.lineWidth = lineWidth;
    ctx.shadowBlur = shadowBlur;
    ctx.shadowColor = 'black';
    ctx.beginPath();
    ctx.arc(x + posOffset, y + posOffset, size, 0, 2 * Math.PI);
    ctx.stroke();
    const padding = Math.ceil(lineWidth + shadowBlur + size);
    return [x - padding, y - padding, (padding + shadowBlur) * 2];
};

export class ImageColorPicker extends React.Component {
    static propTypes = {
        className: PropTypes.string,
        enabled: PropTypes.bool,
        onChange: PropTypes.func,
        onError: PropTypes.func,
        src: PropTypes.string,
        size: PropTypes.object,
    };
    static defaultProps = {
        enabled: true,
    };
    state = {
        loadingImage: false,
    };
    canvasRef = React.createRef();

    componentDidMount() {
        const { src } = this.props;
        if (src) {
            this.loadImage(src);
        }
        const canvas = this.canvasRef.current;
        this.ctx = canvas.getContext('2d');

        this.removeStartListener = createPointerEventListener(canvas, 'start', evt => {
            if (!this.props.enabled || !this.img) {
                return;
            }
            evt.preventDefault();
            this.redrawMarker(evt);
            const removeMoveListener = createPointerEventListener(canvas, 'move', this.redrawMarker);
            const removeEndListener = createPointerEventListener(canvas, 'end', evt => {
                removeMoveListener();
                removeEndListener();
                const { onChange } = this.props;
                const { x, y } = this.getRelativePosition(evt);
                onChange && onChange(getColorAt(this.ctx, x, y));
            });
        });
    }

    componentWillUnmount() {
        this.removeStartListener();
    }

    reportError = e => {
        // eslint-disable-next-line no-console
        console.error(e);
        const { onError } = this.props;
        onError && onError(e);
    };

    redrawMarker = evt =>
        setTimeout(() => {
            const { x, y } = this.getRelativePosition(evt);
            if (this.indicatorPosition) {
                let [x, y, size] = this.indicatorPosition;
                this.clearArea(this.ctx, x, y, size, size);
            }
            if (this.markerPosition) {
                let [x, y, size] = this.markerPosition;
                this.clearArea(this.ctx, x, y, size, size);
            }
            this.markerPosition = drawMarker(this.ctx, x, y);
            this.indicatorPosition = drawIndicator(this.ctx, x, y);
        });

    componentDidUpdate(prevProps) {
        const { src, size } = this.props;
        if (prevProps.src !== src) {
            src && this.loadImage(src);
        }
        if (prevProps.size !== size) {
            this.updateCanvas();
        }
    }

    loadImage(src) {
        this.currentSrc = src;
        this.setState({ loadingImage: true });
        const img = document.createElement('img');
        img.crossOrigin = 'anonymous';
        img.onload = () => {
            // skip if the src is outdated
            if (this.currentSrc !== src) {
                return;
            }
            this.img = img;
            this.setState({ loadingImage: false });
            this.updateCanvas();
        };
        img.onerror = e => {
            this.setState({ loadingImage: false });
            this.reportError(e);
        };
        img.src = src;
    }

    updateCanvas() {
        const { img } = this;
        const canvas = this.canvasRef.current;
        if (!canvas || !img) {
            return;
        }
        const container = canvas.parentElement;
        const { width: pw, height: ph } = container.getBoundingClientRect();
        const { width: ow, height: oh } = img;

        if (ow / oh <= pw / ph) {
            const height = ph;
            const width = height * (ow / oh);

            this.updateCanvasDimensionsAndContent(canvas, img, width, height);
        } else {
            const width = pw;
            const height = width * (oh / ow);

            this.updateCanvasDimensionsAndContent(canvas, img, width, height);
        }

        try {
            this.clearArea = createAreaClearer(this.ctx);
        } catch (e) {
            this.reportError(e);
        }
    }

    updateCanvasDimensionsAndContent(canvas, img, width, height) {
        Object.assign(canvas, { width, height });
        this.ctx.clearRect(0, 0, width, height);
        this.ctx.drawImage(img, 0, 0, width, height);
    }

    getRelativePosition(evt) {
        const canvas = this.canvasRef.current;
        const { left: offsetX, top: offsetY } = canvas.getBoundingClientRect();
        const { x: eX, y: eY } = getPointFromPointerEvent(evt);
        const x = eX - offsetX;
        const y = eY - offsetY;
        return {
            x,
            y,
        };
    }

    render() {
        const { className } = this.props;
        const { loadingImage } = this.state;
        return (
            <React.Fragment>
                <Progress loading={loadingImage} />
                <canvas ref={this.canvasRef} className={className} />
            </React.Fragment>
        );
    }
}

export default withResizeDetectWrapper(ImageColorPicker);
