import React, { Component } from "react";
import PropTypes from "prop-types";
import mergeClassNames from "merge-class-names";

import Navigation from "./Calendar/Navigation";
import CenturyView from "./CenturyView";
import DecadeView from "./DecadeView";
import YearView from "./YearView";
import MonthView from "./MonthView";

import { getBegin, getEnd, getValueRange } from "./shared/dates";
import { setLocale } from "./shared/locales";
import { isCalendarType, isClassName, isMaxDate, isMinDate, isValue } from "./shared/propTypes";
import { between, callIfDefined, mergeFunctions } from "./shared/utils";

const allViews = ["century", "decade", "year", "month"];
const allValueTypes = [...allViews.slice(1), "day"];

const datesAreDifferent = (date1, date2) => (date1 && !date2) || (!date1 && date2) || (date1 && date2 && date1.getTime() !== date2.getTime());

export default class Calendar extends Component {
  get drillDownAvailable() {
    const views = this.getLimitedViews();
    const { view } = this.state;

    return views.indexOf(view) < views.length - 1;
  }

  get drillUpAvailable() {
    const views = this.getLimitedViews();
    const { view } = this.state;

    return views.indexOf(view) > 0;
  }

  /**
   * Returns value type that can be returned with currently applied settings.
   */
  get valueType() {
    const { maxDetail } = this.props;
    return allValueTypes[allViews.indexOf(maxDetail)];
  }

  getValueArray(value) {
    if (value instanceof Array) {
      return value;
    }

    return [this.getValueFrom(value), this.getValueTo(value)];
  }

  getValueFrom = value => {
    if (!value) {
      return null;
    }

    const { maxDate, minDate } = this.props;
    const rawValueFrom = value instanceof Array && value.length === 2 ? value[0] : value;
    const valueFromDate = new Date(rawValueFrom);

    if (Number.isNaN(valueFromDate.getTime())) {
      throw new Error(`Invalid date: ${value}`);
    }

    const valueFrom = getBegin(this.valueType, valueFromDate);

    return between(valueFrom, minDate, maxDate);
  };

  getValueTo = value => {
    if (!value) {
      return null;
    }

    const { maxDate, minDate } = this.props;
    const rawValueTo = value instanceof Array && value.length === 2 ? value[1] : value;
    const valueToDate = new Date(rawValueTo);

    if (Number.isNaN(valueToDate.getTime())) {
      throw new Error(`Invalid date: ${value}`);
    }

    const valueTo = getEnd(this.valueType, valueToDate);

    return between(valueTo, minDate, maxDate);
  };

  /**
   * Returns views array with disallowed values cut off.
   */
  getLimitedViews(props = this.props) {
    const { minDetail, maxDetail } = props;

    return allViews.slice(allViews.indexOf(minDetail), allViews.indexOf(maxDetail) + 1);
  }

  /**
   * Determines whether a given view is allowed with currently applied settings.
   */
  isViewAllowed(props = this.props, view = this.state.view) {
    const views = this.getLimitedViews(props);

    return views.indexOf(view) !== -1;
  }

  /**
   * Gets current value in a desired format.
   */
  getProcessedValue(value) {
    const { returnValue } = this.props;

    switch (returnValue) {
      case "start":
        return this.getValueFrom(value);
      case "end":
        return this.getValueTo(value);
      case "range":
        return this.getValueArray(value);
      default:
        throw new Error("Invalid returnValue.");
    }
  }

  state = {
    activeStartDate: this.getActiveStartDate(),
    hover: null,
    view: this.getView(),
    value: this.props.value
  };

  componentWillMount() {
    setLocale(this.props.locale);
  }

  componentWillReceiveProps(nextProps) {
    const { locale: nextLocale, value: nextValue } = nextProps;
    const { locale } = this.props;
    const { value } = this.state;

    if (nextLocale !== locale) {
      setLocale(nextLocale);
    }

    const nextState = {};

    const allowedViewChanged = nextProps.minDetail !== this.props.minDetail || nextProps.maxDetail !== this.props.maxDetail;

    if (allowedViewChanged && !this.isViewAllowed(nextProps)) {
      nextState.view = this.getView(nextProps);
    }

    if (
      allowedViewChanged ||
      datesAreDifferent(...[nextValue, value].map(this.getValueFrom)) ||
      datesAreDifferent(...[nextValue, value].map(this.getValueTo))
    ) {
      this.updateValues(nextProps);
    } else {
      nextState.activeStartDate = this.getActiveStartDate(nextProps);
    }

    if (!nextProps.selectRange && this.props.selectRange) {
      nextState.hover = null;
    }

    this.setState(nextState);
  }

  updateValues = (props = this.props) => {
    this.setState({
      value: props.value,
      activeStartDate: this.getActiveStartDate(props)
    });
  };

  getActiveStartDate(props = this.props) {
    const rangeType = this.getView(props);
    const valueFrom = this.getValueFrom(props.value) || props.activeStartDate || new Date();
    return getBegin(rangeType, valueFrom);
  }

  getView(props = this.props) {
    const { view } = props;

    if (view && this.getLimitedViews(props).indexOf(view) !== -1) {
      return view;
    }

    return this.getLimitedViews(props).pop();
  }

  /**
   * Called when the user uses navigation buttons.
   */
  setActiveStartDate = activeStartDate => {
    this.setState({ activeStartDate }, () => {
      callIfDefined(this.props.onActiveDateChange, {
        activeStartDate,
        view: this.state.view
      });
    });
  };

  drillDown = activeStartDate => {
    if (!this.drillDownAvailable) {
      return;
    }

    const views = this.getLimitedViews();

    this.setState(
      prevState => {
        const nextView = views[views.indexOf(prevState.view) + 1];
        return {
          activeStartDate,
          view: nextView
        };
      },
      () => {
        callIfDefined(this.props.onDrillDown, {
          activeStartDate,
          view: this.state.view
        });
      }
    );
  };

  drillUp = () => {
    if (!this.drillUpAvailable) {
      return;
    }

    const views = this.getLimitedViews();

    this.setState(
      prevState => {
        const nextView = views[views.indexOf(prevState.view) - 1];
        const activeStartDate = getBegin(nextView, prevState.activeStartDate);

        return {
          activeStartDate,
          view: nextView
        };
      },
      () => {
        callIfDefined(this.props.onDrillUp, {
          activeStartDate: this.state.activeStartDate,
          view: this.state.view
        });
      }
    );
  };

  onChange = value => {
    const { onChange, selectRange } = this.props;

    let nextValue;
    let callback;
    if (selectRange) {
      const { value: previousValue } = this.state;
      // Range selection turned on
      if (
        !previousValue ||
        [].concat(previousValue).length !== 1 // 0 or 2 - either way we're starting a new array
      ) {
        // First value
        nextValue = getBegin(this.valueType, value);
      } else {
        // Second value
        nextValue = getValueRange(this.valueType, previousValue, value);
        callback = () => callIfDefined(onChange, nextValue);
      }
    } else {
      // Range selection turned off
      nextValue = this.getProcessedValue(value);
      callback = () => callIfDefined(onChange, nextValue);
    }

    this.setState({ value: nextValue }, callback);
  };

  onMouseOver = value => {
    this.setState({ hover: value });
  };

  onMouseOut = () => {
    this.setState({ hover: null });
  };

  renderContent() {
    const { calendarType, maxDate, minDate, renderChildren, tileClassName, tileContent } = this.props;
    const { activeStartDate, hover, value, view } = this.state;
    const { onMouseOver, valueType } = this;

    const commonProps = {
      activeStartDate,
      hover,
      maxDate,
      minDate,
      onMouseOver: this.props.selectRange ? onMouseOver : null,
      tileClassName,
      tileContent: tileContent || renderChildren, // For backwards compatibility
      value,
      valueType
    };

    const clickAction = this.drillDownAvailable ? this.drillDown : this.onChange;

    switch (view) {
      case "century":
        return <CenturyView onClick={mergeFunctions(clickAction, this.props.onClickDecade)} {...commonProps} />;
      case "decade":
        return <DecadeView onClick={mergeFunctions(clickAction, this.props.onClickYear)} {...commonProps} />;
      case "year":
        return <YearView formatMonth={this.props.formatMonth} onClick={mergeFunctions(clickAction, this.props.onClickMonth)} {...commonProps} />;
      case "month":
        return (
          <MonthView
            calendarType={calendarType}
            formatShortWeekday={this.props.formatShortWeekday}
            onClick={mergeFunctions(clickAction, this.props.onClickDay)}
            onClickWeekNumber={this.props.onClickWeekNumber}
            showNeighboringMonth={this.props.showNeighboringMonth}
            showWeekNumbers={this.props.showWeekNumbers}
            {...commonProps}
          />
        );
      default:
        throw new Error(`Invalid view: ${view}.`);
    }
  }

  renderNavigation() {
    const { showNavigation } = this.props;

    if (!showNavigation) {
      return null;
    }

    return (
      <Navigation
        activeRange={this.state.activeRange}
        activeStartDate={this.state.activeStartDate}
        drillUp={this.drillUp}
        formatMonthYear={this.props.formatMonthYear}
        maxDate={this.props.maxDate}
        minDate={this.props.minDate}
        next2Label={this.props.next2Label}
        nextLabel={this.props.nextLabel}
        prev2Label={this.props.prev2Label}
        prevLabel={this.props.prevLabel}
        setActiveStartDate={this.setActiveStartDate}
        view={this.state.view}
        views={this.getLimitedViews()}
      />
    );
  }

  render() {
    console.log("hello-calendar");
    const { className, selectRange } = this.props;
    const { value } = this.state;
    const { onMouseOut } = this;
    const valueArray = [].concat(value);

    return (
      <div
        className={mergeClassNames("react-calendar", selectRange && valueArray.length === 1 && "react-calendar--selectRange", className)}
        onMouseOut={selectRange ? onMouseOut : null}
        onBlur={selectRange ? onMouseOut : null}
      >
        {this.renderNavigation()}
        {this.renderContent()}
      </div>
    );
  }
}

Calendar.defaultProps = {
  maxDetail: "month",
  minDetail: "century",
  returnValue: "start",
  showNavigation: true,
  showNeighboringMonth: true,
  view: "month"
};

Calendar.propTypes = {
  activeStartDate: PropTypes.instanceOf(Date),
  calendarType: isCalendarType,
  className: isClassName,
  formatMonth: PropTypes.func,
  formatMonthYear: PropTypes.func,
  formatShortWeekday: PropTypes.func,
  locale: PropTypes.string,
  maxDate: isMaxDate,
  maxDetail: PropTypes.oneOf(allViews),
  minDate: isMinDate,
  minDetail: PropTypes.oneOf(allViews),
  next2Label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
  nextLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
  onActiveDateChange: PropTypes.func,
  onChange: PropTypes.func,
  onClickDay: PropTypes.func,
  onClickDecade: PropTypes.func,
  onClickMonth: PropTypes.func,
  onClickWeekNumber: PropTypes.func,
  onClickYear: PropTypes.func,
  onDrillDown: PropTypes.func,
  onDrillUp: PropTypes.func,
  prev2Label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
  prevLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
  renderChildren: PropTypes.func, // For backwards compatibility
  returnValue: PropTypes.oneOf(["start", "end", "range"]),
  selectRange: PropTypes.bool,
  showNavigation: PropTypes.bool,
  showNeighboringMonth: PropTypes.bool,
  showWeekNumbers: PropTypes.bool,
  tileClassName: PropTypes.oneOfType([PropTypes.func, isClassName]),
  tileContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
  value: PropTypes.oneOfType([PropTypes.string, isValue]),
  view: PropTypes.oneOf(allViews)
};
