'use strict';

/** @jsx createElement */

import { Component, createElement, PropTypes, render } from 'rax';
import View from 'nuke-view';
import Text from 'nuke-text';
import Item from './item';
import ThemeProvider from 'nuke-theme-provider';
import stylesProvider from '../styles';

const { connectStyle } = ThemeProvider;
import { Animate, easeInOutQuad } from './animate';

const noop = () => { };

const VISIBLE_ITEM_COUNT = 5;
const MAX_TOUCH_PATH_LENGTH = 20;
const VELOCITY_DETERMINE_PERIOD = 100;
const MILLISECONDS = 1000;
const MIN_VELOCITY = 50;
const VELOCITY_DECREASE_RATE = 0.9;
const BOUNCE_RESISTANCE = 0.5;
const BOUNCE_RESISTANCE_DISTANCE = 200; // px

function getComputedStyleHeight(el) {
  if (typeof window === 'undefined') {
    return 0;
  }
  return parseFloat(window.getComputedStyle(el).height);
}

class PickerColumn extends Component {
  static propTypes = {
    value: PropTypes.any,
    dataSource: PropTypes.array,
    onChange: PropTypes.func,
    labelMap: PropTypes.func,
    valueMap: PropTypes.func,
  };

  static defaultProps = {
    selectedKey: null,
    dataSource: [],
    onChange: noop,
    labelMap,
    valueMap,
  };

  static contextTypes = {
    onUpdate: PropTypes.func,
    onChange: PropTypes.func,
    __picker__: PropTypes.bool,
    selectedValue: PropTypes.any,
  };

  static childContextTypes = {
    __column__: PropTypes.bool,
    selectedValue: PropTypes.any,
  };

  constructor(props, context) {
    super(props);

    let value;

    if (context.__picker__) {
      value = context.selectedValue[props.index];
    } else if ('value' in props) {
      value = props.value;
    } else {
      value = props.selectedKey;
    }

    this.state = {
      value,
    };

    this.bound = null;

    this.isTracking = false;
    this.isDragging = false;
    this.currentScrollTop = 0;
    this.touchPath = []; // use to calculate move velocity

    ['onChange', 'swipeStart', 'swipeMove', 'swipeEnd'].forEach((m) => {
      this[m] = this[m].bind(this);
    });
  }

  getChildContext() {
    return {
      __column__: true,
      selectedValue: this.state.value,
    };
  }

  componentWillReceiveProps(nextProps, nextContext) {
    if (nextContext.__picker__) {
      this.setState({
        value: nextContext.selectedValue[nextProps.index],
      });
    } else if ('value' in nextProps) {
      this.setState({
        value: nextProps.value,
      });
    }
  }

  componentWillUpdate(nextProps, nextState) {
    if (
      nextProps.dataSource !== this.props.dataSource ||
      JSON.stringify(nextProps.dataSource) !==
      JSON.stringify(this.props.dataSource)
    ) {
      this.changed = true;
    }
  }

  componentDidMount() {
    this.componentDidUpdate();
  }

  componentDidUpdate() {
    // calculate scroll content bound
    // inMatrix arg for config platform
    if (!this.bound || this.changed) {
      this.calculateBound();
    }

    // set scroll offset according to default value
    this.setScroll(this.state.value);

    this.changed = false;
  }

  calculateBound(props) {
    props = props || this.props;

    const { indicator } = this.refs;
    const { children, dataSource } = props;

    let top; let bottom; let itemHeight; let
      length;
    const halfOfVisibleCount = Math.floor(VISIBLE_ITEM_COUNT / 2);
    itemHeight = getComputedStyleHeight(indicator);

    length = dataSource.length;

    bottom = itemHeight * halfOfVisibleCount;
    top = -itemHeight * (length - (VISIBLE_ITEM_COUNT - halfOfVisibleCount));

    this.bound = {
      top,
      bottom,
      itemHeight,
    };
  }

  setScroll(value) {
    const { itemHeight, bottom } = this.bound;
    const { valueMap, dataSource, children, keyMap } = this.props;

    let index = 0;


    let _children;


    let item;
    if (typeof value === 'undefined') {
      value = dataSource[0].key;
    }

    for (let i = 0, l = dataSource.length; i < l; i++) {
      if (keyMap(dataSource[i]).toString() === value.toString()) {
        index = i;
        item = dataSource[i];
        break;
      }
    }

    if (this.context.__picker__) {
      this.context.onUpdate(item, this.props.index);
    }

    this.updateOffset(bottom - itemHeight * index);
  }

  startAnimate(stepFunc, callback, duration, easeFunc) {
    this.stopAnimate();
    return (this.animateId = Animate.start(
      stepFunc,
      callback,
      duration,
      easeFunc
    ));
  }

  stopAnimate() {
    if (this.animateId) {
      Animate.stop(this.animateId);
      this.animateId = null;
    }
  }

  isInBound(scrollTop) {
    const { top, bottom } = this.bound;

    if (scrollTop === undefined) {
      scrollTop = this.currentScrollTop;
    }

    return !(scrollTop > bottom || scrollTop < top);
  }

  getBouncedDiff(scrollTop) {
    const { top, bottom } = this.bound;

    if (!this.isInBound(scrollTop)) {
      if (scrollTop > bottom) {
        return scrollTop - bottom;
      }
      if (scrollTop < top) {
        return scrollTop - top;
      }
    }

    return 0;
  }

  setTransformStyle(offset) {
    const { content } = this.refs;

    if (content) {
      const style = `translate(0, ${offset}px)`;
      // debugger; //bug fix: avoid translate3d, bug in android 4.3
      content.style.WebkitTransform = style;
      content.style.transform = style;
    }
  }

  setOffset(top) {
    this.currentScrollTop = top;
    this.setTransformStyle(top);
  }

  updateOffset(top, animation, callback, easeFunc) {
    let lastScrollTop; let
      diff;

    if (animation) {
      (lastScrollTop = this.currentScrollTop), (diff = top - lastScrollTop);
      this.startAnimate(
        (percent) => {
          this.setOffset(lastScrollTop + diff * percent);
        },
        () => {
          this.setOffset(top);
          callback && callback();
        },
        200,
        easeFunc
      );
    } else {
      this.setOffset(top);
      callback && callback();
    }
  }

  restrictScrollBound(scrollTop) {
    const { top, bottom } = this.bound;
    const { max, min } = Math;

    return min(max(top, scrollTop), bottom);
  }

  // when touch move end or animation end,
  // adjust scrollTop value to stop by nearest picker item
  stopBy() {
    const { abs } = Math;
    // debugger;
    const current = this.currentScrollTop;


    const itemHeight = this.bound.itemHeight;


    const remainder = current % itemHeight;


    const multiple = parseInt(current / itemHeight, 10);


    let stopBy = current;
    if (abs(remainder) >= itemHeight / 2) {
      // reserve the sign that represent scroll direction
      stopBy = remainder / abs(remainder) * (abs(multiple) + 1) * itemHeight;
    } else {
      stopBy = multiple * itemHeight;
    }
    stopBy = this.restrictScrollBound(stopBy);

    this.updateOffset(
      stopBy,
      false,
      () => {
        this.onChange();
      },
      easeInOutQuad
    );
  }

  swipeStart(e) {
    e.preventDefault();

    const pageY = e.touches ? e.touches[0].pageY : e.clientY;

    this.isTracking = true;
    this.isDragging = false;
    this.touchPageY = pageY;
    this.lastScrollTop = this.currentScrollTop;

    this.touchPath = [
      {
        time: Date.now(),
        pageY,
      },
    ];

    this.stopAnimate();
  }

  swipeMove(e) {
    e.preventDefault();

    if (!this.isTracking) {
      return;
    }

    this.isDragging = true;

    const { abs, max, min } = Math;
    const initPageY = this.touchPageY;
    const currentPageY = e.touches ? e.touches[0].pageY : e.clientY;

    let diff = currentPageY - initPageY;
    let currentScrollTop = this.lastScrollTop + diff;

    // bounce
    if (!this.isInBound(currentScrollTop)) {
      let bound; let
        bounce;

      // bounced diff
      diff = this.getBouncedDiff(currentScrollTop);
      bound = currentScrollTop - diff;

      // apply bounce resistance
      diff = min(
        max(-BOUNCE_RESISTANCE_DISTANCE, diff),
        BOUNCE_RESISTANCE_DISTANCE
      );
      bounce = diff * (1 - abs(diff) / (BOUNCE_RESISTANCE_DISTANCE * 2));

      currentScrollTop = bound + bounce;
    }

    this.updateOffset(currentScrollTop);

    // record move path
    this.touchPath.push({
      time: Date.now(),
      pageY: currentPageY,
    });

    // restrict length
    if (this.touchPath.length > MAX_TOUCH_PATH_LENGTH) {
      this.touchPath = this.touchPath.slice(MAX_TOUCH_PATH_LENGTH / 2);
    }
  }

  swipeEnd(e) {
    e.preventDefault();

    // Ignore event when tracking is not enabled (event might be outside of element)
    if (!this.isDragging) {
      return;
    }

    const { abs } = Math;

    // velocity calculation
    const lastTouchTime = Date.now();
    const startPos = this.touchPath.length - 1;

    let endPos = startPos;
    let timeDiff = 0;


    let velocity = 0;

    for (
      let i = startPos - 1;
      i >= 0 && timeDiff < VELOCITY_DETERMINE_PERIOD;
      i--
    ) {
      const position = this.touchPath[i];
      timeDiff = lastTouchTime - position.time;
      endPos = i;
    }

    if (startPos !== endPos) {
      const firstPageY = this.touchPath[endPos].pageY;
      const lastPageY = this.touchPath[startPos].pageY;

      velocity = (lastPageY - firstPageY) / timeDiff * MILLISECONDS;
    }

    if (abs(velocity) > MIN_VELOCITY) {
      const animateId = this.startAnimate((percent, timeDiff, timeFrame) => {
        const currentScrollTop = this.currentScrollTop;

        velocity *= VELOCITY_DECREASE_RATE;

        // bounce
        if (!this.isInBound(currentScrollTop)) {
          velocity *= BOUNCE_RESISTANCE;
        }

        const diff = velocity * timeFrame / MILLISECONDS;

        this.setOffset(currentScrollTop + diff);
        if (abs(velocity) < MIN_VELOCITY) {
          Animate.stop(animateId);
          this.stopBy();
        }
      });
    } else {
      this.stopBy();
    }

    this.isTracking = false;
    this.isDragging = false;
  }

  setValue(value) {
    if (!('value' in this.props)) {
      this.setState({
        value,
      });
    }
  }

  getValue() {
    return this.state.value;
  }

  onChange(e) {
    const scrollTop = this.currentScrollTop;
    const { bottom, itemHeight } = this.bound;
    const itemIndex = Math.round((bottom - scrollTop) / itemHeight);
    const { index, children, dataSource, valueMap, keyMap } = this.props;

    let value; let
      item;

    if (children) {
      item = this.props.children[itemIndex];
      value = item.props.value;
    } else {
      item = dataSource[itemIndex];
      value = keyMap(item);
    }
    if (value === this.state.value) {
      return;
    }

    if (this.context.__picker__) {
      this.context.onChange(value, item, index);
    } else {
      this.setValue(value);
      this.props.onChange(value, item);
    }
  }

  render() {
    const { value } = this.state;
    const styles = this.props.themeStyle;
    const {
      dataSource,
      labelMap,
      valueMap,
      className,
      style = {},
      prefix = this.defaultPrefix,
    } = this.props;

    let { children } = this.props;

    if (!children) {
      children = dataSource.map((item, index) => (
        <Item key={keyMap(item)} value={valueMap(item)}>
          {valueMap(item)}
        </Item>
      ));
    }

    return (
      <div
        x="column-item"
        style={[styles['picker-column'], style]}
        onMouseDown={this.swipeStart}
        onMouseMove={this.swipeMove}
        onMouseUp={this.swipeEnd}
        onMouseLeave={this.swipeEnd}
        onTouchStart={this.swipeStart}
        onTouchMove={this.swipeMove}
        onTouchEnd={this.swipeEnd}
        onTouchCancel={this.swipeEnd}
      >
        <div
          ref="content"
          x="column-item-scroll"
          style={styles['picker-column-scroll']}
        >
          {children}
        </div>
        <div x="column-item-mask" style={styles['picker-column-mask']} />
        <div
          x="column-item-indicator"
          ref="indicator"
          style={styles['picker-column-indicator']}
        />
      </div>
    );
  }
}
PickerColumn.displayName = 'Picker';

const StyledPickerColumn = connectStyle(stylesProvider)(PickerColumn);

export default StyledPickerColumn;
// for label map
export function labelMap(item) {
  if (typeof item === 'object') {
    return item.label;
  }

  return item;
}

// for value map
export function valueMap(item) {
  if (typeof item === 'object') {
    return item.value;
  }

  return item;
}
// for key map
export function keyMap(item) {
  if (typeof item === 'object') {
    return item.key;
  }
  return item;
}
