import React from 'react';
import PropTypes from 'prop-types';
// utils
import { isNaN, isFunction, isEqual } from '../libs/utils';
// style
import './index.css';

class ReactPictureViewer extends React.Component {
  static propTypes = {
    width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), // viewport 视口的宽度
    height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), // viewport 视口的高度
    minimum: PropTypes.number, // 缩放的最小尺寸【零点几】
    maximum: PropTypes.number, // 缩放的最大尺寸
    rate: PropTypes.number, // 缩放的速率
    children: PropTypes.object.isRequired, // slot 插槽
    className: PropTypes.string, // className
    center: PropTypes.bool, // 图片位置是否初始居中
    contain: PropTypes.bool, // 图片尺寸是否初始包含在视口范围内
    cover: PropTypes.bool,
  };

  static defaultProps = {
    width: '600px',
    height: '400px',
    minimum: 0.8,
    maximum: 8,
    rate: 10,
    center: true,
    contain: true,
  };

  state = {
    focus: false, // 鼠标是否按下，处于可拖动状态
    imageWidth: 0, // 图片宽度
    imageHeight: 0, // 图片高度
    startX: 0, // 鼠标按下时，距离 viewport 的初始 X 位置
    startY: 0, // 鼠标按下时，距离 viewport 的初始 Y 位置
    startLeft: 0, // 图片距离 viewport 的初始 Left
    startTop: 0, // 图片距离 viewport 的初始 Top
    currentLeft: 0, // 图片当前距离 viewport 的 left
    currentTop: 0, // 图片当前距离 viewport 的 top
    scale: 1, // 图片缩放比率 minimum - maximum
    hasLoaded: false, // 图片是否已经加载
  };

  constructor() {
    super();
    this.viewportDOM = React.createRef();
    this.imgDOM = React.createRef();
  }

  componentDidMount() {
    const { width, height } = this.props;

    this.imgDOM.current = this.viewportDOM.current.querySelector('img');

    this.initViewport(width, height);
    // 这边需要将滚轮事件使用原生绑定来处理
    // 从而解决新版本 chrome 浏览器带来的 passive event listener
    // 在对图片进行滚动缩放时无法使用 e.preventDefault 来禁用浏览器滚动问题
    this.imgDOM.current.addEventListener('wheel', this.handleMouseWheel, {
      passive: false,
    });

    this.initPicture();
  }

  componentWillReceiveProps(nextProps) {
    // 如果检测到 props 确实有变化，再去重新 init
    const flag =
      !isEqual(this.props, nextProps, 'children') ||
      !isEqual(this.props.children.props, nextProps.children.props);
    flag && this.initPicture(nextProps);
  }

  shouldComponentUpdate(nextProps, nextState, nextContext) {
    return (
      !isEqual(this.state, nextState) ||
      !isEqual(this.props, nextProps, 'children') ||
      !isEqual(this.props.children.props, nextProps.children.props)
    );
  }

  componentWillUpdate(nextProps, nextState) {
    const {
      scale,
      imageWidth: originWidth,
      imageHeight: originHeight,
      currentLeft,
      currentTop,
    } = nextState;
    const currentImageWidth = scale * originWidth;
    const currentImageHeight = scale * originHeight;

    // 改变图片位置
    this.changePosition(currentLeft, currentTop);
    // 改变图片尺寸
    this.changeSize(currentImageWidth, currentImageHeight);
  }

  initViewport = (width, height) => {
    // 如果是字符串，就将字符串作为尺寸设置；否则是数字的话，就在后面加 px 设置
    this.viewportDOM.current.style.width = isNaN(+width) ? width : `${width}px`;
    this.viewportDOM.current.style.height = isNaN(+height)
      ? height
      : `${height}px`;
  };

  /**
   * 图片初始化，包括：
   * 1. 初始图片位置居中
   * 2. 记录初始图片尺寸
   * @param nextProps
   */
  initPicture = nextProps => {
    nextProps = nextProps || this.props;

    const {
      children: {
        props: { src },
      },
      center,
      contain,
      cover,
    } = nextProps;
    const callback = center ? this.changeToCenter : this.changeToBasePoint;

    // 这块有个执行顺序
    // 必须是先确定尺寸，再确定位置
    if (cover) {
      this.changeToCover(src, callback);
    } else if (contain) {
      this.changeToContain(src, callback);
    } else {
      this._getImageOriginSize(src)
        .then(({ width: imageWidth, height: imageHeight }) => {
          this.setState(
            {
              scale: 1,
              imageWidth,
              imageHeight,
            },
            callback,
          );
        })
        .catch(e => {
          console.error(e);
        });
    }
  };

  /**
   * 设置图片尺寸为 contain
   * @param src {String} 需要操作的图片的 src
   * @param callback {Function} changeToContain 完成后的回调函数，接受更新后的图片尺寸，即 imageWidth 和 imageHeight 两个参数
   */
  changeToContain = (src, callback) => {
    src = src || this.props.src;
    callback = isFunction(callback) ? callback : () => {};

    this._getImageOriginSize(src)
      .then(({ width: imageOriginWidth, height: imageOriginHeight }) => {
        const { imageWidth, imageHeight } = this.recalcImageSizeToContain(
          imageOriginWidth,
          imageOriginHeight,
        );
        this.setState(
          {
            scale: 1,
            imageWidth,
            imageHeight,
          },
          () => {
            callback(imageWidth, imageHeight);
          },
        );
      })
      .catch(e => {
        console.error(e);
      });
  };

  /**
   * 设置图片位置为 center
   */
  changeToCenter = () => {
    const { imageWidth, imageHeight } = this.state;
    const [viewportDOM] = [this.viewportDOM.current];
    const [viewPortWidth, viewPortHeight] = [
      viewportDOM.clientWidth,
      viewportDOM.clientHeight,
    ];
    // 设置图片默认位置居中
    const [top, left] = [
      (viewPortHeight - imageHeight) / 2,
      (viewPortWidth - imageWidth) / 2,
    ];

    this.setState({
      currentLeft: left,
      currentTop: top,
      startLeft: left,
      startTop: top,
    });
  };

  /**
   * 设置图片位置为基准点位置
   * 基准点位置，基于视口: top: 0 && left: 0
   */
  changeToBasePoint = () => {
    this.setState({
      currentLeft: 0,
      currentTop: 0,
      startLeft: 0,
      startTop: 0,
    });
  };

  /**
   * 重新计算图片尺寸，使宽高都不会超过视口尺寸
   * @param imageWidth
   * @param imageHeight
   * @returns {*}
   */
  recalcImageSizeToContain = (imageWidth, imageHeight) => {
    const rate = imageWidth / imageHeight;
    const viewportDOM = this.viewportDOM.current;
    const [viewPortWidth, viewPortHeight] = [
      viewportDOM.clientWidth,
      viewportDOM.clientHeight,
    ];
    if (imageWidth > viewPortWidth) {
      imageWidth = viewPortWidth;
      imageHeight = imageWidth / rate;
      return this.recalcImageSizeToContain(imageWidth, imageHeight);
    } else if (imageHeight > viewPortHeight) {
      imageHeight = viewPortHeight;
      imageWidth = imageHeight * rate;
      return this.recalcImageSizeToContain(imageWidth, imageHeight);
    } else {
      return { imageWidth, imageHeight };
    }
  };

  recalcImageSizeToCover = (imageWidth, imageHeight) => {
    const rate = imageWidth / imageHeight;
    const viewportDOM = this.viewportDOM.current;
    const [viewPortWidth, viewPortHeight] = [
      viewportDOM.clientWidth,
      viewportDOM.clientHeight,
    ];
    // 先将宽度设置到容器宽度
    imageWidth = viewPortWidth;
    imageHeight = imageWidth / rate;
    if (imageHeight < viewPortHeight) {
      imageHeight = viewPortHeight;
      imageWidth = imageHeight * rate;
    }
    return { imageWidth, imageHeight };
  };

  /**
   * 设置图片尺寸为 cover
   */
  changeToCover = (src, callback) => {
    src = src || this.props.src;
    callback = isFunction(callback) ? callback : () => {};

    this._getImageOriginSize(src)
      .then(({ width: imageOriginWidth, height: imageOriginHeight }) => {
        const { imageWidth, imageHeight } = this.recalcImageSizeToCover(
          imageOriginWidth,
          imageOriginHeight,
        );
        this.setState(
          {
            scale: 1,
            imageWidth,
            imageHeight,
          },
          () => {
            callback(imageWidth, imageHeight);
          },
        );
      })
      .catch(e => {
        console.error(e);
      });
  }; // eslint-disable-line

  /**
   * 改变图片位置
   * @param currentLeft {Number} 当前 left
   * @param currentTop {Number} 当前 top
   */
  changePosition(currentLeft, currentTop) {
    const imgDOM = this.imgDOM.current;
    imgDOM.style.top = `${currentTop}px`;
    imgDOM.style.left = `${currentLeft}px`;
  }

  /**
   * 调整尺寸
   * @param width
   * @param height
   */
  changeSize(width, height) {
    const imgDOM = this.imgDOM.current;
    imgDOM.style.maxWidth = imgDOM.style.maxHeight = 'none';
    imgDOM.style.width = `${width}px`;
    imgDOM.style.height = `${height}px`;
  }

  /**
   * 处理鼠标按下
   * @param e
   */
  handleMouseDown = e => {
    const currentDOM = e.target || e.toElement;
    if (currentDOM !== this.imgDOM.current) return;

    let { top: startY, left: startX } = this._getOffsetInElement(
      e,
      this.viewportDOM.current,
    );
    this.setState({
      focus: true,
      startX,
      startY,
    });
  };

  /**
   * 处理鼠标移动
   * @param e
   */
  handleMouseMove = e => {
    const { focus, startX, startY, startTop, startLeft } = this.state;
    if (!focus) return;

    let { left: currentX, top: currentY } = this._getOffsetInElement(
      e,
      this.viewportDOM.current,
    );
    let [diffX, diffY] = [currentX - startX, currentY - startY];

    this.setState({
      currentLeft: startLeft + diffX,
      currentTop: startTop + diffY,
    });
  };

  /**
   * 处理鼠标放开
   */
  handleMouseUp = () => {
    const { currentLeft, currentTop } = this.state;
    this.setState({
      focus: false,
      startX: 0,
      startY: 0,
      startLeft: currentLeft,
      startTop: currentTop,
    });
  };

  /**
   * 处理鼠标移出
   */
  handleMouseLeave = () => {
    this.handleMouseUp();
  };

  changeScale = (scale, igonreLimit = false) => {
    let nextScale = scale
    const { scale:nowScale } = this.state
    const { minimum, maximum } = this.props;
    if (!igonreLimit) {
      // 如果放大倍率已超过最大倍率且继续放大，则保持原大小
      // 如果缩小倍率已超过最小倍率且继续缩小，则保持原大小
      if ((nowScale >= maximum && nextScale >= nowScale) || (nowScale <= minimum && nextScale <= nowScale)) {
        nextScale = nowScale
      } else {
        nextScale =
        scale <= minimum
          ? minimum
          : scale >= maximum
          ? maximum
          : scale;
      }
    }
    this.setState({
      scale: nextScale
    })
  }

  /**
   * 处理滚轮缩放
   * @param e {Event Object} 事件对象
   */
  handleMouseWheel = e => {
    const imgDOM = this.imgDOM.current;
    const { minimum, maximum, rate } = this.props;
    const {
      imageWidth: originWidth,
      imageHeight: originHeight,
      currentLeft,
      currentTop,
      scale: lastScale,
    } = this.state;
    const [imageWidth, imageHeight] = [imgDOM.clientWidth, imgDOM.clientHeight];
    const event = e.nativeEvent || e;
    event.preventDefault();
    // 这块的 scale 每次都需要用 1 去加，作为图片的实时缩放比率
    let scale = 1 + event.wheelDelta / (12000 / rate);

    // 最小缩放至 minimum 就不能再缩小了
    // 最大放大至 maximum 倍就不能再放大了
    if (
      (lastScale <= minimum && scale < 1) ||
      (lastScale >= maximum && scale > 1)
    )
      return;

    // 真实的图片缩放比率需要用尺寸相除
    let nextScale = (imageWidth * scale) / originWidth;

    // 进行缩放比率检测
    // 如果小于最小值，使用原始图片尺寸和最小缩放值
    // 如果大于最大值，使用最大图片尺寸和最大缩放值
    nextScale =
      nextScale <= minimum
        ? minimum
        : nextScale >= maximum
        ? maximum
        : nextScale;
    let currentImageWidth = nextScale * originWidth;
    let currentImageHeight = nextScale * originHeight;

    let { left, top } = this._getOffsetInElement(e, this.imgDOM.current);
    let rateX = left / imageWidth;
    let rateY = top / imageHeight;
    let newLeft = rateX * currentImageWidth;
    let newTop = rateY * currentImageHeight;

    this.setState({
      scale: nextScale,
      startLeft: currentLeft + (left - newLeft),
      startTop: currentTop + (top - newTop),
      currentLeft: currentLeft + (left - newLeft),
      currentTop: currentTop + (top - newTop),
    });
  };

  /**
   * 获取鼠标当前相对于某个元素的位置
   * @param e        {object}    原生事件对象
   * @param target {DOMobject} 目标DOM元素
   * @return object 包括offsetLeft和offsetTop
   *
   * Tips:
   * 1.offset 相关属性在 display: none 的元素上失效，为0
   * 2.offsetWidth/offsetHeight 包括border-width，clientWidth/clientHeight不包括border-width，只是可见区域而已
   * 3.offsetLeft/offsetTop 是从当前元素边框外缘开始算，一直到定位父元素的距离，clientLeft/clientTop其实就是border-width
   */
  _getOffsetInElement = (e, target) => {
    let currentDOM = e.target || e.toElement;
    if (!this._inTargetArea(currentDOM, target)) return null;
    let left, top, right, bottom;
    const { left: x, top: y } = this._getOffset(target);
    left = e.clientX - x;
    top = e.clientY - y;
    right = target.offsetWidth - left;
    bottom = target.offsetHeight - top;
    return { top, left, right, bottom };
  };

  /**
   * 判断一个DOM元素是否包裹在另一个DOM元素中【父子关系或者层级嵌套都可以】
   * @param  {Object} DOM         事件对象中的event.target/或者是需要检测的DOM元素
   * @param  {Object} targetDOM   作为限制范围的DOM元素
   * @return {Boolean}            true----是包裹关系，false----不是包裹关系
   */
  _inTargetArea = (DOM, targetDOM) => {
    if (DOM === targetDOM) return true;
    let parent = DOM.parentNode;
    while (parent != null) {
      if (parent === targetDOM) return true;
      DOM = parent;
      parent = DOM.parentNode;
    }
    return false;
  };

  /**
   * 获取某个 DOM 元素相对视口的位置信息
   * @param el {object} 目标元素
   * @return object {object} 位置信息对象
   */
  _getOffset = el => {
    const doc = document.documentElement;
    const docClientWidth = doc.clientWidth;
    const docClientHeight = doc.clientHeight;
    let positionInfo = el.getBoundingClientRect();
    return {
      left: positionInfo.left,
      top: positionInfo.top,
      right: docClientWidth - positionInfo.right,
      bottom: docClientHeight - positionInfo.bottom,
    };
  };

  /**
   * 获取图片原始尺寸信息
   * @param image
   * @returns {Promise<any>}
   * @private
   */
  _getImageOriginSize = image => {
    const src = typeof image === 'object' ? image.src : image;
    const self = this;

    return new Promise(resolve => {
      const image = new Image();
      image.src = src;
      image.onload = function() {
        // 图片首次加载完成修改状态
        if (!self.state.hasLoaded) {
          self.setState({ hasLoaded: true });
        }
        const { width, height } = image;
        resolve({
          width,
          height,
        });
      };
    });
  };

  render() {
    const { children, className } = this.props;
    return (
      <div
        ref={this.viewportDOM}
        className={`react-picture-viewer ${
          this.state.hasLoaded ? '' : 'react-picture-viewer-bofore-loaded'
        } ${className}`}
        onMouseLeave={this.handleMouseLeave}
        onMouseDown={this.handleMouseDown}
        onMouseMove={this.handleMouseMove}
        onMouseUp={this.handleMouseUp}
      >
        {children}
      </div>
    );
  }
}

export default ReactPictureViewer;
