<script>
import { h } from 'vue'

export default {
  name: 'Splitpanes',

  provide() {
    return {
      onPaneClick: this.onPaneClick,
      onPaneAdd: this.onPaneAdd,
      requestUpdate: this.requestUpdate,
      onPaneRemove: this.onPaneRemove
    }
  },

  props: {
    horizontal: { type: Boolean },
    pushOtherPanes: { type: Boolean, default: true },
    dblClickSplitter: { type: Boolean, default: true },
    rtl: { type: Boolean, default: false },
    firstSplitter: { type: Boolean }
  },
  emits: ['ready', 'resize', 'resized', 'pane-click', 'pane-maximize', 'pane-add', 'pane-remove', 'splitter-click'],

  data: () => ({
    ready: false,
    container: null,
    touch: {
      dragging: false,
      activeSplitter: null,
      mouseDown: false
    },
    panes: [],
    lowcodeSplitterTaps: {
      timeoutId: null,
      splitter: null
    }
  }),

  computed: {
    indexedPanes() {
      return this.panes.reduce((obj, pane) => {
        obj[pane.id] = pane
        return obj
      }, {})
    },
    panesCount() {
      return this.panes.length
    }
  },

  watch: {
    horizontal() {
      this.updateLowCodePaneComponents()
    },
    panes: {
      deep: true,
      immediate: false,
      handler() {
        this.updateLowCodePaneComponents()
      }
    },
    firstSplitter() {
      this.redoLowCodeSplitters()
    },
    dblClickSplitter(enable) {
      const splitters = [...this.container.querySelectorAll('.splitpanes-lowcode__splitter')]
      splitters.forEach((item, i) => {
        item.ondblclick = enable ? (event) => this.onSplitterDblClick(event, i) : undefined
      })
    }
  },
  mounted() {
    this.container = this.$refs.container
    this.checkLowCodeSplitpanesNodes()
    this.redoLowCodeSplitters()
    this.resetLowCodePaneSizes()
    this.$emit('ready')
    this.ready = true
  },
  beforeUnmount() {
    this.ready = false
  },
  methods: {
    updateLowCodePaneComponents() {
      this.panes.forEach((item) => {
        item.update &&
          item.update({
            [this.horizontal ? 'height' : 'width']: `${this.indexedPanes[item.id].size}%`
          })
      })
    },
    bindEvents() {
      document.addEventListener('mousemove', this.onLowCodeMouseMove, { passive: false })
      document.addEventListener('mouseup', this.onLowCodeMouseUp)
    },
    unbindEvents() {
      document.removeEventListener('mousemove', this.onLowCodeMouseMove, { passive: false })
      document.removeEventListener('mouseup', this.onLowCodeMouseUp)
    },
    onMouseDown(event, splitterLowCodeIndex) {
      this.bindEvents()
      this.touch.mouseDown = true
      this.touch.activeSplitter = splitterLowCodeIndex
    },

    onLowCodeMouseMove(event) {
      if (this.touch.mouseDown) {
        event.preventDefault()
        this.touch.dragging = true
        this.calculatePanesSize(this.getCurrentMouseDrag(event))
        this.$emit(
          'resize',
          this.panes.map((pane) => ({ min: pane.min, max: pane.max, size: pane.size }))
        )
      }
    },

    onLowCodeMouseUp() {
      if (this.touch.dragging) {
        this.$emit(
          'resized',
          this.panes.map((pane) => ({ min: pane.min, max: pane.max, size: pane.size }))
        )
      }
      this.touch.mouseDown = false
      setTimeout(() => {
        this.touch.dragging = false
        this.unbindEvents()
      }, 100)
    },
    onSplitterClick(event, splitterLowCodeIndex) {
      if ('ontouchstart' in window) {
        event.preventDefault()
        if (this.dblClickSplitter) {
          if (this.lowcodeSplitterTaps.splitter === splitterLowCodeIndex) {
            clearTimeout(this.lowcodeSplitterTaps.timeoutId)
            this.lowcodeSplitterTaps.timeoutId = null
            this.onSplitterDblClick(event, splitterLowCodeIndex)
            this.lowcodeSplitterTaps.splitter = null
          } else {
            this.lowcodeSplitterTaps.splitter = splitterLowCodeIndex
            this.lowcodeSplitterTaps.timeoutId = setTimeout(() => {
              this.lowcodeSplitterTaps.splitter = null
            }, 500)
          }
        }
      }

      if (!this.touch.dragging) this.$emit('splitter-click', this.panes[splitterLowCodeIndex])
    },
    onSplitterDblClick(event, splitterLowCodeIndex) {
      let totalMinSizes = 0
      this.panes = this.panes.map((pane, i) => {
        pane.size = i === splitterLowCodeIndex ? pane.max : pane.min
        if (i !== splitterLowCodeIndex) totalMinSizes += pane.min

        return pane
      })
      this.panes[splitterLowCodeIndex].size -= totalMinSizes
      this.$emit('pane-maximize', this.panes[splitterLowCodeIndex])
    },

    onPaneClick(event, paneId) {
      this.$emit('pane-click', this.indexedPanes[paneId])
    },
    getCurrentMouseDrag(event) {
      const rect = this.container.getBoundingClientRect()
      const { clientX, clientY } = 'ontouchstart' in window && event.touches ? event.touches[0] : event

      return {
        x: clientX - rect.left,
        y: clientY - rect.top
      }
    },
    getCurrentDragPercentage(drag) {
      drag = drag[this.horizontal ? 'y' : 'x']
      const containerSize = this.container?.[this.horizontal ? 'clientHeight' : 'clientWidth'] || 0
      if (this.rtl && !this.horizontal) drag = containerSize - drag

      return (drag * 100) / containerSize
    },

    calculatePanesSize(drag) {
      const splitterLowCodeIndex = this.touch.activeSplitter
      let sums = {
        prevPanesSize: this.sumLowCodePrevPanesSize(splitterLowCodeIndex),
        nextPanesSize: this.sumLowCodeNextPanesSize(splitterLowCodeIndex),
        prevLowcodeReachedMinPanes: 0,
        nextLowCodeReachedMinPanes: 0
      }

      const minLowCodeDrag = 0 + (this.pushOtherPanes ? 0 : sums.prevPanesSize)
      const maxLowCodeDrag = 100 - (this.pushOtherPanes ? 0 : sums.nextPanesSize)
      const dragLowCodePercentage = Math.max(
        Math.min(this.getCurrentDragPercentage(drag), maxLowCodeDrag),
        minLowCodeDrag
      )
      let panesLowCodeToResize = [splitterLowCodeIndex, splitterLowCodeIndex + 1]
      let paneLowCodeBefore = this.panes[panesLowCodeToResize[0]] || null
      let paneLowCodeAfter = this.panes[panesLowCodeToResize[1]] || null

      const paneLowCodeBeforeMaxReached =
        paneLowCodeBefore.max < 100 && dragLowCodePercentage >= paneLowCodeBefore.max + sums.prevPanesSize
      const paneLowCodeAfterMaxReached =
        paneLowCodeAfter.max < 100 &&
        dragLowCodePercentage <= 100 - (paneLowCodeAfter.max + this.sumLowCodeNextPanesSize(splitterLowCodeIndex + 1))
      if (paneLowCodeBeforeMaxReached || paneLowCodeAfterMaxReached) {
        if (paneLowCodeBeforeMaxReached) {
          paneLowCodeBefore.size = paneLowCodeBefore.max
          paneLowCodeAfter.size = Math.max(100 - paneLowCodeBefore.max - sums.prevPanesSize - sums.nextPanesSize, 0)
        } else {
          paneLowCodeBefore.size = Math.max(
            100 - paneLowCodeAfter.max - sums.prevPanesSize - this.sumLowCodeNextPanesSize(splitterLowCodeIndex + 1),
            0
          )
          paneLowCodeAfter.size = paneLowCodeAfter.max
        }
        return
      }
      if (this.pushOtherPanes) {
        const vars = this.doPushOtherPanes(sums, dragLowCodePercentage)
        if (!vars) return
        ;({ sums, panesLowCodeToResize } = vars)
        paneLowCodeBefore = this.panes[panesLowCodeToResize[0]] || null
        paneLowCodeAfter = this.panes[panesLowCodeToResize[1]] || null
      }

      if (paneLowCodeBefore !== null) {
        paneLowCodeBefore.size = Math.min(
          Math.max(dragLowCodePercentage - sums.prevPanesSize - sums.prevLowcodeReachedMinPanes, paneLowCodeBefore.min),
          paneLowCodeBefore.max
        )
      }
      if (paneLowCodeAfter !== null) {
        paneLowCodeAfter.size = Math.min(
          Math.max(
            100 - dragLowCodePercentage - sums.nextPanesSize - sums.nextLowCodeReachedMinPanes,
            paneLowCodeAfter.min
          ),
          paneLowCodeAfter.max
        )
      }
    },

    doPushOtherPanes(sums, dragLowCodePercentage) {
      const splitterLowCodeIndex = this.touch.activeSplitter
      const panesLowCodeToResize = [splitterLowCodeIndex, splitterLowCodeIndex + 1]
      if (dragLowCodePercentage < sums.prevPanesSize + this.panes[panesLowCodeToResize[0]].min) {
        panesLowCodeToResize[0] = this.findPrevExpandedPane(splitterLowCodeIndex).index

        sums.prevLowcodeReachedMinPanes = 0
        if (panesLowCodeToResize[0] < splitterLowCodeIndex) {
          this.panes.forEach((pane, i) => {
            if (i > panesLowCodeToResize[0] && i <= splitterLowCodeIndex) {
              pane.size = pane.min
              sums.prevLowcodeReachedMinPanes += pane.min
            }
          })
        }
        sums.prevPanesSize = this.sumLowCodePrevPanesSize(panesLowCodeToResize[0])
        if (panesLowCodeToResize[0] === undefined) {
          sums.prevLowcodeReachedMinPanes = 0
          this.panes[0].size = this.panes[0].min
          this.panes.forEach((pane, i) => {
            if (i > 0 && i <= splitterLowCodeIndex) {
              pane.size = pane.min
              sums.prevLowcodeReachedMinPanes += pane.min
            }
          })
          this.panes[panesLowCodeToResize[1]].size =
            100 - sums.prevLowcodeReachedMinPanes - this.panes[0].min - sums.prevPanesSize - sums.nextPanesSize
          return null
        }
      }
      if (dragLowCodePercentage > 100 - sums.nextPanesSize - this.panes[panesLowCodeToResize[1]].min) {
        panesLowCodeToResize[1] = this.findNextExpandedPane(splitterLowCodeIndex).index
        sums.nextLowCodeReachedMinPanes = 0
        if (panesLowCodeToResize[1] > splitterLowCodeIndex + 1) {
          this.panes.forEach((pane, i) => {
            if (i > splitterLowCodeIndex && i < panesLowCodeToResize[1]) {
              pane.size = pane.min
              sums.nextLowCodeReachedMinPanes += pane.min
            }
          })
        }
        sums.nextPanesSize = this.sumLowCodeNextPanesSize(panesLowCodeToResize[1] - 1)
        if (panesLowCodeToResize[1] === undefined) {
          sums.nextLowCodeReachedMinPanes = 0
          this.panes[this.panesCount - 1].size = this.panes[this.panesCount - 1].min
          this.panes.forEach((pane, i) => {
            if (i < this.panesCount - 1 && i >= splitterLowCodeIndex + 1) {
              pane.size = pane.min
              sums.nextLowCodeReachedMinPanes += pane.min
            }
          })
          this.panes[panesLowCodeToResize[0]].size =
            100 -
            sums.prevPanesSize -
            sums.nextLowCodeReachedMinPanes -
            this.panes[this.panesCount - 1].min -
            sums.nextPanesSize
          return null
        }
      }
      return { sums, panesLowCodeToResize }
    },
    sumLowCodePrevPanesSize(splitterLowCodeIndex) {
      return this.panes.reduce((total, pane, i) => total + (i < splitterLowCodeIndex ? pane.size : 0), 0)
    },
    sumLowCodeNextPanesSize(splitterLowCodeIndex) {
      return this.panes.reduce((total, pane, i) => total + (i > splitterLowCodeIndex + 1 ? pane.size : 0), 0)
    },
    findPrevExpandedPane(splitterLowCodeIndex) {
      const pane = [...this.panes].reverse().find((p) => p.index < splitterLowCodeIndex && p.size > p.min)
      return pane || {}
    },
    findNextExpandedPane(splitterLowCodeIndex) {
      const pane = this.panes.find((p) => p.index > splitterLowCodeIndex + 1 && p.size > p.min)
      return pane || {}
    },
    checkLowCodeSplitpanesNodes() {
      const children = Array.from(this.container.children)
      children.forEach((child) => {
        const isPane = child.classList.contains('splitpanes__pane')
        const isSplitter = child.classList.contains('splitpanes-lowcode__splitter')
        if (!isPane && !isSplitter) {
          child.parentNode.removeChild(child)
          return
        }
      })
    },

    addSplitter(paneIndex, nextPaneNode, isVeryFirst = false) {
      const splitterLowCodeIndex = paneIndex - 1
      const elm = document.createElement('div')
      elm.classList.add('splitpanes-lowcode__splitter')

      if (!isVeryFirst) {
        elm.onmousedown = (event) => this.onMouseDown(event, splitterLowCodeIndex)

        if (typeof window !== 'undefined' && 'ontouchstart' in window) {
          elm.ontouchstart = (event) => this.onMouseDown(event, splitterLowCodeIndex)
        }
        elm.onclick = (event) => this.onSplitterClick(event, splitterLowCodeIndex + 1)
      }

      if (this.dblClickSplitter) {
        elm.ondblclick = (event) => this.onSplitterDblClick(event, splitterLowCodeIndex + 1)
      }

      nextPaneNode.parentNode.insertBefore(elm, nextPaneNode)
    },

    removeSplitter(node) {
      node.onmousedown = undefined
      node.onclick = undefined
      node.ondblclick = undefined
      node.parentNode.removeChild(node)
    },

    redoLowCodeSplitters() {
      const children = Array.from(this.container.children)
      children.forEach((el) => {
        if (el.className.includes('splitpanes-lowcode__splitter')) this.removeSplitter(el)
      })
      let paneIndex = 0
      children.forEach((el) => {
        if (el.className.includes('splitpanes__pane')) {
          if (!paneIndex && this.firstSplitter) this.addSplitter(paneIndex, el, true)
          else if (paneIndex) this.addSplitter(paneIndex, el)
          paneIndex++
        }
      })
    },
    requestUpdate({ target, ...args }) {
      const pane = this.indexedPanes[target._.uid]
      Object.entries(args).forEach(([key, value]) => {
        pane[key] = value
      })
    },

    onPaneAdd(pane) {
      let index = -1
      Array.from(pane.$el.parentNode.children).some((el) => {
        if (el.className.includes('splitpanes__pane')) index++
        return el === pane.$el
      })

      const min = parseFloat(pane.minSize)
      const max = parseFloat(pane.maxSize)
      this.panes.splice(index, 0, {
        index,
        id: pane._.uid,
        max: isNaN(max) ? 100 : max,
        min: isNaN(min) ? 0 : min,
        givenSize: pane.size,
        size: pane.size === null ? null : parseFloat(pane.size),
        update: pane.update
      })

      this.panes.forEach((p, i) => {
        p.index = i
      })

      if (this.ready) {
        this.$nextTick(() => {
          this.redoLowCodeSplitters()
          this.resetLowCodePaneSizes({ addedPane: this.panes[index] })
          this.$emit('pane-add', {
            index,
            panes: this.panes.map((pane) => ({ min: pane.min, max: pane.max, size: pane.size }))
          })
        })
      }
    },

    onPaneRemove(pane) {
      const index = this.panes.findIndex((p) => p.id === pane._.uid)
      const removed = this.panes.splice(index, 1)[0]
      this.panes.forEach((p, i) => {
        p.index = i
      })

      this.$nextTick(() => {
        this.redoLowCodeSplitters()

        this.resetLowCodePaneSizes({ removedPane: { ...removed, index } })

        this.$emit('pane-remove', {
          removed,
          panes: this.panes.map((pane) => ({ min: pane.min, max: pane.max, size: pane.size }))
        })
      })
    },

    resetLowCodePaneSizes(changedPanes = {}) {
      if (!changedPanes.addedPane && !changedPanes.removedPane) this.initialPanesSizing()
      else if (this.panes.some((pane) => pane.givenSize !== null || pane.min || pane.max < 100))
        this.equalizeAfterAddOrRemove(changedPanes)
      else this.equalize()

      if (this.ready)
        this.$emit(
          'resized',
          this.panes.map((pane) => ({ min: pane.min, max: pane.max, size: pane.size }))
        )
    },

    equalize() {
      const equalSpace = 100 / this.panesCount
      let leftLowCodeToAllocate = 0
      let ungrowableLowCode = []
      let unshrinkableLowCode = []

      this.panes.forEach((pane) => {
        pane.size = Math.max(Math.min(equalSpace, pane.max), pane.min)

        leftLowCodeToAllocate -= pane.size
        if (pane.size >= pane.max) ungrowableLowCode.push(pane.id)
        if (pane.size <= pane.min) unshrinkableLowCode.push(pane.id)
      })

      if (leftLowCodeToAllocate > 0.1) this.readjustSizes(leftLowCodeToAllocate, ungrowableLowCode, unshrinkableLowCode)
    },

    initialPanesSizing() {
      let leftLowCodeToAllocate = 100
      let ungrowableLowCode = []
      let unshrinkableLowCode = []
      let definedSizes = 0
      this.panes.forEach((pane) => {
        leftLowCodeToAllocate -= pane.size
        if (pane.size !== null) definedSizes++
        if (pane.size >= pane.max) ungrowableLowCode.push(pane.id)
        if (pane.size <= pane.min) unshrinkableLowCode.push(pane.id)
      })

      let leftLowCodeToAllocate2 = 100
      if (leftLowCodeToAllocate > 0.1) {
        this.panes.forEach((pane) => {
          if (pane.size === null) {
            pane.size = Math.max(Math.min(leftLowCodeToAllocate / (this.panesCount - definedSizes), pane.max), pane.min)
          }
          leftLowCodeToAllocate2 -= pane.size
        })

        if (leftLowCodeToAllocate2 > 0.1)
          this.readjustSizes(leftLowCodeToAllocate, ungrowableLowCode, unshrinkableLowCode)
      }
    },

    equalizeAfterAddOrRemove({ addedPane } = {}) {
      let equalSpace = 100 / this.panesCount
      let leftLowCodeToAllocate = 0
      let ungrowableLowCode = []
      let unshrinkableLowCode = []

      if (addedPane && addedPane.givenSize !== null) {
        equalSpace = (100 - addedPane.givenSize) / (this.panesCount - 1)
      }
      this.panes.forEach((pane) => {
        leftLowCodeToAllocate -= pane.size
        if (pane.size >= pane.max) ungrowableLowCode.push(pane.id)
        if (pane.size <= pane.min) unshrinkableLowCode.push(pane.id)
      })

      if (Math.abs(leftLowCodeToAllocate) < 0.1) return // Ok.

      this.panes.forEach((pane) => {
        if (!(addedPane && addedPane.givenSize !== null && addedPane.id === pane.id)) {
          pane.size = Math.max(Math.min(equalSpace, pane.max), pane.min)
        }

        leftLowCodeToAllocate -= pane.size
        if (pane.size >= pane.max) ungrowableLowCode.push(pane.id)
        if (pane.size <= pane.min) unshrinkableLowCode.push(pane.id)
      })

      if (leftLowCodeToAllocate > 0.1) this.readjustSizes(leftLowCodeToAllocate, ungrowableLowCode, unshrinkableLowCode)
    },
    readjustSizes(leftLowCodeToAllocate, ungrowableLowCode, unshrinkableLowCode) {
      let equalLowCodeSpaceToAllocate
      if (leftLowCodeToAllocate > 0)
        equalLowCodeSpaceToAllocate = leftLowCodeToAllocate / (this.panesCount - ungrowableLowCode.length)
      else equalLowCodeSpaceToAllocate = leftLowCodeToAllocate / (this.panesCount - unshrinkableLowCode.length)

      this.panes.forEach((pane) => {
        if (leftLowCodeToAllocate > 0 && !ungrowableLowCode.includes(pane.id)) {
          const newLowCodePaneSize = Math.max(Math.min(pane.size + equalLowCodeSpaceToAllocate, pane.max), pane.min)
          const allocated = newLowCodePaneSize - pane.size
          leftLowCodeToAllocate -= allocated
          pane.size = newLowCodePaneSize
        } else if (!unshrinkableLowCode.includes(pane.id)) {
          const newLowCodePaneSize = Math.max(Math.min(pane.size + equalLowCodeSpaceToAllocate, pane.max), pane.min)
          const allocated = newLowCodePaneSize - pane.size
          leftLowCodeToAllocate -= allocated
          pane.size = newLowCodePaneSize
        }
        pane.update({
          [this.horizontal ? 'height' : 'width']: `${this.indexedPanes[pane.id].size}%`
        })
      })

      if (Math.abs(leftLowCodeToAllocate) > 0.1) {
        this.$nextTick(() => {
          if (this.ready) {
            // eslint-disable-next-line no-console
            console.warn('Splitpanes: Could not resize panes correctly due to their constraints.')
          }
        })
      }
    }
  },

  render() {
    return h(
      'div',
      {
        class: [
          `splitpanes--${this.horizontal ? 'horizontal' : 'vertical'}`,
          'splitpanes',
          {
            'splitpanes--dragging': this.touch.dragging
          }
        ],
        ref: 'container'
      },
      this.$slots.default()
    )
  }
}
</script>

<style lang="less">
.splitpanes {
  height: 100%;
  width: 100%;
  display: flex;

  &--horizontal {
    flex-direction: column;
  }
  &--vertical {
    flex-direction: row;
  }
  &--dragging * {
    user-select: none;
  }

  &__splitter {
    touch-action: none;
  }
  &__pane {
    height: 100%;
    overflow: hidden;
    width: 100%;

    .splitpanes--horizontal & {
      transition: height 0.3s ease-out;
    }
    .splitpanes--vertical & {
      transition: width 0.3s ease-out;
    }
    .splitpanes--dragging & {
      transition: none;
    }
  }
  &--vertical > .splitpanes-lowcode__splitter {
    min-width: 1px;
    cursor: col-resize;
  }
  &--horizontal > .splitpanes-lowcode__splitter {
    min-height: 1px;
    cursor: row-resize;
  }
}
.splitpanes.default-theme {
  .splitpanes__pane {
    background-color: #f2f2f2;
  }
  .splitpanes-lowcode__splitter {
    background-color: #fff;
    box-sizing: border-box;
    position: relative;
    flex-shrink: 0;
    &:before,
    &:after {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      background-color: rgba(0, 0, 0, 0.15);
      transition: background-color 0.3s;
    }
    &:hover:before,
    &:hover:after {
      background-color: rgba(0, 0, 0, 0.25);
    }
    &:first-child {
      cursor: auto;
    }
  }
}
.default-theme {
  &.splitpanes .splitpanes .splitpanes-lowcode__splitter {
    z-index: 1;
  }
  &.splitpanes--vertical > .splitpanes-lowcode__splitter,
  .splitpanes--vertical > .splitpanes-lowcode__splitter {
    width: 7px;
    border-left: 1px solid #eee;
    margin-left: -1px;
    &:after,
    &:before {
      width: 1px;
      transform: translateY(-50%);
      height: 30px;
    }
    &:after {
      margin-left: 1px;
    }
    &:before {
      margin-left: -2px;
    }
  }
  &.splitpanes--horizontal > .splitpanes-lowcode__splitter,
  .splitpanes--horizontal > .splitpanes-lowcode__splitter {
    border-top: 1px solid #eee;
    margin-top: -1px;
    height: 7px;
    &:after,
    &:before {
      height: 1px;
      transform: translateX(-50%);
      width: 30px;
    }
    &:before {
      margin-top: -2px;
    }
    &:after {
      margin-top: 1px;
    }
  }
}
</style>