<template>
  <schedule-wrapper
    class="c-schedule-calendar"
    v-bind="scheduleOptions"
    :dirty="dirty"
    @add="addProgram"
    @submit="submit"
    @save="onSave"
    @click.stop
  >
    <template #header>
      <button
        v-if="editable"
        class="l-flex__none c-sibling-item o-button"
        @click="today"
      >
        本月
      </button>
      <i
        class="c-schedule-calendar__btn el-icon-arrow-left c-sibling-item u-pointer"
        :class="{ show: canToPresent }"
        @click="toPresent"
      />
      <span class="c-schedule-calendar__time u-readonly">{{ date }}</span>
      <i
        class="c-schedule-calendar__btn el-icon-arrow-right u-pointer"
        :class="{ show: canToNext }"
        @click="toNext"
      />
    </template>
    <div class="l-flex__auto l-flex--col c-schedule-calendar__main">
      <div class="l-flex__none l-flex c-schedule-calendar__header">
        <div class="l-flex__auto u-readonly">周日</div>
        <div class="l-flex__auto u-readonly">周一</div>
        <div class="l-flex__auto u-readonly">周二</div>
        <div class="l-flex__auto u-readonly">周三</div>
        <div class="l-flex__auto u-readonly">周四</div>
        <div class="l-flex__auto u-readonly">周五</div>
        <div class="l-flex__auto u-readonly">周六</div>
      </div>
      <div class="c-schedule-calendar__content">
        <div
          v-for="(week, weekIndex) in weeks"
          :key="weekIndex"
          class="l-flex c-schedule-calendar__row"
        >
          <div
            v-for="(day, dayIndex) in week"
            :key="dayIndex"
            class="c-schedule-calendar__col day c-day-card"
            :class="{ 'enabled': day.editable, 'disabled': day.disabled, 'invalid': day.invalid }"
            @dblclick="addProgramByDay(day)"
          >
            <span class="c-day-card__day">{{ day.value }}</span>
            <div
              v-if="day.programs.length > 0"
              class="l-flex--row"
            >
              <pop-detail
                class="l-flex__auto"
                :program="day.programs[0]"
              >
                <program-item
                  class="c-day-card__program"
                  :program="day.programs[0]"
                  :editable="editable"
                  @edit="editProgram"
                  @remove="removeProgram"
                />
              </pop-detail>
            </div>
            <pop-list
              v-if="day.programs.length > 1"
              class="l-flex__auto"
              :programs="day.programs"
              :editable="editable"
              @edit="editProgram"
              @remove="removeProgram"
            >
              <div class="c-day-card__count u-ellipsis u-pointer">
                +{{ day.programs.length - 1 }}
              </div>
            </pop-list>
          </div>
        </div>
      </div>
    </div>
    <el-dialog
      title="排期设置"
      :visible.sync="editing"
      custom-class="c-dialog"
      :close-on-click-modal="false"
      :before-close="handleCloseEditDialog"
    >
      <schedule-edit
        v-if="program"
        ref="editor"
        :program="program"
        :ratio="ratio"
        :min-date="min"
        :max-date="max"
      />
      <template #footer>
        <button
          class="o-button"
          @click="saveProgram"
        >
          确定
        </button>
        <button
          class="o-button cancel"
          @click="handleCloseEditDialog"
        >
          取消
        </button>
      </template>
    </el-dialog>
    <el-dialog
      title="冲突提醒"
      :visible.sync="conflicting"
      custom-class="c-dialog"
      :close-on-click-modal="false"
      append-to-body
    >
      <div class="has-bottom-padding u-bold">
        <div>局部覆盖：被冲突的节目的时间段将被切割</div>
        <div>覆盖：被冲突的节目将被移除</div>
      </div>
      <div
        v-for="conflict in conflicts"
        :key="conflict.key"
      >
        与 {{ conflict.target.name }} 于 {{ conflict.start }} ~ {{ conflict.end }} 冲突
      </div>
      <template #footer>
        <button
          class="o-button"
          @click="_coverConflicts"
        >
          局部覆盖
        </button>
        <button
          class="o-button"
          @click="_coverConflictsFull"
        >
          覆盖
        </button>
        <button
          class="o-button cancel"
          @click="conflicting = false"
        >
          取消
        </button>
      </template>
    </el-dialog>
  </schedule-wrapper>
</template>

<script>
import { parseTime } from '@/utils'
import scheduleMixin from '../mixins/schedule'
import ScheduleWrapper from '../components/ScheduleWrapper'
import ScheduleEdit from './ScheduleEdit'
import ProgramItem from './ProgramItem'
import PopDetail from './PopDetail'
import PopList from './PopList'

export default {
  name: 'ScheduleCalendar',
  components: {
    ScheduleWrapper,
    ScheduleEdit,
    ProgramItem,
    PopDetail,
    PopList
  },
  mixins: [scheduleMixin],
  data () {
    return {
      weeks: [],
      min: null,
      max: null,
      current: null,
      editing: false,
      program: null,
      dirty: false,
      conflicting: false,
      conflicts: []
    }
  },
  computed: {
    date () {
      return this.current ? `${this.current.year}.${this.current.month + 1}` : ''
    },
    canToPresent () {
      return this.current ? this._compare(this.current, this.min, false) > 0 : false
    },
    canToNext () {
      return this.current && this.max ? this._compare(this.current, this.max, false) < 0 : !!this.current
    }
  },
  methods: {
    init () {
      const now = this._transform(Date.now())
      let current = {
        year: now.year,
        month: now.month
      }
      if (this.editable) {
        this._setMinDate()
        if (this.programs.length) {
          const minProgramDate = this._transform(this.programs[0].startDateTime)
          if (this._compare(now, minProgramDate, false) < 0) {
            current = {
              year: minProgramDate.year,
              month: minProgramDate.month
            }
          }
        }
      } else {
        if (this.programs.length) {
          this.min = this._transform(this.programs[0].startDateTime)
          this.max = this.programs.reduce((curr, { endDateTime }) => {
            const date = this._transform(endDateTime)
            if (curr) {
              return this._compare(curr, date) < 0 ? date : curr
            }
            return date
          }, null)
        } else {
          this.max = this.min = now
        }
        if (this._compare(now, this.max, false) > 0) {
          current = {
            year: this.min.year,
            month: this.min.month
          }
        }
      }
      this.current = current
      this._calculate()
    },
    fix () {
      let endTime = Date.now() + 60000
      if (this.programs.some(({ endDateTime }) => new Date(endDateTime).getTime() <= endTime)) {
        return this.$confirm(
          '存在过期或将要过期(<=60s)节目，是否移除？',
          '提示',
          {
            type: 'warning',
            distinguishCancelAndClose: true,
            confirmButtonText: '移除',
            cancelButtonText: '保留'
          }
        ).then(() => {
          endTime = Date.now() + 60000
          this.scheduleOptions.programs = this.programs.filter(({ endDateTime }) => new Date(endDateTime).getTime() > endTime)
          if (this._compare(this.current, this._transform(endTime), false) <= 0) {
            this._calculate()
          }
          return this.programs
        }, action => {
          if (action === 'cancel') {
            return this.programs
          }
          return Promise.reject()
        })
      }
      return Promise.resolve(this.programs)
    },
    onSave () {
      this.fix().then(programs => {
        this.save(programs.map(({ id, name, startDateTime, endDateTime }) => {
          endDateTime = this._offsetDateTime(endDateTime, 1)
          return {
            programId: id,
            programName: name,
            startDateTime, endDateTime
          }
        })).then(() => {
          this.dirty = false
        })
      })
    },
    _transformPrograms (programs) {
      return programs.sort(this._sortPrograms).map(this._transformProgram)
    },
    _sortPrograms (a, b) {
      return new Date(a.startDateTime).getTime() - new Date(b.startDateTime).getTime()
    },
    _transformProgram ({ programId, programName, startDateTime, endDateTime }) {
      // 服务器保存的时间采用左闭右开原则
      return this._createProgram(programId, programName, startDateTime, this._offsetDateTime(endDateTime, -1))
    },
    _createProgram (id, name, startDateTime, endDateTime) {
      return {
        id, name, startDateTime, endDateTime,
        key: Math.random().toString(16).slice(2),
        days: this._getDays(startDateTime, endDateTime)
      }
    },
    _offsetDateTime (dateTime, val) {
      return parseTime(new Date(dateTime).getTime() + val * 1000, '{y}-{m}-{d} {h}:{i}:{s}')
    },
    _getDays (startDateTime, endDateTime) {
      const days = {}
      const { year: endYear, month: endMonth, day: endDay } = this._transform(endDateTime)
      const end = `${endYear}-${endMonth + 1}-${endDay}`
      let { year, month, day } = this._transform(startDateTime)
      let start = `${year}-${month + 1}-${day}`
      while (start !== end) {
        days[start] = 1
        ;({ year, month, day } = this._transform(new Date(year, month, day + 1)))
        start = `${year}-${month + 1}-${day}`
      }
      days[start] = 1
      return days
    },
    removeProgram ({ key, name }) {
      this.$confirm(
        `确定移除节目 ${name}`,
        '提示',
        {
          type: 'warning',
          confirmButtonText: '确定',
          cancelButtonText: '取消'
        }
      ).then(() => {
        this.dirty = true
        this._removeProgram(key)
        this._calculate()
      })
    },
    _removeProgram (key) {
      if (!key) {
        return
      }
      const index = this.programs.findIndex(program => program.key === key)
      this.programs.splice(index, 1)
      if (index === 0) {
        this._setMinDate()
        if (this._compare(this.current, this.min, false) < 0) {
          this.current = {
            year: this.min.year,
            month: this.min.month
          }
        }
      }
    },
    _setMinDate () {
      let minDate = this._transform(Date.now())
      if (this.programs.length) {
        const minProgramDate = this._transform(this.programs[0].startDateTime)
        if (this._compare(minDate, minProgramDate, false) > 0) {
          minDate = minProgramDate
        }
      }
      if (!this.min || this._compare(this.min, minDate) < 0) {
        this.min = minDate
      }
    },
    addProgram () {
      this.editProgram({})
    },
    addProgramByDay (day) {
      if (day.editable) {
        const { year, month } = this.current
        const date = day.value
        this.editProgram({
          startDateTime: `${year}-${(month + 1).toString().padStart(2, '0')}-${date} ${this._compare({ year, month, day: date }, this._transform(Date.now())) === 0 ? parseTime(new Date(), '{h}:{i}:{s}') : '00:00:00'}`,
          endDateTime: `${year}-${(month + 1).toString().padStart(2, '0')}-${date} 23:59:59`
        })
      }
    },
    editProgram (program) {
      if (this.editable) {
        this.program = program
        this.editing = true
      }
    },
    saveProgram () {
      const { id, name, startDateTime, endDateTime } = this.$refs.editor.getValue()
      if (!startDateTime || !endDateTime) {
        this.$message({
          type: 'warning',
          message: '请选择生效时间'
        })
        return
      }
      if (!id) {
        this.$message({
          type: 'warning',
          message: '请选择节目'
        })
        return
      }
      if (this.program.id &&
        id === this.program.id &&
        startDateTime === this.program.startDateTime &&
        endDateTime === this.program.endDateTime
      ) {
        this.handleCloseEditDialog()
        return
      }
      this._program = { id, name, startDateTime, endDateTime }
      if (this._checkConflict(this._program, this.program.key)) {
        this._saveProgram()
      }
    },
    _saveProgram () {
      this._removeProgram(this.program.key)
      this._mergeOrAdd()
      this.dirty = true
      this._calculate()
      this.handleCloseEditDialog()
    },
    _mergeOrAdd () {
      let program = this._program
      const timestamp = new Date(program.startDateTime).getTime()
      const insertIndex = this.programs.findIndex(item => new Date(item.startDateTime).getTime() > timestamp)
      let pre = null
      let next = null
      let add = true
      if (~insertIndex) {
        pre = this.programs[insertIndex - 1]
        next = this.programs[insertIndex]
      } else {
        pre = this.programs[this.programs.length - 1]
      }
      if (next && program.id === next.id && new Date(program.endDateTime).getTime() + 1000 === new Date(next.startDateTime).getTime()) {
        next.startDateTime = program.startDateTime
        program = next
        add = false
      }
      if (pre && program.id === pre.id && new Date(pre.endDateTime).getTime() + 1000 === new Date(program.startDateTime).getTime()) {
        pre.endDateTime = program.endDateTime
        this._removeProgram(program.key)
        program = pre
        add = false
      }
      program.key = Math.random().toString(16).slice(2)
      program.days = this._getDays(program.startDateTime, program.endDateTime)
      if (add) {
        if (next) {
          this.programs.splice(insertIndex, 0, program)
        } else {
          this.programs.push(program)
        }
      }
    },
    _checkConflict (program, key) {
      if (this.programs.length) {
        const cstartDateTime = new Date(program.startDateTime)
        const cendDateTime = new Date(program.endDateTime)
        this.conflicts = this.programs.filter(item => {
          const startDateTime = new Date(item.startDateTime)
          const endDateTime = new Date(item.endDateTime)
          return item.key !== key && !(cstartDateTime > endDateTime || cendDateTime < startDateTime)
        }).map(item => {
          const startDateTime = new Date(item.startDateTime)
          const endDateTime = new Date(item.endDateTime)
          return {
            target: item,
            key: item.key,
            start: cstartDateTime > startDateTime ? program.startDateTime : item.startDateTime,
            end: cendDateTime > endDateTime ? item.endDateTime : program.endDateTime
          }
        })
        if (this.conflicts.length) {
          this.conflicting = true
          return false
        }
      }
      return true
    },
    _coverConflicts () {
      const now = Date.now()
      this.conflicts.forEach(({ target, key, start, end }) => {
        const { startDateTime, endDateTime } = target
        if (new Date(endDateTime).getTime() <= now || startDateTime === start && endDateTime === end) {
          this._removeProgram(key)
          return
        }
        if (startDateTime === start) {
          target.startDateTime = this._offsetDateTime(end, 1)
        } else {
          if (endDateTime !== end) {
            const index = this.programs.findIndex(({ key }) => key === target.key)
            this.programs.splice(index + 1, 0, this._createProgram(target.id, target.name, this._offsetDateTime(end, 1), target.endDateTime))
          }
          target.endDateTime = this._offsetDateTime(start, -1)
        }
        target.days = this._getDays(target.startDateTime, target.endDateTime)
      })
      this.conflicting = false
      this._saveProgram()
    },
    _coverConflictsFull () {
      this.conflicts.forEach(({ key }) => this._removeProgram(key))
      this.conflicting = false
      this._saveProgram()
    },
    handleCloseEditDialog () {
      this.program = null
      this.editing = false
    },
    today () {
      const { year, month } = this._transform(Date.now())
      this.current = { year, month }
      this._calculate()
    },
    _transform (timestamp) {
      const date = new Date(timestamp)
      return {
        year: date.getFullYear(),
        month: date.getMonth(),
        day: date.getDate()
      }
    },
    _compare (a, b, full = true) {
      if (a.year === b.year) {
        if (full && a.month === b.month) {
          return a.day - b.day
        }
        return a.month - b.month
      }
      return a.year - b.year
    },
    _createItem (date, disabled) {
      const dateObj = this._transform(date)
      const invalid = this._compare(dateObj, this._transform(Date.now())) < 0
      const day = `${dateObj.year}-${dateObj.month + 1}-${dateObj.day}`
      return {
        value: dateObj.day,
        editable: this.editable && !disabled && !invalid,
        invalid,
        disabled,
        programs: this.programs.length ? this.programs.filter(program => {
          return program.days[day]
        }) : []
      }
    },
    _calculate () {
      const { year, month } = this.current
      const monthDays = this._getPresentMonthDays(year, month).concat(this._currentMonthDays(year, month))
      const diff = 42 - monthDays.length
      const dates = diff === 0 ? monthDays : monthDays.concat(this._getNextMonthDays(year, month, diff))
      const weeks = []
      let row
      for (let i = 0; i < dates.length; i++) {
        if (i % 7 === 0) {
          row = weeks.length
          weeks[row] = []
        }
        weeks[row][i % 7] = dates[i]
      }
      this.weeks = weeks
    },
    _currentMonthDays (year, month) {
      const dateArr = []
      const lastDay = new Date(year, month + 1, 0).getDate()

      for (let i = 1; i <= lastDay; i++) {
        dateArr.push(this._createItem(new Date(year, month, i)))
      }
      return dateArr
    },
    _getPresentMonthDays (year, month) {
      const dateArr = []
      const week = new Date(year, month, 1).getDay()
      const fill = week === 0 ? 7 : week

      for (let i = fill; i > 0; i--) {
        dateArr.push(this._createItem(new Date(year, month, 1 - i), true))
      }

      return dateArr
    },
    _getNextMonthDays (year, month, count) {
      const dateArr = []

      for (let i = 0; i < count; i++) {
        dateArr.push(this._createItem(new Date(year, month + 1, 1 + i), true))
      }

      return dateArr
    },
    _offsetMonth (offset) {
      if (offset < 0) {
        if (this.current.month === 0) {
          this.current.year -= 1
          this.current.month = 11
        } else {
          this.current.month -= 1
        }
      } else if (offset > 0) {
        if (this.current.month === 11) {
          this.current.year += 1
          this.current.month = 0
        } else {
          this.current.month += 1
        }
      }
      this._calculate()
    },
    toPresent () {
      if (this.canToPresent) {
        this._offsetMonth(-1)
      }
    },
    toNext () {
      if (this.canToNext) {
        this._offsetMonth(1)
      }
    }
  }
}
</script>

<style lang="scss" scoped>
.c-schedule-calendar {
  min-height: 200px;

  &__tip {
    color: $black;
    line-height: 1;
  }

  &__time {
    width: 100px;
    color: $black;
    font-size: 24px;
    font-weight: bold;
    text-align: center;
  }

  &__btn {
    color: $blue;
    font-size: 18px;
    font-weight: bold;
    visibility: hidden;

    &.show {
      visibility: visible;
    }
  }

  &__main {
    color: $black;
    font-size: 16px;
  }

  &__content {
    min-height: 0;
    border: 1px solid $gray--light;
    overflow-y: auto;
  }

  &__row + &__row {
    border-top: 1px solid $gray--light;
  }

  &__col {
    flex: 1 1 0;
    display: flex;
    min-width: 0;
    height: 108px;
    user-select: none;

    &.disabled {
      color: $gray;
    }

    &.invalid {
      background-color: lighten($gray--light, 4%);
      cursor: not-allowed;
    }

    &.day.enabled {
      cursor: pointer;

      &:hover {
        transition: background-color 0.4s;
        background-color: #f2f8fe;
      }
    }

    & + & {
      border-left: 1px solid $gray--light;
    }
  }

  &__header {
    height: 40px;
    font-weight: bold;
    text-align: center;
  }
}

.c-day-card {
  display: inline-flex;
  flex-direction: column;
  padding: 10px 10px 0;
  font-size: 14px;
  line-height: 1;
  text-align: left;

  &__day {
    align-self: flex-end;
    font-size: 16px;
  }

  &__program {
    max-width: 100%;
    margin-top: 8px;
  }

  &__count {
    align-self: flex-start;
    display: inline-block;
    padding: 6px 10px;
    margin-top: 10px;
    color: #fff;
    font-weight: bold;
    border-radius: 4px;
    background-color: #fb8885;
  }
}
</style>
