<template>
  <section class="resizable-layout" :style="styleObj" ref="layoutRef">
    <slot></slot>
  </section>
</template>

<script lang="ts">
import { defineComponent, provide, PropType } from 'vue';
import mitt, { Emitter } from 'mitt';
import ResizableLayoutItem from './ResizableLayoutItem.vue';
import { EmitterType } from '@/constants/emitter';
import ProvideInject from '@/constants/provideInject';

interface DOMElement extends HTMLElement {
  [key: string]: any;
}

type Direction = 'horizontal' | 'vertical';

/**
 * 由于 Composition Api 的限制，通过slot动态插入的子组件难以直接访问父组件定义在 setup 中的方法(父组件也难以直接访问子组件定义在 setup 中的方法)，
 * 故此组件(ResizableLayout)以及子组件(ResizableLayoutItem)使用传统的 Options API 开发。
 * 事实上，通过控制台打印，发现通过setup中的getCurrentInstance()获取到的实例中有一个 parent 属性和 ctx 属性，分别可以直接获取父组件实例和setup中定义的方法，
 * 使用时类似于这种方式 (getCurrentInstance().parent as any).ctx 和 (getCurrentInstance() as any).ctx。
 * 本组件第一版就是通过这种方式实现父子组件的通信，但是 Vue 官方文档中没有找到任何相关信息，且官方文档中说明了：文档中没说明的功能，代表它不存在。
 * 更重要的一点是：项目在 build 上线之后(即NODE_ENV=production)， (getCurrentInstance() as any).ctx这种方式就失效了，需要通过(getCurrentInstance() as any).ctx._.setupState才能获取，
 * 为了避免之后 Vue 版本更新可能改变内部 api 导致组件失效，因此决定使用Options API
 */

export default defineComponent({
  name: 'resizable-layout',
  props: {
    // 布局的方向：横向或纵向
    direction: {
      type: String as PropType<Direction>,
      default: 'horizontal',
      validator: (direction: Direction) => {
        return ['horizontal', 'vertical'].includes(direction);
      },
    },
    // 防抖函数中需要延迟的毫秒数
    debounce: {
      type: Number,
      default: 100,
    },
    // 防抖函数中允许被延迟的最大值
    maxWait: {
      type: Number,
      default: Number.MAX_SAFE_INTEGER,
    },
  },
  data() {
    return {
      layoutItemElements: [] as Array<DOMElement>, // 子组件 **(resizable-layout-item)** 的 DOM 实例数组
      layoutItemInstances: [] as any[], // 子组件的Vue实例数组
      layoutItemsMinSize: [] as number[], // 子组件的最小尺寸数组
    };
  },
  computed: {
    // 将 props 中的 direction 转为 css3 中 flex-direction 属性对应的值
    flexDirection(): string {
      return this.direction === 'vertical' ? 'column' : 'row';
    },
    styleObj(): { flexDirection: string } {
      return {
        flexDirection: this.flexDirection,
      };
    },
  },
  setup(props, context) {
    const { slots } = context;
    if (slots.default) {
      const children = slots.default();
      if (
        children.some(
          child => child.type !== ResizableLayoutItem && !child.type.toString().startsWith('Symbol(') // Symbol(Comment) 代表注释
        )
      ) {
        console.error('<resizable-layout> 中只能直接包含 <resizable-layout-item> 子组件', children);
      }
    }
  },
  created() {
    this.init();
  },
  mounted() {
    this.setHtmlDataAttribute();
  },
  methods: {
    // 初始化
    init() {
      const self = this as any;
      provide(ProvideInject.DIRECTION, self.flexDirection);
      provide(ProvideInject.DEBOUNCE, self.debounce);
      provide(ProvideInject.MAX_WAIT, self.maxWait);
      // provide(ProvideInject.BAR_WIDTH, this.barWidth);
      // provide(ProvideInject.DOTTED, this.dotted);

      // 事件监听器
      self.emitter = mitt() as Emitter;

      self.currentMoveId = null; // symbol 类型，当前move事件的唯一标志

      // 拖拽条移动事件监听
      if (self.flexDirection === 'column') {
        self.emitter.on(EmitterType.DRAG_BAR_MOVE, self.verticalHandler);
      } else {
        self.emitter.on(EmitterType.DRAG_BAR_MOVE, self.horizontalHandler);
      }
    },

    // 给子组件设置 html 的 data- 属性
    // data-index 子组件(resizable-layout-item)的索引，即位于父组件中第几个(从 0 开始)
    // data-size 子组件(resizable-layout-item)在鼠标刚刚按下的时候的尺寸
    setHtmlDataAttribute() {
      const parentElement = this.$refs.layoutRef as HTMLElement;
      const childElements = parentElement.children;
      for (let i = 0; i < childElements.length; i += 1) {
        const element = childElements[i] as HTMLElement;
        element.setAttribute('data-index', `${i}`);
        element.setAttribute('data-size', `${element.offsetWidth}`);
        this.layoutItemElements[i] = element;
      }
    },

    // 约定大写字母开头表示为供外界调用的方法
    // 当布局改变时（如子组件通过 v-if 动态添加或删除）刷新布局
    Refresh() {
      setTimeout(() => {
        this.layoutItemElements.length = 0;
        // 重新设置 html 的 data- 属性
        this.setHtmlDataAttribute();

        this.layoutItemInstances.map(instance => {
          // 在 ResizableLayoutItem 中的 unmounted 函数中添加了 $unmounted 字段，
          // 标记该对象已经 unmounted
          if (!instance.$unmounted) {
            instance.RequestEmitter();
          }
          return instance;
        });
        // 清空子组件的Vue实例数组
        this.layoutItemInstances.length = 0;
        // 清空子组件的最小尺寸数组
        this.layoutItemsMinSize.length = 0;
      }, 100);
    },

    // 约定大写字母开头表示为供外界调用的方法
    // 供子组件调用，给子组件返回 emitter
    GiveEmitter({ instance, index, minSize }: ResizableLayout.IDataParam) {
      instance.RegisterEmitter((this as any).emitter);
      this.layoutItemInstances[index] = instance;
      this.layoutItemsMinSize[index] = minSize;
    },

    // 返回 data-size
    getSize(element: HTMLElement) {
      return parseInt(element.getAttribute('data-size') || '0', 10);
    },

    // 重新分配子组件的 flex-grow
    assignChildrenFlexGrow(index: number, distance: number) {
      // 此时仍然是当前的move事件，即鼠标未松开

      // 需要调整大小的子组件是否都满足 min-width(min-height) 的要求，初始值设为 true，假设都满足
      let allFitMinWidth = true;
      // 需要调整大小的子组件
      for (let i = index, sign = 1; i <= index + 1; i += 1, sign = -sign) {
        const flexGrow = this.getSize(this.layoutItemElements[i]) + sign * distance;
        if (flexGrow < this.layoutItemsMinSize[i]) {
          allFitMinWidth = false;
          break;
        }
      }
      // allFitMinWidth为false，则终止重新分配
      if (!allFitMinWidth) {
        return;
      }

      // 需要调整大小的子组件的前面的子组件
      // 他们保持原来的尺寸就好
      for (let i = 0; i < index; i += 1) {
        this.layoutItemInstances[i].AdjustFlexGrow(this.getSize(this.layoutItemElements[i]));
      }

      // 需要调整大小的子组件
      for (let i = index, sign = 1; i <= index + 1; i += 1, sign = -sign) {
        const flexGrow = this.getSize(this.layoutItemElements[i]) + sign * distance;
        // 通知子组件调整自身大小
        this.layoutItemInstances[i].AdjustFlexGrow(flexGrow);
        // 通知子组件触发 resize 事件
        this.layoutItemInstances[i].EmitResize();
      }

      // 需要调整大小的子组件的后面的子组件
      // 他们保持原来的尺寸就好
      for (let i = index + 2; i < this.layoutItemInstances.length; i += 1) {
        this.layoutItemInstances[i].AdjustFlexGrow(this.getSize(this.layoutItemElements[i]));
      }
    },

    // 拖拽事件监听处理器
    moveHandler(sizeProperty: string, moveParam: ResizableLayout.IMoveParam) {
      const { distance, index, moveId } = moveParam;
      if ((this as any).currentMoveId !== moveId) {
        // 一个新的move事件，即鼠标松开了之后重新按下之后的move事件
        (this as any).currentMoveId = moveId;
        // 设置子组件的 data-size，用于记录在 "重新分配子组件的 flex-grow" 之前，各子组件的宽度
        this.layoutItemElements.map(element => {
          element.setAttribute('data-size', `${element[sizeProperty]}`);
          return element;
        });
      }
      this.assignChildrenFlexGrow(index, distance);
    },

    // 水平布局拖拽监听
    horizontalHandler(moveParam: ResizableLayout.IMoveParam) {
      this.moveHandler('offsetWidth', moveParam);
    },

    // 垂直布局拖拽监听
    verticalHandler(moveParam: ResizableLayout.IMoveParam) {
      this.moveHandler('offsetHeight', moveParam);
    },
  },
});
</script>

<style lang="scss" scoped>
.resizable-layout {
  position: relative;
  display: flex;
  width: 100%;
  height: 100%;

  ::v-deep > .resizable-layout-item {
    &:last-child {
      > .drag-bar {
        display: none;
      }
    }
  }
}
</style>
