import React from 'react'
import PropTypes from 'prop-types'
import moment from 'moment'
// update to @material-ui/pickers v4 when they add range support:
// https://github.com/mui-org/material-ui-pickers/issues/364#issuecomment-575697596
import { MuiPickersUtilsProvider, DatePicker } from '@material-ui/pickers'
import MomentUtils from '@date-io/moment'

import Button from '@material-ui/core/Button'
import Card from '@material-ui/core/Card'
import Tabs from '@material-ui/core/Tabs'
import Tab from '@material-ui/core/Tab'
import Box from '@material-ui/core/Box'
import Grid from '@material-ui/core/Grid'
import Typography from '@material-ui/core/Typography'
import IconButton from '@material-ui/core/IconButton'
import CloseIcon from '@material-ui/icons/Close'
import SyncIcon from '@material-ui/icons/Sync'

import en from 'locales/en'
import { connectToContext } from 'Provider'
import {
  Container,
  LayerTitle,
  DateContainer,
  MarkContainer,
  TimesliderBar,
  HighlightedRange,
  BarContainer,
  DateMark,
  BottomContainer,
  Tickmark,
  TooManyForPreview
} from './styled'
import { datesDiffDay, datesSameDay } from './utils'
import { DragHandle } from 'DragHandle'
import Draggable from 'react-draggable'

// const MAX_DATES = 300

function TabPanel (props) {
  const { children, value, index } = props

  return value === index && <Box p={3} style={{ padding: '10px 24px' }}>{children}</Box>
}

TabPanel.propTypes = {
  children: PropTypes.node,
  value: PropTypes.number,
  index: PropTypes.number
}

/**
 * TimeSliderBase component ui used by TimeSlider
 * @component
 * @category TimeSlider
 * @since 0.12.0
 */
class TimeSliderBase extends React.Component {
  constructor (props) {
    super(props)

    this.state = {
      dates: [],
      numOfDays: 0,
      selectedDate: null,
      selectedDateRange: [],
      leftPosition: 0,
      rangeMin: 0,
      rangeMax: 0,
      isMouseDown: false,
      firstDayOfFirstMonth: undefined,
      index: 0
    }

    // these refs are used to increase performance & calculcate offsets
    this.highlightDiv = null
    this.containerNode = null
    this.dateContainerDiv = null
    this.markContainer = null

    this.keydownHandler = e => this.cycleDates(e.key)
  }

  componentDidMount () {
    const { tabs } = this.props

    tabs.length && this.createDateTicks(tabs[this.state.index])
    // this.props.layer.on('filter:change', this.moveHandler)

    // this listener allows the user to go next/back through the data via arrow keys
    document.addEventListener('keydown', this.keydownHandler)
  }

  componentWillUnmount () {
    // this.props.layer.un('filter:change', this.moveHandler)
    document.removeEventListener('keydown', this.keydownHandler)
  }

  UNSAFE_componentWillReceiveProps (nextProps) { // eslint-disable-line camelcase
    if (nextProps.tabs.length) this.createDateTicks(nextProps.tabs[this.state.index])
  }

  createDateTicks = tab => {
    const dates = tab.dates
      .map(date => new Date(date)) /* we convert all dates to JS dates for easier use */
      .sort((a, b) => a - b) /* the sort must happen before the filter in order to remove dup dates */
      .filter((d, i, a) => datesDiffDay(a[i], a[i - 1])) /* this removes dup dates (precision is down to the day) */
    const firstDayOfFirstMonth = moment(dates[0]).startOf('month')

    this.setState({
      dates,
      tooManyDates: false,
      selectedDate: null,
      selectedDateRange: [],
      firstDayOfFirstMonth,
      numOfDays: moment(dates[dates.length - 1]).diff(moment(dates[0]), 'days', true)
    })
  }

  calculateLeftPlacement = (date, elementWidth, containerWidth, padding = 24) => {
    const { dates } = this.state

    const adjustedContainerWidth = containerWidth - padding // the useable area within the container is the container's width minus the padding
    const datesInRange = moment(dates[dates.length - 1]).diff(moment(dates[0])) // the length of time represented in the current range
    const timeToPixels = adjustedContainerWidth / datesInRange // the ratio of time to pixels
    const deltaTime = moment(date).diff(moment(dates[0])) // the length of time that has elapsed since the first date in the range
    /**
      deltaTime * timeToPixels => the number of pixels represented by the time thast has elapsed
      deltaTime * timeToPixels - (elementWidth / 2) => subtract half of the element's width to center the placed elements
      (deltaTime * timeToPixels - (elementWidth / 2)) + padding => compensate for the padding
    */
    const initialLeftPosition = (deltaTime * timeToPixels - (elementWidth / 2)) + padding
    const leftPosition = initialLeftPosition < adjustedContainerWidth // if the calculated position is greater than the total length (this typically happens with the last element in the range) we need to subtract the padding
      ? initialLeftPosition
      : initialLeftPosition - padding

    return leftPosition >= 0 ? leftPosition : 0 // safety check to make sure we don't end up with any negative values
  }

  calculateDateSliderPosition = (date, e) => {
    const { rangeMin, rangeMax, dates } = this.state
    const { leftDate, rightDate } = this.setDatesForCalendar(rangeMin, rangeMax)

    if (rangeMax - rangeMin !== 0) {
      const selectedDateRange = [leftDate, rightDate]

      this.setState({
        isMouseDown: false,
        selectedDateRange,
        selectedDate: null
      })
      this.props.onDatesChange({
        id: this.props.tabs[this.state.index].id,
        selectedDateRange
      })
    } else {
      const selectedDateRange = [dates[0], dates[dates.length - 1]]

      this.setState({ selectedDateRange })
      this.props.onDatesChange({
        id: this.props.tabs[this.state.index].id,
        selectedDateRange
      })
    }
  }

  setDatesForCalendar = (leftPosition, rightPosition) => {
    const { numOfDays, dates } = this.state
    const { width: timeSliderWidth } = this.containerNode.getBoundingClientRect()
    const leftDate = (leftPosition / (timeSliderWidth - 4)) * numOfDays
    const rightDate = (rightPosition / (timeSliderWidth + 4)) * numOfDays

    return {
      leftDate: new Date(moment(dates[0]).add(leftDate, 'days')),
      rightDate: new Date(moment(dates[0]).add(rightDate, 'days'))
    }
  }

  cycleDates = direction => {
    const { selectedDate, dates } = this.state
    const index = dates.findIndex(d => d === selectedDate)

    // if no single date is selected and you hit the right arrow key
    // set the first possible date as the selected date
    if (direction === 'ArrowRight' && selectedDate === null) return this.setSelectedDate(dates[0])

    // depending on right/left arrow we select the next/previous date
    if (direction === 'ArrowRight') {
      index + 1 === dates.length
        ? this.setSelectedDate(dates[dates.length - 1])
        : this.setSelectedDate(dates[index + 1])
    } else if (direction === 'ArrowLeft') {
      index - 1 < 0
        ? this.setSelectedDate(dates[0])
        : this.setSelectedDate(dates[index - 1])
    }
  }

  setSelectedDate = selectedDate => {
    const selectedDateRange = []

    // if a single date is selected, you cannot also have a date range selected
    this.setState({
      selectedDate,
      selectedDateRange
    })
    this.props.onDatesChange({
      id: this.props.tabs[this.state.index].id,
      selectedDate,
      selectedDateRange
    })
  }

  updateSelectedRange (first, second) {
    if (first < second) {
      this.setState({ rangeMin: first, rangeMax: second })
    } else {
      this.setState({ rangeMin: second, rangeMax: first })
    }
  }

  // this tracks if the mouse is being dragged on the slider
  handleMouseDown = e => {
    this.setState({
      isMouseDown: true,
      mouseDownEpoch: Date.now(),
      firstPosition: e.pageX - this.containerNode.getBoundingClientRect().left
    })
  }

  // we do a direct DOM access to avoid tons of setState re-renders
  handleMouseMove = e => {
    const { isMouseDown, firstPosition } = this.state

    if (isMouseDown) {
      const secondPosition = e.clientX - this.containerNode.getBoundingClientRect().left

      this.updateSelectedRange(firstPosition, secondPosition)
    }
  }

  handleMouseUp = e => {
    const { rangeMin, rangeMax, mouseDownEpoch } = this.state
    // const rightPosition = e.pageX - this.containerNode.getBoundingClientRect().left
    const { leftDate, rightDate } = this.setDatesForCalendar(rangeMin, rangeMax)

    this.calculateDateSliderPosition()

    // we manually calculate if the mouse is being clicked vs dragged
    if (Date.now() - mouseDownEpoch > 250) {
      this.setState({
        isMouseDown: false,
        selectedDate: null,
        selectedDateRange: [leftDate, rightDate]
      })
    } else {
      this.setState({ isMouseDown: false })
    }
  }

  resetState = () => {
    const selectedDate = null
    const selectedDateRange = []

    this.setState({
      selectedDate,
      selectedDateRange,
      leftPosition: 0,
      rangeMin: 0,
      rangeMax: 0
    })
    this.props.onDatesChange({
      id: this.props.tabs[this.state.index].id,
      selectedDate,
      selectedDateRange
    })
  }

  // loops through the date range and renders the dates on the top of the timeslider
  renderLabels = (dates, firstDayOfFirstMonth) => {
    const display = date => moment(date).format(`MMM 'YY`)
    const padding = 24

    // if no dates or ref is undefined, do not proceed
    if (!dates?.length || !this.dateContainerDiv) return

    const { width: containerWidth } = this.dateContainerDiv.getBoundingClientRect()
    const monthsInRange = Math.ceil(moment(dates[dates.length - 1]).diff(moment(dates[0]), 'months', true)) // the ceiling number of months in the range to avoid vals < 1
    const calcLabelWidth = (divisor) => (containerWidth - padding) / (monthsInRange / divisor) // calculate the width of the label
    const datesDiv = []

    let divisor = 1

    while (calcLabelWidth(divisor) < 50 && divisor < 10) divisor += 1 // make sure our labels are at least 50px wide but only try for 10 iterations
    const labelWidth = calcLabelWidth(divisor)

    for (let i = 0; i < monthsInRange; i += divisor) { // interate by the divisor
      const futureMonth = moment(firstDayOfFirstMonth).add(i, 'M')
      const position = i > 0 ? i / divisor : 0 // get the current date's position relative to the other date labels
      const initialLeftPosition = position * labelWidth + padding
      const leftPosition = initialLeftPosition <= containerWidth ? initialLeftPosition : initialLeftPosition - padding // calculate the position of the label with a similar algorithm to that described in calculateLeftPlacement

      datesDiv.push(<DateMark
        key={i}
        left={leftPosition}
        width={labelWidth}>{display(futureMonth)}</DateMark>)
    }

    return datesDiv
  }

  // loops through the captured data and displays a mark for each image
  renderMarks = ({ dates, tickColor }) => {
    const { selectedDate } = this.state
    const padding = 24

    if (!this.markContainer) return
    const { width: containerWidth } = this.markContainer.getBoundingClientRect()

    const ticks = dates.map((date, i) => {
      const leftPosition = this.calculateLeftPlacement(date, 4, containerWidth, padding)

      return (
        <Tickmark
          key={i}
          selected={moment(dates[i]).isSame(selectedDate)}
          style={{ left: `${leftPosition}px` }}
          tickColor={tickColor}>
        </Tickmark>
      )
    })

    return ticks
  }

  onTabClicked = (_, index) => {
    this.setState({ index })
    this.props.onTabChange(index)
  }

  render () {
    const { tabs, translations, draggable } = this.props
    const {
      tooManyDates,
      dates,
      selectedDate,
      selectedDateRange,
      firstDayOfFirstMonth,
      rangeMin,
      rangeMax,
      index
    } = this.state

    return (
      <MuiPickersUtilsProvider utils={MomentUtils}>
        <Typography component='div'>
          <Draggable
            axis='both'
            handle='.timesliderdrag'>
            <Container id='timesliderbase'>
              <Grid container justify='center'>
                <Card style={{ width: '100%', paddingTop: '4px' }}>
                  {draggable ? <DragHandle className='timesliderdrag' /> : null}
                  <Tabs
                    style={{ marginRight: '60px' }}
                    indicatorColor='primary'
                    value={index}
                    onChange={this.onTabClicked}
                    aria-label='simple tabs example'
                    variant='scrollable'
                    scrollButtons='auto'>
                    {tabs.map((tab, i) => (
                      <Tab label={`Layer ${i + 1}`} key={i} />
                    ))}
                  </Tabs>
                  {tooManyDates ? (
                    <TooManyForPreview>{translations['_ol_kit_.TimeSliderBase.tooMany']}</TooManyForPreview>
                  ) : (
                    tabs.map((tab, i) => (
                      <TabPanel value={index} index={index} key={i}>
                        <LayerTitle>{tab.title}</LayerTitle>
                        <DateContainer ref={node => { this.dateContainerDiv = node }}>
                          {this.renderLabels(dates, firstDayOfFirstMonth)}
                        </DateContainer>
                        <BarContainer
                          onMouseDown={this.handleMouseDown}
                          onMouseUp={this.handleMouseUp}
                          onMouseMove={this.handleMouseMove}
                          ref={node => { this.containerNode = node }}>
                          <TimesliderBar barPlacement={16} barHeight={2} />
                          <MarkContainer
                            ref={node => { this.markContainer = node }}>
                            {this.renderMarks(tab)}
                          </MarkContainer>
                          <HighlightedRange
                            style={{ display: rangeMin || rangeMax ? 'block' : 'none' }}
                            left={rangeMin}
                            right={rangeMax}
                            width={rangeMax - rangeMin}
                            ref={node => { this.highlightDiv = node }} />
                        </BarContainer>
                      </TabPanel>
                    ))
                  )}
                  <BottomContainer>
                    {translations['_ol_kit_.TimeSliderBase.dateRange'] || 'Date Range'}
                    <DatePicker
                      disableFuture
                      variant='inline'
                      format='DD/MM/YYYY'
                      value={selectedDateRange.length ? selectedDateRange[0] : dates[0]}
                      onChange={date => {
                        this.calculateDateSliderPosition()
                        const { width } = this.containerNode.getBoundingClientRect()

                        this.setState({
                          selectedDateRange: [date, selectedDateRange[1]],
                          rangeMin: this.calculateLeftPlacement(date, 1, width, 24)
                        })
                      }} />
                    {` ${translations['_ol_kit_.TimeSliderBase.to'] || 'To'} `}
                    <DatePicker
                      disableFuture
                      variant='inline'
                      format='DD/MM/YYYY'
                      value={selectedDateRange.length ? selectedDateRange[1] : dates[dates.length - 1]}
                      onChange={date => {
                        this.calculateDateSliderPosition()
                        const { width } = this.containerNode.getBoundingClientRect()

                        this.setState({
                          selectedDateRange: [selectedDateRange[0], date],
                          rangeMax: this.calculateLeftPlacement(date, 1, width, 24)
                        })
                      }} />

                    <Button disabled={!selectedDate} onClick={() => this.cycleDates('ArrowLeft')} variant='contained' color='primary' style={{ marginRight: '5px' }}>
                      {translations['_ol_kit_.TimeSliderBase.previous']}
                    </Button>
                    <Button disabled={datesSameDay(selectedDate, dates[dates.length - 1])} onClick={() => this.cycleDates('ArrowRight')} variant='contained' color='primary'>
                      {translations['_ol_kit_.TimeSliderBase.next']}
                    </Button>

                    <IconButton onClick={this.resetState}>
                      <SyncIcon color='primary' />
                    </IconButton>
                  </BottomContainer>
                  <IconButton onClick={this.props.onClose} style={{ position: 'absolute', top: '5px', right: '5px' }} aria-label='delete'>
                    <CloseIcon />
                  </IconButton>
                </Card>
              </Grid>
            </Container>
          </Draggable>
        </Typography>
      </MuiPickersUtilsProvider>
    )
  }
}

TimeSliderBase.defaultProps = {
  onClose: () => {},
  onDatesChange: () => {},
  onTabChange: () => {},
  translations: en,
  draggable: true
}

TimeSliderBase.propTypes = {
  /** callback fired when TimeSliderBase 'x' is clicked  */
  onClose: PropTypes.func,
  /** callback fired when date selection or range is changed */
  onDatesChange: PropTypes.func,
  /** callback fired when a new tab is clicked */
  onTabChange: PropTypes.func,
  /** separates tabs + corresponding dates into groups */
  tabs: PropTypes.arrayOf(PropTypes.shape({
    dates: PropTypes.array,
    id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
    tickColor: PropTypes.string,
    title: PropTypes.string
  })),
  /** object with key/value pairs for translated strings */
  translations: PropTypes.shape({
    '_ol_kit_.TimeSliderBase.dateRange': PropTypes.string,
    '_ol_kit_.TimeSliderBase.next': PropTypes.string,
    '_ol_kit_.TimeSliderBase.previous': PropTypes.string,
    '_ol_kit_.TimeSliderBase.selectedEndDate': PropTypes.string,
    '_ol_kit_.TimeSliderBase.selectedStartDate': PropTypes.string,
    '_ol_kit_.TimeSliderBase.to': PropTypes.string,
    '_ol_kit_.TimeSliderBase.tooMany': PropTypes.string
  }),
  /** Boolean to allow timeslider to be dragged around the screen */
  draggable: PropTypes.bool
}

export default connectToContext(TimeSliderBase)