import Immutable from 'immutable';
import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import classSet from 'classnames';

// import FlatButton from 'ui/FlatButton';
// import Icon from 'ui/Icon';

import './index.css';

/**
 * Флаг, включающий отображение отладочного изображения с результатом редактирования
 * @constant
 * @type {boolean}
 */
const DEBUG_IMAGE_ENABLED = false;

/**
 * Список доступных направлений перемещений
 * @constant
 * @type {Array}
 */
const DRAG_DIRECTIONS = [
    'nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se',
];

/**
 * Таблица противоположных направлений перемещений
 * @constant
 * @type {Object}
 */
const OPPOSITE_DRAG_DIRECTION = {
    nw: 'se',
    n: 's',
    ne: 'sw',
    w: 'e',
    e: 'w',
    sw: 'ne',
    s: 'n',
    se: 'nw',
};

/**
 * Вычисляет абсолютное положение элемента
 * @param   {Node}    element
 * @returns {Object}  { left: number, top: number }
 */
function getPosition(element) {
    const position = { left: 0, top: 0 };

    while (element.offsetParent) {
        position.left += element.offsetLeft;
        position.top += element.offsetTop;
        element = element.offsetParent;
    }

    return position;
}

/**
 * Возвращает тип курсора для заданного направления перемещения
 * @param   {String}  direction
 * @returns {String}
 */
function getCursor(direction) {
    return `${direction}-resize`;
}

/**
 * Возвращает расстояние между точками вдоль координатных осей
 * @param   {Object}  point1  { x: number, y: number }  координаты точки 1
 * @param   {Object}  point2  { x: number, y: number }  координаты точки 2
 * @returns {Object}  { dx: number, dy: number }
 */
function getDelta(point1, point2) {
    return {
        dx: point2.x - point1.x,
        dy: point2.y - point1.y,
    };
}

/*
Составляющие кроппера:
- scaledImage: оригинальное изображение из выбранного пользователем файла,
  ужатое средствами CSS до размеров компонента;
- overlay: тёмный слой, оттеняющий лежащее под ним изображение scaledImage;
- frame: пунктирная рамка области выделения поверх тёмного слоя;
- frameImage: то же оригинальное изображение внутри рамки выделения,
  не закрытое тёмным слоем.
*/

const Cropper = React.createClass({

    mixins: [PureRenderMixin],

    /**
     * Возвращает начальные стили основных компонентов кроппера
     * @returns {Immutable.Map}
     */
    _getInitialStyles() {
        return Immutable.fromJS({
            scaledImage: {},
            frame: {},
            frameImage: {},
        });
    },

    getInitialState() {
        return {
            src: this.props.src,
            styles: this._getInitialStyles(),
        };
    },

    componentWillMount() {
        this._reset();
    },

    componentDidMount() {
        this._scale();
        this.setState({ key: Math.random() });
    },

    componentWillUnmount() {
        if (this._buffer) {
            this._buffer.forEach((buffer, index) => this._buffer[index] = null);
            this._buffer = null;
        }
        if (this._demo) {
            document.body.removeChild(this._demo);
            this._demo = null;
        }
    },

    componentWillUpdate(nextProps, nextState) {
        if (this.state.src !== nextState.src) {
            this._changed = true;
            this._reset(nextState);
        }
    },

    /**
     * Устанавливает изображение для редактирования
     * @param   {String}  src  путь к изображению или его base64-представление
     */
    setImage(src) {
        this.setState({ src });
    },

    /**
     * Очищает область редактирования
     */
    discard() {
        this.setImage(null);
    },

    /**
     * Возвращает base64-представление отредактированного изображения
     * @returns {String}
     */
    getSelectedImage() {
        const { src, styles, failed } = this.state;

        if (!src || failed || !this._changed) {
            return null;
        }

        if (!this._buffer) {
            this._buffer = [
                document.createElement('canvas'),
                document.createElement('canvas'),
            ];

            if (DEBUG_IMAGE_ENABLED) {
                this._demo = document.createElement('img');
                this._demo.style = 'position:absolute;left:0;top:0;z-index:20000;';
                document.body.appendChild(this._demo);
            }
        }

        const { frame, scaledImage } = styles.toJS();
        const context = this._buffer.map(buffer => buffer.getContext('2d'));

        const rescaledFrame = {};
        const scalingRatio = this._getScalingRatio();

        // вычисляем размеры области выделения кроппера относительно
        // исходных размеров изображения
        ['left', 'top', 'width', 'height'].forEach(entry => {
            rescaledFrame[entry] = frame[entry] / scalingRatio;
        });

        const container = ReactDOM.findDOMNode(this.refs.container);

        // в первый буфер переносим полное изображение из кроппера
        // в исходном размере, включая поля вокруг изображения
        this._buffer[0].width = container.offsetWidth / scalingRatio;
        this._buffer[0].height = container.offsetHeight / scalingRatio;

        context[0].fillStyle = '#fff';
        context[0].fillRect(0, 0, this._buffer[0].width, this._buffer[0].height);

        context[0].drawImage(
            this._image,
            0, 0, this._image.width, this._image.height,
            (scaledImage.left || 0) / scalingRatio, (scaledImage.top || 0) / scalingRatio,
            this._image.width, this._image.height
        );

        // во второй буфер переносим часть первого буфера, соответствующую
        // выделенной области в кроппере
        this._buffer[1].width = rescaledFrame.width;
        this._buffer[1].height = rescaledFrame.height;

        context[1].drawImage(
            this._buffer[0],
            rescaledFrame.left, rescaledFrame.top, rescaledFrame.width, rescaledFrame.height,
            0, 0, rescaledFrame.width, rescaledFrame.height
        );

        if (this._demo) {
            this._demo.src = this._buffer[1].toDataURL('image/jpeg');
        }

        return this._buffer[1].toDataURL('image/jpeg');
    },

    /**
     * Возвращает коэффициент сжатия изображения в контейнере кроппера
     * @returns {Number}
     */
    _getScalingRatio() {
        const container = ReactDOM.findDOMNode(this.refs.container);
        const image = this._image;

        if (!container || !image) {
            return;
        }

        return image.width > image.height ?
            Math.min(container.offsetWidth, image.width) / image.width :
            Math.min(container.offsetHeight, image.height) / image.height;
    },

    /**
     * Вызывает перерисовку изображения в кроппере
     * @param {Object}  state
     */
    _reset(state) {
        if (!state) {
            state = this.state;
        }

        this._image = new Image();
        this._image.onload = this._handleImageLoad;
        this._image.onerror = this._handleImageFail;
        this._image.src = state.src;
    },

    /**
     * Обрабатывает успешную загрузку изображения
     */
    _handleImageLoad() {
        this.setState({
            styles: this._getInitialStyles(),
            failed: false,
        });
        this._scale();
    },

    /**
     * Обрабатывает ошибку при загрузке изображения
     */
    _handleImageFail() {
        this.setState({
            failed: true,
        });
    },

    /**
     * Устанавливает размеры и положение основных компонент кроппера:
     * scaledImage, frame, frameImage;
     * их назначение описано перед определением компонента
     */
    _scale() {
        const container = ReactDOM.findDOMNode(this.refs.container);
        const image = this._image || ReactDOM.findDOMNode(this.refs.image);

        if (!container || !image) {
            return;
        }

        const { scaledImage, frame, frameImage } = this.state.styles.toJS();
        let ratio;

        // вписываем редактируемое изображение в контейнер кроппера
        if (image.width > image.height) {
            scaledImage.width = Math.min(container.offsetWidth, image.width);
            ratio = scaledImage.width / image.width;
            scaledImage.height = ratio * image.height;
        } else {
            scaledImage.height = Math.min(container.offsetHeight, image.height);
            ratio = scaledImage.height / image.height;
            scaledImage.width = ratio * image.width;
        }

        // если исходное изображение меньше размеров контейнера,
        // то у изображения будут положительные отступы от границ контейнера
        scaledImage.left = (container.offsetWidth - scaledImage.width) / 2;
        scaledImage.top = (container.offsetHeight - scaledImage.height) / 2;

        // задаем начальные размеры и положение области выделения
        if (frame.width === undefined || frame.height === undefined) {
            const defaultFrameSize = 0.8 * Math.min(scaledImage.width, scaledImage.height);
            const minFrameSize = this.props.minSize * ratio;

            frame.width = Math.max(defaultFrameSize, minFrameSize);
            frame.height = frame.width;
            frame.left = (scaledImage.left || 0) + (scaledImage.width - frame.width) / 2;
            frame.top = (scaledImage.top || 0) + (scaledImage.height - frame.height) / 2;
        }

        frameImage.width = scaledImage.width;
        frameImage.height = scaledImage.height;
        frameImage.marginLeft = -(scaledImage.width - frame.width) / 2;
        frameImage.marginTop = -(scaledImage.height - frame.height) / 2;

        this.setState({
            styles: Immutable.fromJS({ scaledImage, frame, frameImage }),
        });
    },

    /**
     * Возвращает координаты точки, в которой произошло событие `event`
     * @param   {Object}  event
     * @returns {Object}  { x: number, y: number, direction: String, withinFrame: boolean }
     */
    _getPoint(event) {
        const container = ReactDOM.findDOMNode(this.refs.container);
        const containerPosition = getPosition(container);

        const point = {
            x: event.pageX - containerPosition.left,
            y: event.pageY - containerPosition.top,
        };

        if (event.target.classList.contains('cropper-knob')) {
            point.direction = DRAG_DIRECTIONS.filter(direction =>
                event.target.classList.contains(`cropper-knob__${direction}`))[0];
        }

        const frame = ReactDOM.findDOMNode(this.refs.frame);

        point.withinFrame = event.target === frame || frame.contains(event.target);

        return point;
    },

    /**
     * Обрабатывает нажатия кнопки мыши
     * @param   {Object}  event
     */
    _handleMouseDown(event) {
        this._startDragging(event);
    },

    /**
     * Обрабатывает перемещения курсора
     * @param   {Object}  event
     */
    _handleMouseMove(event) {
        this._drag(event);
    },

    /**
     * Обрабатывает отжатие кнопки мыши
     */
    _handleMouseUp() {
        this._stopDragging();
    },

    /**
     * Обрабатывает выход курсора за пределы контейнера
     */
    _handleMouseLeave() {
        this._stopDragging();
    },

    /**
     * Обрабатывает перемещения курсора в кроппере
     * @param   {Object}  event
     */
    _drag(event) {
        if (!this._startPoint) {
            return;
        }

        event.preventDefault();

        const { direction, withinFrame } = this._startPoint;

        if (direction) {
            this._dragToResize(event);
        } else if (withinFrame) {
            this._dragAround(event);
        }
    },

    /**
     * Обрабатывает перетаскивание, изменяющее размеры области выделения
     * @param   {Object}  event
     */
    _dragToResize(event) {
        const currentPoint = this._getPoint(event);

        if (!this._startPoint || !currentPoint) {
            return;
        }

        const { frame } = this.state.styles.toJS();
        let frameSide;

        switch (this._startPoint.direction) {
            case 'nw':
                frameSide = Math.abs(this._fixedPoint.x - currentPoint.x);
                frame.left = this._fixedPoint.x - frameSide;
                frame.top = this._fixedPoint.y - frameSide;
                break;

            case 'n':
                frameSide = Math.abs(this._fixedPoint.y - currentPoint.y);
                frame.left = this._fixedPoint.x - frameSide / 2;
                frame.top = this._fixedPoint.y - frameSide;
                break;

            case 'ne':
                frameSide = Math.abs(this._fixedPoint.x - currentPoint.x);
                frame.left = this._fixedPoint.x;
                frame.top = this._fixedPoint.y - frameSide;
                break;

            case 'w':
                frameSide = Math.abs(this._fixedPoint.x - currentPoint.x);
                frame.left = this._fixedPoint.x - frameSide;
                frame.top = this._fixedPoint.y - frameSide / 2;
                break;

            case 'e':
                frameSide = Math.abs(this._fixedPoint.x - currentPoint.x);
                frame.left = this._fixedPoint.x;
                frame.top = this._fixedPoint.y - frameSide / 2;
                break;

            case 'sw':
                frameSide = Math.abs(this._fixedPoint.x - currentPoint.x);
                frame.left = this._fixedPoint.x - frameSide;
                frame.top = this._fixedPoint.y;
                break;

            case 's':
                frameSide = Math.abs(this._fixedPoint.y - currentPoint.y);
                frame.left = this._fixedPoint.x - frameSide / 2;
                frame.top = this._fixedPoint.y;
                break;

            case 'se':
                frameSide = Math.abs(this._fixedPoint.x - currentPoint.x);
                frame.left = this._fixedPoint.x;
                frame.top = this._fixedPoint.y;
                break;
        }

        frame.width = frameSide;
        frame.height = frameSide;

        this._updateFrame(frame);
    },

    /**
     * Обрабатывает перетаскивание, изменяющее положение области выделения
     * @param   {Object}  event
     */
    _dragAround(event) {
        const currentPoint = this._getPoint(event);

        if (!this._startPoint || !currentPoint) {
            return;
        }

        const { frame } = this.state.styles.toJS();
        const delta = getDelta(this._startPoint, currentPoint);

        const x = this._initialFramePosition.x + delta.dx;
        const y = this._initialFramePosition.y + delta.dy;

        const container = ReactDOM.findDOMNode(this.refs.container);
        const frameElement = ReactDOM.findDOMNode(this.refs.frame);

        // ограничиваем положение рамки выделения границами контейнера
        frame.left = Math.min(Math.max(0, x), container.offsetWidth - frameElement.offsetWidth);
        frame.top = Math.min(Math.max(0, y), container.offsetHeight - frameElement.offsetHeight);

        this._updateFrame(frame);
    },

    /**
     * Нормирует и обновляет стили области выделения
     * @param   {Object}  frame  изменённые стили области выделения
     */
    _updateFrame(frame) {
        const containerImage = ReactDOM.findDOMNode(this.refs.containerImage);

        let styles = this.state.styles;
        const { frameImage } = styles.toJS();

        this._constrainFrame(frame);

        frameImage.marginLeft = -frame.left + containerImage.offsetLeft;
        frameImage.marginTop = -frame.top + containerImage.offsetTop;

        styles = styles
            .set('frame', frame)
            .set('frameImage', frameImage);

        this.setState({ styles, key: Math.random() });
    },

    /**
     * Ограничивает размеры области выделения значением, заданным в `props.minSize`
     * @param   {Object}  frame  стили области выделения
     * @returns {Object}
     */
    _constrainFrame(frame) {
        const minFrameSize = this.props.minSize * this._getScalingRatio();

        frame.width = Math.max(frame.width, minFrameSize);
        frame.height = Math.max(frame.height, minFrameSize);

        return frame;
    },

    /**
     * Обрабатывает начало перетаскивания
     * @param   {Object}  event
     */
    _startDragging(event) {
        // в начале перемещений фиксируем координаты подвижной точки
        this._startPoint = this._getPoint(event);

        const { direction, withinFrame } = this._startPoint;

        if (direction) {
            // если тянем за угол рамки выделения,
            // в начале также фиксируем координаты неподвижной точки -
            // угол, противоположный тому, за который тянем
            this._fixedPoint = this._getFrameCornerPosition(
                OPPOSITE_DRAG_DIRECTION[direction]
            );
        } else if (withinFrame) {
            // если тянем за точку внутри рамки выделения,
            // в начале фиксируем исходное положение рамки
            this._initialFramePosition = this._getFrameCornerPosition();
        }

        this.setState({
            styles: this.state.styles.set('container', {
                cursor: getCursor(direction),
            }),
        });
    },

    /**
     * Обрабатывает окончание перетаскивания
     */
    _stopDragging() {
        this._startPoint = null;

        this.setState({
            styles: this.state.styles.remove('container'),
        });

        if (DEBUG_IMAGE_ENABLED) {
            this.getSelectedImage();
        }
    },

    /**
     * Возвращает положение угла области выделения кроппера
     * @param   {String}  type  тип, расположение угла
     * @returns {Object}  { x: number, y: number }
     */
    _getFrameCornerPosition(type) {
        // по умолчанию - левый верхний угол, north-west
        if (!type) {
            type = 'nw';
        }

        const frame = ReactDOM.findDOMNode(this.refs.frame);
        let x;
        let y;

        const style = window.getComputedStyle(frame);
        const paddings = ['Top', 'Right', 'Bottom', 'Left'].map(side => parseFloat(style[`padding${side}`]));

        const box = {
            left: frame.offsetLeft,
            top: frame.offsetTop,
            width: frame.offsetWidth - (paddings[1] + paddings[3]),
            height: frame.offsetHeight - (paddings[0] + paddings[2]),
        };

        switch (type) {
            case 'nw':
                x = box.left;
                y = box.top;
                break;

            case 'n':
                x = box.left + box.width / 2;
                y = box.top;
                break;

            case 'ne':
                x = box.left + box.width;
                y = box.top;
                break;

            case 'w':
                x = box.left;
                y = box.top + box.height / 2;
                break;

            case 'e':
                x = box.left + box.width;
                y = box.top + box.height / 2;
                break;

            case 'sw':
                x = box.left;
                y = box.top + box.height;
                break;

            case 's':
                x = box.left + box.width / 2;
                y = box.top + box.height;
                break;

            case 'se':
                x = box.left + box.width;
                y = box.top + box.height;
                break;
        }

        return { x, y };
    },

    /**
     * Возвращает массив подвижных точек области выделения кроппера
     * @returns {Array}
     */
    _renderDragControls() {
        return DRAG_DIRECTIONS.map((direction, index) => (
            <div
                key={index}
                ref={`${direction}Knob`}
                className={`cropper-knob cropper-knob__${direction}`}
            />
        ));
    },

    render() {
        const { defaultSrc, imageDescription } = this.props;
        const { src, failed, key } = this.state;
        let { styles } = this.state;

        const isEmpty = !src || failed;
        let image;

        styles = styles.toJS();

        if (isEmpty && defaultSrc) {
            styles.container = {
                backgroundImage: `url(${defaultSrc})`,
                backgroundColor: 'transparent',
            };
        }

        if (!isEmpty) {
            image = <img src={src} ref="image" alt={imageDescription} />;
        }

        const className = classSet({
            cropper: true,
            cropper_intact: !this._changed,
            cropper_empty: isEmpty,
        });

        return (
            <div
                className={className}
                ref="container"
                key={key}
                style={styles.container}
                onMouseDown={this._handleMouseDown}
                onMouseUp={this._handleMouseUp}
                onMouseMove={this._handleMouseMove}
                onMouseLeave={this._handleMouseLeave}
            >

                <div className="cropper-image" ref="containerImage" style={styles.scaledImage}>
                    {image}
                </div>
                <div className="cropper-overlay" />
                <div className="cropper-frame" ref="frame" style={styles.frame}>
                    <div className="cropper-inner-frame">
                        <img
                            src={src}
                            ref="image"
                            style={styles.frameImage}
                        />
                    </div>
                    {this._renderDragControls()}
                </div>

                {/* отключаем кнопку "сбросить аватар" до поддержки в Директории
                <FlatButton className="cropper__discard" onClick={this.discard}>
                    <Icon type="trash" size="s"/>
                </FlatButton>
                */}

            </div>
        );
    },

});

// @if NODE_ENV='development'
Cropper.propTypes = {
    src: PropTypes.string,
    defaultSrc: PropTypes.string,
    minSize: PropTypes.number,
    imageDescription: PropTypes.string,
};
// @endif

Cropper.defaultProps = {
    minSize: 0,
};

export default Cropper;
