<template>
  <div
    ref="container"
    :style="{
      position: 'relative',
      userSelect: disabled ? undefined : 'none',
      overflow: 'hidden',
      touchAction: disabled ? undefined : 'none',
    }"
    :class="{ disabledDrag: disabled }"
  >
    <slot :selected="intersected" />
  </div>
</template>

<script setup lang="ts">
/// https://github.com/andi23rosca/drag-select-vue/blob/master/src/DragSelect.vue
import { ref, watch, onMounted, onBeforeUnmount, type PropType } from "vue";
import VueTypes from "vue-types";

function getDimensions(p1: coord, p2: coord): rect {
  return {
    width: Math.abs(p1.x - p2.x),
    height: Math.abs(p1.y - p2.y),
  };
}
function collisionCheck(node1: DOMRect, node2: DOMRect): boolean {
  return (
    node1.left < node2.left + node2.width &&
    node1.left + node1.width > node2.left &&
    node1.top < node2.top + node2.height &&
    node1.top + node1.height > node2.top
  );
}

type coord = { x: number; y: number };
type rect = { width: number; height: number };

const props = defineProps({
  attribute: VueTypes.string.isRequired,
  color: VueTypes.string.def("#4299E1"),
  opacity: VueTypes.number.def(0.7),
  modelValue: {
    type: Object as PropType<Set<string>>,
    required: false,
    default: () => ref(new Set()),
  },
  disabled: {
    type: Boolean,
    required: false,
    default: false,
  },
});

const emit = defineEmits<{
  (event: "update:modelValue", value: Set<string>): void;
  (event: "dragDone", value: Set<string>): void;
  (event: "dragStart"): void;
}>();

const intersected = ref<Set<string>>(props.modelValue);
const container = ref<HTMLElement>();

watch(intersected, (val) => {
  emit("update:modelValue", val);
});
watch(props.modelValue, (val) => {
  if (intersected.value === val) {
    return;
  }
  intersected.value = val;
});

onMounted(() => {
  if (!container.value) {
    console.error(`Container is not referenced.`);
    return;
  }
  const uContainer = container.value;
  let containerRect = uContainer.getBoundingClientRect();

  function getCoords(e: MouseEvent | Touch): coord {
    return {
      x: e.clientX - containerRect.left,
      y: e.clientY - containerRect.top,
    };
  }
  let children: HTMLCollection;
  let box = document.createElement("div");
  box.setAttribute("data-drag-box-component", "");
  box.style.position = "absolute";
  box.style.backgroundColor = props.color;
  box.style.opacity = `${props.opacity}`;
  let start = { x: 0, y: 0 };
  let end = { x: 0, y: 0 };
  function intersection() {
    const rect = box.getBoundingClientRect();
    const localIntersected = new Set<string>();
    for (let i = 0; i < children.length; i++) {
      if (collisionCheck(rect, children[i].getBoundingClientRect())) {
        const attr = children[i].getAttribute(props.attribute);
        if (children[i].hasAttribute(props.attribute)) {
          localIntersected.add(attr as string);
        }
      }
    }

    let dismatch = false;
    for (const oldVal of intersected.value) {
      if (!localIntersected.has(oldVal)) {
        dismatch = true;
        break;
      }
    }
    if (!dismatch) {
      for (const newVal of localIntersected) {
        if (!intersected.value.has(newVal)) {
          dismatch = true;
          break;
        }
      }
    }
    if (dismatch) {
      intersected.value = localIntersected;
    }
  }
  function touchStart(e: TouchEvent) {
    e.preventDefault();
    startDrag(e.touches[0]);
  }
  function touchMove(e: TouchEvent) {
    e.preventDefault();
    drag(e.touches[0]);
  }

  let isMine = false;
  function startDrag(e: MouseEvent | Touch) {
    if (props.disabled) {
      return;
    }
    containerRect = uContainer.getBoundingClientRect();
    children = uContainer.children;
    start = getCoords(e);
    end = start;
    document.addEventListener("mousemove", drag);
    document.addEventListener("touchmove", touchMove);
    box.style.top = start.y + "px";
    box.style.left = start.x + "px";
    uContainer.append(box);
    intersection();
    isMine = true;
    emit("dragStart");
  }
  function drag(e: MouseEvent | Touch) {
    if (props.disabled) {
      return;
    }
    end = getCoords(e);
    const dimensions = getDimensions(start, end);
    if (end.x < start.x) {
      box.style.left = end.x + "px";
    }
    if (end.y < start.y) {
      box.style.top = end.y + "px";
    }
    box.style.width = dimensions.width + "px";
    box.style.height = dimensions.height + "px";
    intersection();
  }
  function endDrag() {
    if (props.disabled) {
      return;
    }
    start = { x: 0, y: 0 };
    end = { x: 0, y: 0 };
    box.style.width = "0";
    box.style.height = "0";
    document.removeEventListener("mousemove", drag);
    document.removeEventListener("touchmove", touchMove);
    box.remove();
    if (isMine) {
      emit("dragDone", intersected.value);
    }
    isMine = false;
  }

  watch(
    () => props.disabled,
    (disabledNext) => {
      if (disabledNext) {
        uContainer.removeEventListener("mousedown", startDrag);
        uContainer.removeEventListener("touchstart", touchStart);
        document.removeEventListener("mouseup", endDrag);
        document.removeEventListener("touchend", endDrag);
      } else {
        uContainer.addEventListener("mousedown", startDrag);
        uContainer.addEventListener("touchstart", touchStart);
        document.addEventListener("mouseup", endDrag);
        document.addEventListener("touchend", endDrag);
      }
    }
  );

  if (!props.disabled) {
    uContainer.addEventListener("mousedown", startDrag);
    uContainer.addEventListener("touchstart", touchStart);
    document.addEventListener("mouseup", endDrag);
    document.addEventListener("touchend", endDrag);
  }

  onBeforeUnmount(() => {
    uContainer.removeEventListener("mousedown", startDrag);
    uContainer.removeEventListener("touchstart", touchStart);
    document.removeEventListener("mouseup", endDrag);
    document.removeEventListener("touchend", endDrag);
  });
});
</script>
