text: import _ from 'lodash';
  import React, {Component} from 'react';
  import {
    AccessibilityInfo,
    PanResponder,
    Animated,
    View,
    Text,
    Image
  } from 'react-native';
  import PropTypes from 'prop-types';
  import XDate from 'xdate';
  import {CALENDAR_KNOB} from '../testIDs';
  
  import dateutils from '../dateutils';
  import {parseDate} from '../interface';
  import styleConstructor from './style';
  import CalendarList from '../calendar-list';
  import Calendar from '../calendar';
  import asCalendarConsumer from './asCalendarConsumer';
  import WeekCalendar from './weekCalendar';
  import Week from './week';
  
  const commons = require('./commons');
  const UPDATE_SOURCES = commons.UPDATE_SOURCES;
  const POSITIONS = {
    CLOSED: 'closed',
    OPEN: 'open'
  };
  const SPEED = 20;
  const BOUNCINESS = 6;
  const CLOSED_HEIGHT = 120; // header + 1 week
  const WEEK_HEIGHT = 46;
  const KNOB_CONTAINER_HEIGHT = 20;
  const HEADER_HEIGHT = 68;
  const DAY_NAMES_PADDING = 24;
  
  /**
   * @description: Expandable calendar component
   * @extends: CalendarList
   * @extendslink: docs/CalendarList
   * @example: https://github.com/wix/react-native-calendars/blob/master/example/src/screens/expandableCalendar.js
   */
  class ExpandableCalendar extends Component {
    static displayName = 'ExpandableCalendar';
  
    static propTypes = {
      ...CalendarList.propTypes,
      /** position of the calendar ('open' or 'closed') */
      position: PropTypes.oneOf(_.values(POSITIONS)),
      /** the initial position of the calendar ('open' or 'closed') */
      initialPosition: PropTypes.oneOf(_.values(POSITIONS)),
      /** an option to disable the pan gesture and disable the opening and closing of the calendar (initialPosition will persist)*/
      disablePan: PropTypes.bool,
      /** whether to hide the knob  */
      hideKnob: PropTypes.bool,
      /** source for the left arrow image */
      leftArrowImageSource: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.func]),
      /** source for the right arrow image */
      rightArrowImageSource: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.func]),
      /** whether to have shadow/elevation for the calendar */
      allowShadow: PropTypes.bool,
      /** whether to disable the week scroll in closed position */
      disableWeekScroll: PropTypes.bool
    }
  
    static defaultProps = {
      horizontal: true,
      initialPosition: POSITIONS.CLOSED,
      firstDay: 0,
      leftArrowImageSource: require('../calendar/img/previous.png'),
      rightArrowImageSource: require('../calendar/img/next.png'),
      allowShadow: true
    }
  
    static positions = POSITIONS;
  
    constructor(props) {
      super(props);
  
      this.style = styleConstructor(props.theme);
      this.closedHeight = CLOSED_HEIGHT + (props.hideKnob ? 0 : KNOB_CONTAINER_HEIGHT);
      this.numberOfWeeks = this.getNumberOfWeeksInMonth(XDate(this.props.context.date));
      this.openHeight = this.getOpenHeight();
  
      const startHeight = props.initialPosition === POSITIONS.CLOSED ? this.closedHeight : this.openHeight;
      this._height = startHeight;
      this._wrapperStyles = {style: {height: startHeight}};
      this._headerStyles = {style: {top: props.initialPosition === POSITIONS.CLOSED ? 0 : -HEADER_HEIGHT}};
      this._weekCalendarStyles = {style: {}};
      this.wrapper = undefined;
      this.calendar = undefined;
      this.visibleMonth = this.getMonth(this.props.context.date);
      this.initialDate = props.context.date; // should be set only once!!!
      this.headerStyleOverride = {
        'stylesheet.calendar.header': {
          week: {
            marginTop: 7,
            marginBottom: -4, // reduce space between dayNames and first line of dates
            flexDirection: 'row',
            justifyContent: 'space-around'
          }
        }
      };
  
      this.state = {
        deltaY: new Animated.Value(startHeight),
        headerDeltaY: new Animated.Value(props.initialPosition === POSITIONS.CLOSED ? 0 : -HEADER_HEIGHT),
        position: props.initialPosition,
        screenReaderEnabled: false
      };
  
      this.panResponder = PanResponder.create({
        onMoveShouldSetPanResponder: this.handleMoveShouldSetPanResponder,
        onPanResponderGrant: this.handlePanResponderGrant,
        onPanResponderMove: this.handlePanResponderMove,
        onPanResponderRelease: this.handlePanResponderEnd,
        onPanResponderTerminate: this.handlePanResponderEnd
      });
    }
  
    componentDidMount() {
      if (AccessibilityInfo) {
        if (AccessibilityInfo.isScreenReaderEnabled) {
          AccessibilityInfo.isScreenReaderEnabled().then(this.handleScreenReaderStatus);
        } else if (AccessibilityInfo.fetch) {
          // Support for older RN versions
          AccessibilityInfo.fetch().then(this.handleScreenReaderStatus);
        }
      }
    }
  
    componentDidUpdate(prevProps) {
      const {date} = this.props.context;
      if (date !== prevProps.context.date) {
        // date was changed from AgendaList, arrows or scroll
        this.scrollToDate(date);
      }
  
      const {position} = this.props;
      if (position && position !== prevProps.position) { 
        position === POSITIONS.CLOSED
          ? this.close()
          : this.open();
      }
    }
  
    handleScreenReaderStatus = (screenReaderEnabled) => {
      this.setState({screenReaderEnabled});
    };
  
    updateNativeStyles() {
      this.wrapper && this.wrapper.setNativeProps(this._wrapperStyles);
      if (!this.props.horizontal) {
        this.header && this.header.setNativeProps(this._headerStyles);
      } else {
        this.weekCalendar && this.weekCalendar.setNativeProps(this._weekCalendarStyles);
      }
    }
  
    /** Scroll */
  
    scrollToDate(date) {
      if (this.calendar) {
        if (!this.props.horizontal) {
          this.calendar.scrollToDay(XDate(date), 0, true);
        } else if (this.getMonth(date) !== this.visibleMonth) { // don't scroll if the month is already visible
          this.calendar.scrollToMonth(XDate(date));
        }
      }
    }
  
    scrollPage(next) {
      if (this.props.horizontal) {
        const d = parseDate(this.props.context.date);
  
        if (this.state.position === POSITIONS.OPEN) {
          d.setDate(1);
          d.addMonths(next ? 1 : -1);
        } else {
          const {firstDay} = this.props;
          let dayOfTheWeek = d.getDay();
          if (dayOfTheWeek < firstDay && firstDay > 0) {
            dayOfTheWeek = 7 + dayOfTheWeek;
          }
          const firstDayOfWeek = (next ? 7 : -7) - dayOfTheWeek + firstDay;
          d.addDays(firstDayOfWeek);
        }
        _.invoke(this.props.context, 'setDate', this.getDateString(d), UPDATE_SOURCES.PAGE_SCROLL);
      }
    }
  
    /** Utils */
    getOpenHeight() {
      if (!this.props.horizontal) {
        return Math.max(commons.screenHeight, commons.screenWidth);
      }
      return CLOSED_HEIGHT + (WEEK_HEIGHT * (this.numberOfWeeks - 1)) + (this.props.hideKnob ? 12 : KNOB_CONTAINER_HEIGHT);
    }
  
    getDateString(date) {
      return date.toString('yyyy-MM-dd');
    }
  
    getYear(date) {
      const d = XDate(date);
      return d.getFullYear();
    }
  
    getMonth(date) {
      const d = XDate(date);
      // getMonth() returns the month of the year (0-11). Value is zero-index, meaning Jan=0, Feb=1, Mar=2, etc.
      return d.getMonth() + 1;
    }
  
    getNumberOfWeeksInMonth(month) {
      const days = dateutils.page(month, this.props.firstDay);
      return days.length / 7;
    }
  
    getMarkedDates() {
      const {context, markedDates} = this.props;
  
      if (markedDates) {
        const marked = _.cloneDeep(markedDates);
        if (marked[context.date]) {
          marked[context.date].selected = true;
        } else {
          marked[context.date] = {selected: true};
        }
        return marked;
      }
      return {[context.date]: {selected: true}};
    }
  
    shouldHideArrows() {
      if (!this.props.horizontal) {
        return true;
      }
      return this.props.hideArrows || false;
    }
  
    isLaterDate(date1, date2) {
      if (date1.year > this.getYear(date2)) {
        return true;
      }
      if (date1.year === this.getYear(date2)) {
        if (date1.month > this.getMonth(date2)) {
          return true;
        }
      }
      return false;
    }
  
    /** Pan Gesture */
  
    handleMoveShouldSetPanResponder = (e, gestureState) => {
      if (this.props.disablePan) {
        return false;
      }
      if (!this.props.horizontal && this.state.position === POSITIONS.OPEN) {
        // disable pan detection when vertical calendar is open to allow calendar scroll
        return false;
      }
      if (this.state.position === POSITIONS.CLOSED && gestureState.dy < 0) {
        // disable pan detection to limit to closed height
        return false;
      }
      return gestureState.dy > 5 || gestureState.dy < -5;
    };
    handlePanResponderGrant = () => {
  
    };
    handlePanResponderMove = (e, gestureState) => {
      // limit min height to closed height
      this._wrapperStyles.style.height = Math.max(this.closedHeight, this._height + gestureState.dy);
  
      if (!this.props.horizontal) {
        // vertical CalenderList header
        this._headerStyles.style.top = Math.min(Math.max(-gestureState.dy, -HEADER_HEIGHT), 0);
      } else {
        // horizontal Week view
        if (this.state.position === POSITIONS.CLOSED) {
          this._weekCalendarStyles.style.opacity = Math.min(1, Math.max(1 - gestureState.dy / 100, 0));
        }
      }
  
      this.updateNativeStyles();
    };
    handlePanResponderEnd = () => {
      this._height = this._wrapperStyles.style.height;
      this.bounceToPosition();
    };
  
    /** Animated */
  
    bounceToPosition(toValue) {
      if (!this.props.disablePan) {
        const {deltaY} = this.state;
        const threshold = this.openHeight / 1.75;
  
        let isOpen = this._height >= threshold;
        const newValue = isOpen ? this.openHeight : this.closedHeight;
  
        deltaY.setValue(this._height); // set the start position for the animated value
        this._height = toValue || newValue;
        isOpen = this._height >= threshold; // re-check after this._height was set
  
        Animated.spring(deltaY, {
          toValue: this._height,
          speed: SPEED,
          bounciness: BOUNCINESS,
          useNativeDriver: false
        }).start(this.onAnimatedFinished);
  
        this.setPosition();
        this.closeHeader(isOpen);
        this.resetWeekCalendarOpacity(isOpen);
      }
    }
  
    onAnimatedFinished = ({finished}) => {
      if (finished) {
        // this.setPosition();
      }
    }
  
    setPosition() {
      const isClosed = this._height === this.closedHeight;
      this.setState({position: isClosed ? POSITIONS.CLOSED : POSITIONS.OPEN});
      if (this.props.onPositionChange) {
        this.props.onPositionChange(isClosed ? POSITIONS.CLOSED : POSITIONS.OPEN);
      }
    }
  
    resetWeekCalendarOpacity(isOpen) {
      this._weekCalendarStyles.style.opacity = isOpen ? 0 : 1;
      this.updateNativeStyles();
    }
  
    closeHeader(isOpen) {
      const {headerDeltaY} = this.state;
  
      headerDeltaY.setValue(this._headerStyles.style.top); // set the start position for the animated value
  
      if (!this.props.horizontal && !isOpen) {
        Animated.spring(headerDeltaY, {
          toValue: 0,
          speed: SPEED / 10,
          bounciness: 1,
          useNativeDriver: false
        }).start();
      }
    }
  
    /** Events */
  
    onPressArrowLeft = () => {
      this.scrollPage(false);
    }
    onPressArrowRight = () => {
      this.scrollPage(true);
    }
  
    onDayPress = (value) => { // {year: 2019, month: 4, day: 22, timestamp: 1555977600000, dateString: "2019-04-23"}
      _.invoke(this.props.context, 'setDate', value.dateString, UPDATE_SOURCES.DAY_PRESS);
  
      setTimeout(() => { // to allows setDate to be completed
        if (this.state.position === POSITIONS.OPEN) {
          this.bounceToPosition(this.closedHeight);
        }
      }, 0);
    }
  
    close() {
      this.bounceToPosition(this.closedHeight);
    }
  
    open() {
      this.bounceToPosition(this.openHeight);
    }
  
    onVisibleMonthsChange = (value) => {
      if (this.visibleMonth !== _.first(value).month) {
        this.visibleMonth = _.first(value).month; // equivalent to this.getMonth(value[0].dateString)
  
        // for horizontal scroll
        const {date, updateSource} = this.props.context;
  
        if (this.visibleMonth !== this.getMonth(date) && updateSource !== UPDATE_SOURCES.DAY_PRESS) {
          const next = this.isLaterDate(_.first(value), date);
          this.scrollPage(next);
        }
  
        // updating openHeight
        setTimeout(() => { // to wait for setDate() call in horizontal scroll (this.scrollPage())
          const numberOfWeeks = this.getNumberOfWeeksInMonth(parseDate(this.props.context.date));
          if (numberOfWeeks !== this.numberOfWeeks) {
            this.numberOfWeeks = numberOfWeeks;
            this.openHeight = this.getOpenHeight();
            if (this.state.position === POSITIONS.OPEN) {
              this.bounceToPosition(this.openHeight);
            }
          }
        }, 0);
      }
    }
  
    /** Renders */
  
    renderWeekDaysNames() {
      const weekDaysNames = dateutils.weekDayNames(this.props.firstDay);
  
      return (
        <View
          style={[
            this.style.weekDayNames,
            {
              paddingLeft: _.get(this.props, 'calendarStyle.paddingLeft') + 6 || DAY_NAMES_PADDING,
              paddingRight: _.get(this.props, 'calendarStyle.paddingRight') + 6 || DAY_NAMES_PADDING
            }
          ]}
        >
          {weekDaysNames.map((day, index) => (
            <Text allowFontScaling={false} key={day+index} style={this.style.weekday} numberOfLines={1}>{day}</Text>
          ))}
        </View>
      );
    }
  
    renderHeader() {
      const monthYear = XDate(this.props.context.date).toString('MMMM yyyy');
  
      return (
        <Animated.View
          ref={e => this.header = e}
          style={[this.style.header, {height: HEADER_HEIGHT, top: this.state.headerDeltaY}]}
          pointerEvents={'none'}
        >
          <Text allowFontScaling={false} style={this.style.headerTitle}>{monthYear}</Text>
          {this.renderWeekDaysNames()}
        </Animated.View>
      );
    }
  
    renderWeekCalendar() {
      const {position} = this.state;
      const {disableWeekScroll} = this.props;
      const WeekComponent = disableWeekScroll ? Week : WeekCalendar;
  
      return (
        <Animated.View
          ref={e => this.weekCalendar = e}
          style={{
            position: 'absolute',
            left: 0,
            right: 0,
            top: HEADER_HEIGHT + (commons.isAndroid ? 8 : 4), // align row on top of calendar's first row
            opacity: position === POSITIONS.OPEN ? 0 : 1
          }}
          pointerEvents={position === POSITIONS.CLOSED ? 'auto' : 'none'}
        >
          <WeekComponent
            {...this.props}
            current={this.props.context.date}
            onDayPress={this.onDayPress}
            markedDates={this.getMarkedDates()} // for Week component
            style={this.props.calendarStyle}
            allowShadow={false}
            hideDayNames={true}
            accessibilityElementsHidden // iOS
            importantForAccessibility={'no-hide-descendants'} // Android
          />
        </Animated.View>
      );
    }
  
    renderKnob() {
      // TODO: turn to TouchableOpacity with onPress that closes it
      return (
        <View style={this.style.knobContainer} pointerEvents={'none'} testID={`${this.props.testID}-knob`}>
          <View style={this.style.knob} testID={CALENDAR_KNOB}/>
        </View>
      );
    }
  
    renderArrow = (direction) => {
      if (_.isFunction(this.props.renderArrow)) {
        return this.props.renderArrow(direction);
      }
  
      return (
        <Image
          source={direction === 'right' ? this.props.rightArrowImageSource : this.props.leftArrowImageSource}
          style={this.style.arrowImage}
          testID={`${this.props.testID}-${direction}-arrow`}
        />
      );
    }
  
    render() {
      const {style, hideKnob, horizontal, allowShadow, theme, ...others} = this.props;
      const {deltaY, position, screenReaderEnabled} = this.state;
      const isOpen = position === POSITIONS.OPEN;
      const themeObject = Object.assign(this.headerStyleOverride, theme);
  
      return (
        <View testID={this.props.testID} style={[allowShadow && this.style.containerShadow, style]}>
          {screenReaderEnabled ?
            <Calendar
              testID="calendar"
              {...others}
              theme={themeObject}
              onDayPress={this.onDayPress}
              markedDates={this.getMarkedDates()}
              hideExtraDays
              renderArrow={this.renderArrow}
            />
            :
            <Animated.View
              ref={e => {this.wrapper = e;}}
              style={{height: deltaY}}
              {...this.panResponder.panHandlers}
            >
              <CalendarList
                testID="calendar"
                horizontal={horizontal}
                {...others}
                theme={themeObject}
                ref={r => this.calendar = r}
                current={this.initialDate}
                onDayPress={this.onDayPress}
                onVisibleMonthsChange={this.onVisibleMonthsChange}
                pagingEnabled
                scrollEnabled={isOpen}
                markedDates={this.getMarkedDates()}
                hideArrows={this.shouldHideArrows()}
                onPressArrowLeft={this.onPressArrowLeft}
                onPressArrowRight={this.onPressArrowRight}
                hideExtraDays={!horizontal}
                renderArrow={this.renderArrow}
                staticHeader
              />
              {horizontal && this.renderWeekCalendar()}
              {!hideKnob && this.renderKnob()}
              {!horizontal && this.renderHeader()}
            </Animated.View>
          }
        </View>
      );
    }
  }
  
  export default asCalendarConsumer(ExpandableCalendar);
  