<template>
  <div
    v-loading.lock="loading"
    element-loading-background="rgba(0, 0, 0, 0.8)"
    class="c-designer"
    @contextmenu.prevent
  >
    <audio
      ref="audio"
      :src="bgmUrl"
      :muted.prop="muted"
      autoplay
      @ended="onAudioEnded"
      @error="onAudioError"
    />
    <div class="c-designer__side c-side">
      <div class="c-side__tool">
        <div
          class="c-side__item"
          :class="{ active: tabIndex === 0 }"
          @click="tabIndex = 0"
        >
          工具栏
        </div>
        <div
          v-if="hasWidgets"
          class="c-side__item"
          :class="{ active: tabIndex === 1 }"
          @click="tabIndex = 1"
        >
          图层
        </div>
      </div>
      <div
        v-show="tabIndex === 0"
        class="c-side__content"
        @dragstart="widgetOnDragStart"
        @dragend="widgetOnDragEnd"
      >
        <div
          v-for="cfg in widgetConfigs"
          :key="cfg.key"
          :data-type="cfg.type"
          class="c-widget"
          draggable
        >
          <span class="c-widget__icon">
            <i :class="cfg.icon" />
          </span>
          <span class="c-widget__text u-ellipsis">{{ cfg.label }}</span>
        </div>
      </div>
      <div
        v-show="tabIndex === 1"
        class="c-side__content"
      >
        <div
          v-for="(layer, index) in layers"
          :key="layer.id"
          class="c-widget"
          :class="{ active: layer.id === selectedWidgetId }"
          @mousedown="onLayerClick($event, index)"
        >
          <span class="c-widget__icon">
            <i :class="layer.icon" />
          </span>
          <span
            class="c-widget__text"
            :title="layer.name"
          >
            {{ layer.name }}
          </span>
        </div>
      </div>
    </div>
    <div class="c-designer__main">
      <div class="l-flex--row c-designer__tool">
        <div
          v-if="program"
          class="l-flex--row c-designer__tip"
        >
          <span class="c-sibling-item c-designer__name u-ellipsis">{{ program.name }}</span>
          <span class="c-sibling-item c-designer__ratio">{{ program.resolutionRatio }}</span>
        </div>
        <el-tooltip
          class="c-designer__btn"
          content="保存"
          :hide-after="2000"
        >
          <i
            class="iconfont iconsave"
            @click="onSave"
          />
        </el-tooltip>
        <el-tooltip
          v-if="hasWidgets"
          class="c-designer__btn"
          content="清空"
          :hide-after="2000"
        >
          <i
            class="iconfont iconlajitong"
            @click="clear"
          />
        </el-tooltip>
        <el-tooltip
          class="c-designer__btn"
          content="缩放"
          :hide-after="2000"
        >
          <div
            class="o-scale-slider"
            @click="toScale"
          >
            <div
              class="l-flex--row o-scale-slider__wrapper"
              :class="{ expand: dragScale }"
            >
              <i
                class="el-icon-zoom-out"
                @click="scaleDown"
              />
              <el-slider
                v-model="scale"
                class="o-scale-slider__slider"
                :min="minScale"
                :max="maxScale"
                @change="toScale"
              />
              <i
                class="el-icon-zoom-in"
                @click="scaleUp"
              />
            </div>
            <svg-icon
              v-if="dragScale"
              icon-class="exit-fullscreen"
              @click.stop="hideScale"
            />
            <svg-icon
              v-else
              icon-class="fullscreen"
            />
          </div>
        </el-tooltip>
        <el-tooltip
          v-if="hasAudio"
          class="c-designer__btn"
          content="音效"
          :hide-after="2000"
        >
          <div @click="toggleMute">
            <div
              class="o-wave"
              :class="{ muted }"
            >
              <span class="o-wave__line" />
              <span class="o-wave__line" />
              <span class="o-wave__line" />
              <span class="o-wave__line" />
            </div>
          </div>
        </el-tooltip>
        <el-tooltip
          v-if="hasNext"
          class="c-designer__btn"
          content="切歌"
          :hide-after="2000"
        >
          <div @click="chooseBgm">
            <i class="o-next" />
          </div>
        </el-tooltip>
      </div>
      <div
        ref="wrapper"
        class="c-designer__content"
        @mousedown="onScreenClick"
      >
        <div
          class="c-designer__wrapper"
          :style="wrapperStyles"
        >
          <div
            ref="canvas"
            class="c-designer__canvas"
            :style="[transformStyles, styles]"
            @dragover.prevent
            @drop.prevent="widgetOnDrop"
          >
            <div
              class="c-designer__background has-bg"
              :style="backgroundStyles"
            />
            <div
              v-show="grid"
              class="c-designer__grid"
            />
            <widget
              v-for="(item, index) in widgets"
              :key="item.id"
              ref="widgets"
              :scale="100 / scale"
              :node="item"
              :root="node"
              @focus="onWidgetFocus(index)"
              @blur="onWidgetBlur"
              @menu="onWidgetMenu($event, index)"
            />
          </div>
        </div>
      </div>
    </div>
    <div class="c-designer__side large c-side">
      <div class="c-side__tool">
        <div
          v-for="(tab, index) in dynamicOptions"
          :key="index"
          class="c-side__item"
          :class="{ active: optionIndex === index }"
          @click="optionIndex = index"
        >{{ tab.label }}</div>
      </div>
      <div
        v-for="(tab, index) in dynamicOptions"
        v-show="optionIndex === index"
        :key="index"
        class="c-side__content"
        :class="{ active: optionIndex === index }"
      >
        <dynamic-item
          v-for="item in tab.list"
          :key="item.key"
          :root="node"
          :node="widget"
          :attr="item"
          @choose="onEditAssets"
        />
      </div>
    </div>
    <content-menu
      :visible.sync="visibleContentMenu"
      :style-obj="styleObj"
      @delete="deleteLayer"
      @copy="copyLayer"
      @top="topLayer"
      @low="lowLayer"
      @up="upLayer"
      @down="downLayer"
    />
    <el-dialog
      title="数据"
      :visible.sync="dialogVisibleAssets"
      custom-class="c-dialog"
      :close-on-click-modal="false"
      :before-close="onCloseAssetsDialog"
    >
      <draggable
        v-if="sources"
        v-model="sources"
        class="c-grid"
        :class="{ dragging }"
        animation="300"
        @start="onSourceDragStart"
        @end="onSourceDragEnd"
      >
        <div
          v-for="(source, index) in sources"
          :key="index"
          class="c-card u-pointer"
          @click="onView(source)"
        >
          <div class="c-card__content">
            <i
              class="c-card__icon iconfont iconlajitong"
              @mousedown.stop
              @pointerdown.stop
              @click.stop="onDelAsset(index)"
            />
            <i
              v-if="source.thumbnailUrl"
              class="o-image"
              :style="{ 'background-image': `url('${source.thumbnailUrl}')` }"
            />
            <div class="c-card__text u-ellipsis">{{ source.name }}</div>
          </div>
        </div>
      </draggable>
      <template #footer>
        <button
          class="o-button"
          @click="onAddAsset"
        >
          新增
        </button>
        <button
          class="o-button"
          @click="onSaveAssets"
        >
          确定
        </button>
        <button
          class="o-button cancel"
          @click="onCloseAssetsDialog"
        >
          取消
        </button>
      </template>
    </el-dialog>
    <el-dialog
      title="请选择"
      :visible.sync="dialogServerData"
      :custom-class="dialogServerSize"
      append-to-body
    >
      <template v-if="dialogServerData">
        <grid-table
          v-if="isImage"
          :schema="assetSchema"
        >
          <grid-table-item>
            <template v-slot="item">
              <div class="c-card u-pointer">
                <div
                  class="c-card__content"
                  @dblclick="onChoosenAsset(item)"
                >
                  <div
                    class="o-image"
                    :style="{ 'background-image': `url('${item.thumbnailUrl}')` }"
                  >
                    <span class="c-card__name u-ellipsis">{{ item.originalName }}</span>
                  </div>
                </div>
              </div>
            </template>
          </grid-table-item>
        </grid-table>
        <schema-table
          v-else
          :schema="assetSchema"
          @row-dblclick="onChoosenAsset"
        />
      </template>
    </el-dialog>
    <preview-dialog ref="previewDialog" />
  </div>
</template>

<script>
import Draggable from 'vuedraggable'
import domToImage from 'dom-to-image'
import { updateProgram } from '@/api/program'
import {
  getAssets,
  getThumbnailUrl
} from '@/api/asset'
import {
  State,
  AssetType
} from '@/constant'
import {
  widgets,
  normalize,
  getOptions,
  getIcon,
  copy,
  fix,
  getDuration,
  toJSON
} from './core/utils'
import base from './core/base'
import Widget from './core/widget/Widget.vue'
import ContentMenu from './core/components/ContentMenu.vue'
import DynamicItem from './core/components/DynamicItem.vue'

export default {
  name: 'BigScreenDesigner',
  components: {
    Widget,
    Draggable,
    ContentMenu,
    DynamicItem
  },
  mixins: [base],
  data () {
    return {
      loading: false,
      snapping: false,
      padding: 10,
      tabIndex: 0,
      optionIndex: 0,
      widgetConfigs: widgets,
      grid: false,
      visibleContentMenu: false,
      styleObj: null,
      widgetAttr: null,
      dialogVisibleAssets: false,
      sources: null,
      dragging: false,
      dragScale: false,
      dialogServerData: false,
      assetSchema: {
        list: this.getAssets,
        transform: this.transformAsset,
        cols: [
          { prop: 'file', label: '缩略图', type: 'asset' },
          { prop: 'originalName', label: '名称' },
          {
            type: 'invoke', render: [
              { label: '查看', on: this.onViewAsset }
            ]
          }
        ]
      }
    }
  },
  computed: {
    hasWidgets () {
      return this.widgets.length > 0
    },
    dynamicOptions () {
      if (this.widget) {
        return getOptions(this.widget.type)
      }
      return []
    },
    layers () {
      const layers = []
      for (let i = this.widgets.length - 1; i >= 0; i--) {
        const { id, type, layerName } = this.widgets[i]
        layers.push({
          id: id,
          icon: getIcon(type),
          name: layerName
        })
      }
      return layers
    },
    isImage () {
      return this.widgetAttr?.type === 'data' && this.widgetAttr.options?.type === AssetType.IMAGE
    },
    selectedWidgetId () {
      return this.widget.id
    },
    wrapperStyles () {
      return this.node ? {
        width: `${this.node.width * this.scale / 100}px`,
        height: `${this.node.height * this.scale / 100}px`
      } : null
    },
    dialogServerSize () {
      return `c-dialog ${this.isImage ? 'large' : 'medium'}`
    }
  },
  watch: {
    selectedWidgetIndex (val, old) {
      if (old > -1) {
        const id = this.widgets[old]?.id
        id && this.$refs.widgets[this.$refs.widgets.findIndex(w => w.node.id === id)]?.$refs.draggable.setActive(false)
      }
      if (val > -1) {
        const id = this.widgets[val]?.id
        id && this.$refs.widgets[this.$refs.widgets.findIndex(w => w.node.id === id)]?.$refs.draggable.setActive(true)
      }
      this.optionIndex = 0
    }
  },
  methods: {
    clear () {
      this.$confirm(
        '清空后所有数据将丢失，确定清空画布？',
        { type: 'warning' }
      ).then(() => {
        this.tabIndex = 0
        this.selectedWidgetIndex = -1
        const { width, height } = this.node
        this.initCanvas({ width, height })
      })
    },
    widgetOnDragStart (evt) {
      evt.dataTransfer.setData('type', evt.target.dataset.type)
      this.dragging = true
    },
    widgetOnDragEnd () {
      this.dragging = false
    },
    widgetOnDrop (evt) {
      const type = evt.dataTransfer.getData('type')
      if (type) {
        let { offsetX: left, offsetY: top } = evt
        const node = normalize({ type })
        top -= node.height / 2
        if (top < 0) {
          top = 0
        }
        left -= node.width / 2
        if (left < 0) {
          left = 0
        }
        node.top = top
        node.left = left
        this.node.widgets.push(node)
      }
    },
    onWidgetFocus (index) {
      this.selectedWidgetIndex = index
      this.grid = true
      this.visibleContentMenu = false
    },
    onWidgetBlur () {
      this.grid = false
    },
    onWidgetMenu (evt, index) {
      this.selectedWidgetIndex = index
      this.onRightClick(evt)
    },
    onLayerClick (evt, index) {
      this.selectedWidgetIndex = this.widgets.length - 1 - index
      this.onRightClick(evt)
    },
    onScreenClick () {
      this.selectedWidgetIndex = -1
    },
    onRightClick (evt) {
      const { button, clientY: top, clientX: left } = evt
      if (button !== 2) {
        return
      }
      evt.stopPropagation()
      if (top || left) {
        this.styleObj = {
          display: 'block',
          top: `${top}px`,
          left: `${left}px`
        }
      }
      this.visibleContentMenu = true
    },
    // 删除
    deleteLayer () {
      this.widgets.splice(this.selectedWidgetIndex, 1)
      this.selectedWidgetIndex = -1
    },
    // 复制
    copyLayer () {
      this.widgets.splice(this.selectedWidgetIndex + 1, 0, copy(this.widget))
    },
    // 置顶
    topLayer () {
      if (this.selectedWidgetIndex < this.widgets.length - 1) {
        this.widgets.push(this.widgets.splice(
          this.selectedWidgetIndex,
          1
        )[0])
        this.selectedWidgetIndex = this.widgets.length - 1
      }
    },
    // 置底
    lowLayer () {
      if (this.selectedWidgetIndex > 0) {
        this.widgets.unshift(this.widgets.splice(
          this.selectedWidgetIndex,
          1
        )[0])
        this.selectedWidgetIndex = 0
      }
    },
    // 上移一层
    upLayer () {
      if (this.selectedWidgetIndex < this.widgets.length - 1) {
        this.widgets[this.selectedWidgetIndex] = this.widgets.splice(
          this.selectedWidgetIndex + 1,
          1,
          this.widgets[this.selectedWidgetIndex]
        )[0]
        this.selectedWidgetIndex += 1
      }
    },
    // 下移一层
    downLayer () {
      if (this.selectedWidgetIndex > 0) {
        this.widgets[this.selectedWidgetIndex] = this.widgets.splice(
          this.selectedWidgetIndex - 1,
          1,
          this.widgets[this.selectedWidgetIndex]
        )[0]
        this.selectedWidgetIndex -= 1
      }
    },
    onEditAssets (attr) {
      this.widgetAttr = attr
      if (attr.options && attr.options.solo) {
        this.onAddAsset()
      } else {
        this.sources = this.widget[attr.key].map(
          this.isImage
            ? asset => {
              return {
                ...asset,
                thumbnailUrl: getThumbnailUrl(asset.keyName)
              }
            }
            : asset => {
              return {
                ...asset,
                thumbnailUrl: asset.thumbnail && getThumbnailUrl(asset.thumbnail)
              }
            }
        )
        this.dialogVisibleAssets = true
      }
    },
    onCloseAssetsDialog () {
      this.widgetAttr = null
      this.dialogVisibleAssets = false
    },
    changeAttr (value) {
      const { key, options } = this.widgetAttr
      this.widget[key] = value
      if (options) {
        const { callback } = options
        callback && callback.call(this.widget, this.node)
      }
    },
    onSaveAssets () {
      this.changeAttr(this.sources.map(({ name, keyName, thumbnail, duration, size, md5 }) => {
        const source = { name, keyName, size, md5 }
        if (!this.isImage) {
          if (thumbnail) {
            source.thumbnail = thumbnail
          }
          source.duration = duration
        }
        return source
      }))
      this.onCloseAssetsDialog()
    },
    onAddAsset () {
      this.dialogServerData = true
    },
    getAssets (params) {
      return getAssets({
        type: this.widgetAttr.options.type,
        status: State.RESOLVED,
        ...params
      })
    },
    transformAsset (asset) {
      if (asset.type === AssetType.IMAGE) {
        return {
          ...asset,
          thumbnailUrl: getThumbnailUrl(asset.thumbnail)
        }
      }
      return {
        ...asset,
        file: { thumbnail: asset.thumbnail }
      }
    },
    onView (keyName) {
      this.onViewAsset({ type: this.widgetAttr.options.type, keyName })
    },
    onViewAsset ({ type, keyName }) {
      this.$refs.previewDialog.show({ type, url: keyName })
    },
    onChoosenAsset ({ originalName, keyName, thumbnail, thumbnailUrl, duration, size, md5 }) {
      const source = {
        name: originalName,
        keyName,
        size,
        md5
      }
      if (this.dialogVisibleAssets) {
        if (this.isImage) {
          source.thumbnailUrl = thumbnailUrl
        } else {
          if (thumbnail) {
            source.thumbnail = thumbnail
            source.thumbnailUrl = getThumbnailUrl(thumbnail)
          }
          source.duration = duration
        }
        this.sources.unshift(source)
      } else {
        this.changeAttr([source])
      }
      this.dialogServerData = false
    },
    onDelAsset (index) {
      this.$confirm(
        '确定移除该数据？',
        { type: 'warning' }
      ).then(() => {
        this.sources.splice(index, 1)
      })
    },
    onSourceDragStart () {
      this.dragging = true
    },
    onSourceDragEnd () {
      this.dragging = false
    },
    toScale () {
      this.dragScale = true
    },
    hideScale () {
      this.dragScale = false
    },
    scaleDown () {
      this.scale = Math.max(this.scale - 10, this.minScale)
    },
    scaleUp () {
      this.scale = Math.min(this.scale + 10, this.maxScale)
    },
    onSave () {
      this.check().then(() => {
        this.selectedWidgetIndex = -1
        this.loading = true
        this.$nextTick(() => {
          this._save().finally(() => {
            this.loading = false
          })
        })
      })
    },
    check () {
      const warning = fix(this.node)
      if (warning) {
        return this.$confirm(
          `${warning}，确定保存？`,
          { type: 'warning' }
        )
      }
      return Promise.resolve()
    },
    async _save () {
      try {
        const base64 = await this.snap()
        const result = await updateProgram({
          id: this.program.id,
          duration: getDuration(this.node),
          itemJsonStr: JSON.stringify(toJSON(this.node)),
          base64
        })
        if (result) {
          window.opener?.postMessage({
            id: this.program.id,
            base64
          })
          this.$message({
            type: 'success',
            message: '保存成功'
          })
        } else {
          this.$message({
            type: 'warning',
            message: '保存失败'
          })
        }
      } catch (e) {
        console.warn(e)
        this.$message({
          type: 'warning',
          message: '保存失败'
        })
      }
    },
    snap () {
      this.snapping = true
      return domToImage.toJpeg(this.$refs.canvas, {
        filter (node) {
          const { tagName } = node
          if (tagName === 'CANVAS') {
            return /^data:.+;base64,.+/.test(node.toDataURL())
          }
          return tagName !== 'VIDEO' && tagName !== 'IFRAME'
        },
        width: this.node.width * this.scale / 100 | 0,
        height: this.node.height * this.scale / 100 | 0,
        quality: 0.1
      }).finally(() => {
        this.snapping = false
      })
    }
  }
}
</script>

<style lang="scss" scoped>
$drak: #242a30;

.c-designer {
  display: flex;
  height: 100%;

  &__side {
    flex: none;
    width: 180px;
    min-height: 0;

    &.large {
      width: 240px;
    }
  }

  &__main {
    flex: 1 0 0;
    display: flex;
    flex-direction: column;
    position: relative;
    min-width: 0;
  }

  &__content {
    flex: 1 0 0;
    position: relative;
    padding: 10px;
    overflow: auto;
    background: url("~@/assets/dot.png") repeat;
  }

  &__tool {
    flex: 0 0 40px;
    background-color: $drak;
  }

  &__tip {
    padding: 0 16px;
  }

  &__name {
    max-width: 400px;
  }

  &__name,
  &__ratio {
    color: $gray--dark;
    font-size: 16px;
    line-height: 1;
  }

  &__btn {
    display: inline-flex;
    justify-content: center;
    align-items: center;
    width: 55px;
    height: 100%;
    color: $gray;
    cursor: pointer;

    &:hover {
      background-color: #191d22;
    }

    &:active {
      background-color: darken(#191d22, 5%);
    }

    &.iconlajitong {
      font-size: 20px;
    }
  }

  &__wrapper {
    min-width: 100%;
    min-height: 100%;
  }

  &__canvas {
    position: relative;
    background-color: #000;
    overflow: visible;

    &::before {
      content: "";
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background-color: currentColor;
      z-index: -1;
    }
  }

  &__background {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: -1;
  }

  &__grid {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-size: 30px 30px, 30px 30px;
    background-image: linear-gradient(hsla(0, 0%, 100%, 0.1) 1px, transparent 0),
      linear-gradient(90deg, hsla(0, 0%, 100%, 0.1) 1px, transparent 0);
  }
}

.c-side {
  display: flex;
  flex-direction: column;
  background-color: $drak;

  &__tool {
    flex: none;
    display: flex;
  }

  &__item {
    flex: 1 0 0;
    color: #909399;
    font-size: 14px;
    text-align: center;
    line-height: 40px;
    background-color: #242f3b;
    user-select: none;
    cursor: pointer;

    &.active {
      color: #409eff;
      background-color: #31455d;
    }
  }

  &__content {
    flex: 1 0 0;
    min-height: 0;
    padding: 15px;
    overflow-y: auto;

    &::-webkit-scrollbar {
      width: 4px;
      height: 4px;
    }

    &::-webkit-scrollbar-track-piece {
      background-color: #29405c;
    }

    &::-webkit-scrollbar-track {
      box-shadow: 1px 1px 5px rgba(116, 148, 170, 0.5) inset;
    }

    &::-webkit-scrollbar-thumb {
      min-height: 20px;
      background-clip: content-box;
      box-shadow: 0 0 0 5px rgba(116, 148, 170, 0.5) inset;
    }
  }
}

.c-widget {
  display: flex;
  align-items: center;
  position: relative;
  width: 100%;
  height: 48px;
  margin-bottom: 1px;
  padding: 0 6px;
  color: #bfcbd9;
  font-size: 12px;
  user-select: none;
  cursor: pointer;

  &.active {
    background-color: #31455d;
  }

  &__icon {
    flex: none;
    display: inline-block;
    margin-right: 10px;
    width: 52px;
    height: 30px;
    color: #409eff;
    font-size: 16px;
    line-height: 30px;
    text-align: center;
    border: 1px solid #3a4659;
    background-color: #282a30;
  }

  &__text {
    flex: 1 1 auto;
    min-width: 0;
  }
}

.c-grid.dragging .c-card .c-card__icon {
  display: none;
}

.c-card {
  display: inline-block;
  position: relative;
  background-color: rgba(0, 0, 0, 0.8);

  &:hover &__icon {
    display: block;
    padding: 6px;
    color: #fff;
    border-radius: 50%;
    background-color: rgba(0, 0, 0, 0.6);
    cursor: pointer;
  }

  &.sortable-chosen &__icon {
    display: none;
  }

  &__content {
    position: relative;
    padding-top: 60%;
  }

  &__icon {
    display: none;
    position: absolute;
    top: 0;
    right: 0;
    z-index: 9;
  }

  &__name {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 4px 6px;
    color: $gray;
    font-size: 14px;
    text-align: center;
    background-color: rgba(0, 0, 0, 0.6);
  }

  &__text {
    position: absolute;
    top: 50%;
    left: 8px;
    right: 8px;
    color: #fff;
    font-size: 14px;
    text-align: center;
    transform: translateY(-50%);
  }
}

.o-image {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-position: center;
  background-size: contain;
  background-repeat: no-repeat;
}

.o-scale-slider {
  justify-content: flex-start;
  min-width: 48px;
  width: auto;
  padding: 0 $spacing;
  font-size: 20px;

  &__wrapper {
    width: 0;
    transition: width 0.1s;
    overflow: hidden;

    &.expand {
      width: 184px;
    }
  }

  &__slider {
    width: 100px;
    margin: 0 16px;
  }

  .el-icon-zoom-in + .svg-icon {
    margin-left: 16px;
  }

  .svg-icon {
    font-size: 16px;
  }
}
</style>
