import React from 'react';
import PropTypes from 'prop-types';
import Button from '../Button';
import Icon from '../Icon';
import TextIcon from '../TextIcon';
import { isDataURL } from '../_helpers/tools';

import './cropper.scss';
import SelectInput from '../SelectInput/index';

function dataURLtoFile(dataurl, filename) {
  const arr = dataurl.split(',');
  const mime = arr[0].match(/:(.*?);/)[1];
  const bstr = atob(arr[1]);
  const u8arr = new Uint8Array(bstr.length);
  let n = bstr.length;

  while (n--) { // eslint-disable-line no-plusplus
    u8arr[n] = bstr.charCodeAt(n);
  }
  return new File([u8arr], filename, { type: mime });
}


const propTypes = {
  value: PropTypes.shape({
    preview: PropTypes.string.isRequired,
    meta: PropTypes.object,
    type: PropTypes.string.isRequired
  }).isRequired,
  sizes: PropTypes.arrayOf(PropTypes.shape({
    width: PropTypes.number,
    height: PropTypes.number,
    label: PropTypes.string
  })),
  initialSize: PropTypes.string,
  onCancelCrop: PropTypes.func.isRequired,
  onCrop: PropTypes.func.isRequired,
  meta: PropTypes.object
};

const defaultProps = {
  sizes: [
    {
      width: 400,
      height: 300,
      label: 'ratio 4/3 (400px / 300px)'
    },
    {
      width: 400,
      height: 400,
      label: 'ratio 1/1 (400px / 400px)'
    }
  ],
  initialSize: 'contain',
  meta: undefined
};

class Cropper extends React.Component {
  state = {
    selectedSize: 0,
    dragging: false,
    image: {},
    mouse: {
      x: null,
      y: null
    },
    preview: null,
    zoom: 1,
    compression: 0.91,
    fill: '#ffffff'
  };

  listeners = []; // eslint-disable-line react/sort-comp

  componentDidMount() {
    const canvas = this.canvasNode;
    const { value, initialSize, meta } = this.props;
    if (!canvas) {
      return;
    }
    const context = canvas.getContext('2d');
    context.scale(2, 2);
    this.prepareImage(value.preview, initialSize, meta || null);

    this.listeners = {
      mousemove: e => this.mouseMoveListener(e),
      mouseup: e => this.mouseUpListener(e),
      mousedown: e => this.mouseDownListener(e)
    };

    window.addEventListener('mousemove', this.listeners.mousemove, false);
    window.addEventListener('mouseup', this.listeners.mouseup, false);
    canvas.addEventListener('mousedown', this.listeners.mousedown, false);
    document.onselectstart = e => this.preventSelection(e);
  }


  componentWillReceiveProps({ value, initialSize, meta }) {
    if (value.preview !== this.props.value.preview) {
      this.prepareImage(value.preview, initialSize, meta || null);
    }
  }

  componentDidUpdate() {
    this.resetSize();
  }

  // make sure we clean up listeners when unmounted.
  componentWillUnmount() {
    this.colorSelectNode.blur();
    const canvas = this.canvasNode;
    window.removeEventListener('mousemove', this.listeners.mousemove);
    window.removeEventListener('mouseup', this.listeners.mouseup);
    canvas.removeEventListener('mousedown', this.listeners.mousedown);
  }

  getRefCanvas = canvasNode => { this.canvasNode = canvasNode; };
  getRefColorSelect = colorSelectNode => { this.colorSelectNode = colorSelectNode; };

  resetSize(customSelectedSize) {
    const { sizes } = this.props;
    const { selectedSize, zoom } = this.state;
    const { width, height } = sizes[customSelectedSize || selectedSize];
    const context = this.canvasNode.getContext('2d');
    context.clearRect(0, 0, width, height);
    if (typeof customSelectedSize === 'number' && customSelectedSize !== selectedSize) {
      setTimeout(() => context.scale(2, 2), 0);
    }
    this.addImageToCanvas(context, this.state.image, zoom, customSelectedSize);
  }

  fitImageToCanvas(mode = 'cover') {
    const { image } = this.state;
    const zoom = this._calculateZoomToFit(mode, image.width, image.height);
    const { x, y } = this._calculateImageAlignement('center', image.width * zoom, image.height * zoom);
    this.setState({
      image: { ...image, x, y }, zoom
    });
  }

  prepareImage(imageUri, initialSize = 'contain', meta = null) {
    const img = new Image();
    if (!isDataURL(imageUri)) img.crossOrigin = 'anonymous';
    img.onload = () => {
      const scaledImage = {
        height: img.height,
        width: img.width,
        resource: img
      };
      let zoom;
      let x;
      let y;
      if (meta !== null) {
        x = meta.x;
        y = meta.y;
        zoom = meta.zoom;
      } else {
        zoom = this._calculateZoomToFit(initialSize, img.width, img.height);
        const imageAlignement = this._calculateImageAlignement('center', img.width * zoom, img.height * zoom);
        x = imageAlignement.x;
        y = imageAlignement.y;
      }
      const { fill, compression } = meta !== null ? meta : this.state;
      scaledImage.x = x;
      scaledImage.y = y;
      this.setState({ dragging: false, image: scaledImage, preview: this.toDataURL(), zoom, fill, compression });
    };
    img.src = imageUri;
  }

  mouseDownListener() {
    this.setState({
      dragging: true,
      mouse: {
        x: null,
        y: null
      }
    });
  }

  _calculateZoomToFit(mode = 'cover', originalWidth, originalHeight) {
    const { sizes } = this.props;
    const { selectedSize } = this.state;
    const { width, height } = sizes[selectedSize];
    const canvasAspectRatio = height / width;
    const imageAspectRatio = originalHeight / originalWidth;
    let zoom;

    if (mode === 'cover') {
      zoom = canvasAspectRatio > imageAspectRatio ?
        height / originalHeight
        : width / originalWidth;
    } else {
      zoom = canvasAspectRatio > imageAspectRatio ?
        width / originalWidth
        : height / originalHeight;
    }
    return zoom;
  }

  _calculateImageAlignement(mode = 'center', scaledWidth, scaledHeight) {
    const { sizes } = this.props;
    const { image: { x: imageX, y: imageY }, selectedSize } = this.state;
    const { width, height } = sizes[selectedSize];
    let x;
    let y;

    switch (mode) {
      case 'top-left':
        x = 0;
        y = 0;
        break;
      case 'top':
        x = (width - scaledWidth) / 2;
        y = 0;
        break;
      case 'top-right':
        x = (width - scaledWidth);
        y = 0;
        break;
      case 'left':
        x = 0;
        y = (height - scaledHeight) / 2;
        break;
      case 'center':
        x = (width - scaledWidth) / 2;
        y = (height - scaledHeight) / 2;
        break;
      case 'right':
        x = (width - scaledWidth);
        y = (height - scaledHeight) / 2;
        break;
      case 'bottom-left':
        x = 0;
        y = (height - scaledHeight);
        break;
      case 'bottom':
        x = (width - scaledWidth) / 2;
        y = (height - scaledHeight);
        break;
      case 'bottom-right':
        x = (width - scaledWidth);
        y = (height - scaledHeight);
        break;
      default:
        x = imageX;
        y = imageY;
    }

    return { x, y };
  }

  alignImage(mode = 'cover') {
    const { image, image: { width, height }, zoom } = this.state;
    const { x, y } = this._calculateImageAlignement(mode, width * zoom, height * zoom);
    this.setState({ image: { ...image, x, y } });
  }

  preventSelection(e) {
    if (this.state.dragging) {
      e.preventDefault();
    }
  }

  mouseUpListener() {
    this.setState({ dragging: false });
  }

  mouseMoveListener(e) {
    if (!this.state.dragging) return;

    const mouseX = e.clientX;
    const mouseY = e.clientY;
    const imageX = this.state.image.x;
    const imageY = this.state.image.y;

    const newImage = this.state.image;
    if (this.state.mouse.x && this.state.mouse.y) {
      const dx = this.state.mouse.x - mouseX;
      const dy = this.state.mouse.y - mouseY;

      newImage.x = imageX - dx;
      newImage.y = imageY - dy;
    }

    this.setState({
      image: this.state.image,
      mouse: {
        x: mouseX,
        y: mouseY
      }
    });
  }

  addImageToCanvas(context, image, zoom, customSelectedSize) {
    if (!image.resource) return;
    const { sizes } = this.props;
    const { zoom: defaultZoom, selectedSize } = this.state;
    const { width, height } = sizes[customSelectedSize || selectedSize];

    const resultZoom = zoom || defaultZoom;
    context.save();
    context.fillStyle = this.state.fill; // eslint-disable-line no-param-reassign
    context.fillRect(0, 0, width * 2, height * 2);
    context.globalCompositeOperation = 'source-over'; // eslint-disable-line no-param-reassign
    context.drawImage(image.resource, image.x, image.y, image.width * resultZoom, image.height * resultZoom);
    context.restore();
  }

  toDataURL(exporting = false) {
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    const { sizes } = this.props;
    const { selectedSize } = this.state;
    const { width, height } = sizes[selectedSize];

    let { x, y } = this.state.image;
    let zoom = this.state.zoom;
    const { height: imageHeight, width: imageWidth, resource } = this.state.image;

    if (exporting) {
      canvas.width = width;
      canvas.height = height;
      if (this.state.zoom <= 0.8) {
        canvas.width = width * 2;
        canvas.height = height * 2;
        x *= 2;
        y *= 2;
        zoom *= 2;
      }
    } else {
      context.scale(2, 2);
      canvas.width = width * 2;
      canvas.height = height * 2;
    }

    this.addImageToCanvas(context, {
      x, y, height: imageHeight, width: imageWidth, resource
    }, zoom);

    let dataUrl;
    if (exporting) {
      dataUrl = canvas.toDataURL('image/jpeg', this.state.compression);
    } else {
      dataUrl = canvas.toDataURL();
    }

    return dataUrl;
  }

  handleCrop = () => {
    const { zoom, fill, compression, image: { x, y } } = this.state;
    const { onCrop, value: { name } } = this.props;
    const preview = this.toDataURL(true);
    let newName;
    if (name.lastIndexOf('.') > 0) {
      newName = name.replace(/\.[^.$]+$/, '.jpg');
    } else {
      newName = `${name}.jpg`;
    }
    const data = dataURLtoFile(preview, newName);
    data.preview = preview;
    data.meta = {
      zoom, x, y, fill, compression
    };
    onCrop(data);
  };

  handleZoomUpdate = e => {
    this.setState({ zoom: parseFloat(e.target.value, 10) });
  };

  handleChangeSize = (size = 0) => {
    const { selectedSize } = this.state;
    if (selectedSize !== size) {
      this.resetSize(size);
      this.setState({ selectedSize: size });
    }
  };

  render() {
    const { zoom, fill, selectedSize } = this.state;
    const { sizes, onCancelCrop } = this.props;
    const { width, height } = sizes[selectedSize];

    const displayWidth = Math.min(width, 908);
    const displayHeight = width === displayWidth ? height
      : Math.round((height / width) * displayWidth);

    const qualityTitle = (<strong>Image quality :</strong>);
    let zoomNode;
    switch (true) {
      case zoom <= 0.5:
        zoomNode = (<span styleName="helper high">{qualityTitle} Great, even on retina screens</span>);
        break;
      case zoom <= 1:
        zoomNode = (<span styleName="helper medium">{qualityTitle} Ok, but not sharp on retina</span>);
        break;
      default:
        zoomNode = (<span styleName="helper low">{qualityTitle} Ugly on any display</span>);
    }
    const qualityAlert = (
      <div styleName="helpers size">
        <Button
          styleName="helper-button"
          onClick={() => this.setState({ zoom: 0.5 })}
          variant="icon"
          size="sm"
          type="button"
        ><TextIcon name="resize-small" size="xs"> Set 50%</TextIcon></Button>
        <Button
          styleName="helper-button"
          onClick={() => this.setState({ zoom: 1 })}
          variant="icon"
          size="sm"
          type="button"
        ><TextIcon name="resize-full" size="xs">Set 100%</TextIcon></Button>
        <Button
          styleName="helper-button"
          onClick={() => this.fitImageToCanvas('cover')}
          variant="icon"
          size="sm"
          type="button"
        ><TextIcon name="resize-cover" size="xs">Cover</TextIcon></Button>
        <Button
          styleName="helper-button"
          onClick={() => this.fitImageToCanvas('contain')}
          variant="icon"
          size="sm"
          type="button"
        ><TextIcon name="resize-contain" size="xs">Contain</TextIcon></Button>
        {zoomNode}
      </div>
    );

    return (
      <div styleName="base">
        <div styleName="content-canvas" style={{ width: displayWidth }}>
          <canvas
            ref={this.getRefCanvas}
            width={width * 2}
            height={height * 2}
            style={{ width: displayWidth, height: displayHeight }}
          />
          <div styleName="helpers">
            <div styleName="helper">Display dimensions: {width}x{height}</div>
            <div styleName="helper">
              {'Recommended minimum original size for great HDPI/Retina rendering : '}
              {width * 2}x{height * 2}
            </div>
          </div>
        </div>
        <div styleName="form">
          <div>
            <div styleName="setting">
              <div styleName="select-size">
                <strong styleName="name">Image Size :</strong>
                <SelectInput
                  value={selectedSize}
                  options={sizes.map(({ label }, row) => ({ value: row, label }))}
                  onChange={this.handleChangeSize}
                />
              </div>
              <strong styleName="name">Zoom level :</strong>
              <div>
                <div styleName="slider-zoom">
                  <span styleName="value">
                    {Math.round(zoom * 100)} %
                  </span>
                  <input
                    type="range"
                    name="zoom"
                    onChange={this.handleZoomUpdate}
                    min="0.01"
                    max="2"
                    step="0.0001"
                    defaultValue={zoom}
                    value={zoom}
                  />
                </div>
                {qualityAlert}
              </div>
            </div>
            <div styleName="setting">
              <strong styleName="name">Alignment</strong>
              <div styleName="alignment-selector">
                <button onClick={() => this.alignImage('top-left')} type="button">
                  <Icon name="up" size="xs" styleName="arrow up-left" />
                </button>
                <button onClick={() => this.alignImage('top')} type="button">
                  <Icon name="up" size="xs" styleName="arrow" />
                </button>
                <button onClick={() => this.alignImage('top-right')} type="button">
                  <Icon name="up" size="xs" styleName="arrow up-right" />
                </button>
                <button onClick={() => this.alignImage('left')} type="button">
                  <Icon name="left" size="xs" styleName="arrow" />
                </button>
                <button onClick={() => this.alignImage('center')} type="button" />
                <button onClick={() => this.alignImage('right')} type="button">
                  <Icon name="right" size="xs" styleName="arrow" />
                </button>
                <button onClick={() => this.alignImage('bottom-left')} type="button">
                  <Icon name="down" size="xs" styleName="arrow down-left" />
                </button>
                <button onClick={() => this.alignImage('bottom')} type="button">
                  <Icon name="down" size="xs" styleName="arrow" />
                </button>
                <button onClick={() => this.alignImage('bottom-right')} type="button">
                  <Icon name="down" size="xs" styleName="arrow down-right" />
                </button>
              </div>
            </div>
            <div styleName="setting">
              <strong styleName="name">Background color</strong>
              <div>
                <input
                  ref={this.getRefColorSelect}
                  type="color"
                  value={fill}
                  styleName="backgroundPicker"
                  onChange={e => this.setState({ fill: e.target.value })}
                />
              </div>
            </div>
          </div>
          <Button
            onClick={onCancelCrop}
            variant="cancel"
            type="button"
          >
            Cancel
          </Button>
          <Button
            onClick={this.handleCrop}
            type="button"
          >
            Crop
          </Button>
        </div>
      </div>
    );
  }
}

Cropper.propTypes = propTypes;
Cropper.defaultProps = defaultProps;

export default Cropper;
