<template>
  <div class="h-screen w-screen">
    <v-stage class="h-full w-full" ref="stage" :config="stageSize" @mousedown="handleMouseDown"
      @mousemove="handleMouseMove" @mouseup="handleMouseUp" @click="handleStageClick">
      <v-layer ref="layer">
        <template v-for="item in objMap.values()" :key="item.id">
          <v-group :config="{
            x: item.x,
            y: item.y,
            draggable: true,
            id: `group-${item.id}`,
          }" @dragstart="handleDragStart" @dragend="handleDragEnd" @mouseover="handleCanvasObjectMouseOver">
            <v-rect :config="item.config" />
            <v-rect v-show="!isUndefined(item.box)" :config="{
              ...item.box,
              visible: !isUndefined(item.box),
              stroke: 'red',
              strokeWidth: 3,
            }">
            </v-rect>
          </v-group>
        </template>

        <v-transformer ref="transformer" />
        <v-rect ref="selectRect" v-if="selectionRectangle.visible" :config="{
          x: Math.min(selectionRectangle.x1, selectionRectangle.x2),
          y: Math.min(selectionRectangle.y1, selectionRectangle.y2),
          width: Math.abs(selectionRectangle.x2 - selectionRectangle.x1),
          height: Math.abs(selectionRectangle.y2 - selectionRectangle.y1),
          fill: '#0069FF88',
        }" />
      </v-layer>
    </v-stage>
    <div class="absolute top-20 left-10">
      <input type="checkbox" class="checkbox" @change="handleCacheChange" />
      cache shapes
    </div>
  </div>
</template>

<script setup lang="ts">
import { isNull, isUndefined } from "mathjs";
import Konva from "konva";
import type {
  VGroup,
  VLayer,
  VNode,
  VStage,
  VTransformer,
} from "@/utils/VueKonvaType";
import { ref, reactive, watch, onMounted, useTemplateRef } from "vue";

const stageSize = {
  width: window.innerWidth,
  height: window.innerHeight,
};

type CanvasObjectBox = {
  x: number;
  y: number;
  width: number;
  height: number;
};

type CanvasObject = {
  type: "Rect";
  config: Konva.RectConfig;
  id: string;
  x: number;
  y: number;
  box?: CanvasObjectBox;
};

function calculateRectBounding(
  width: number,
  height: number,
  rotation: number,
  padding?: number,
) {
  // calculate bounding box for rotated rectangle
  const radians = (rotation * Math.PI) / 180;
  const cos = Math.cos(radians);
  const sin = Math.sin(radians);

  // calculate corners of the rectangle
  const corners = [
    { x: 0, y: 0 },
    { x: width, y: 0 },
    { x: width, y: height },
    { x: 0, y: height },
  ].map((point) => ({
    x: point.x * cos - point.y * sin,
    y: point.x * sin + point.y * cos,
  }));

  // find bounding box dimensions
  const minX = Math.min(...corners.map((p) => p.x));
  const maxX = Math.max(...corners.map((p) => p.x));
  const minY = Math.min(...corners.map((p) => p.y));
  const maxY = Math.max(...corners.map((p) => p.y));

  if (padding)
    return {
      x: minX - padding,
      y: minY - padding,
      width: maxX - minX + padding * 2,
      height: maxY - minY + padding * 2,
    };
  else
    return {
      x: minX,
      y: minY,
      width: maxX - minX,
      height: maxY - minY,
    };
}

const objMap = reactive<Map<string, CanvasObject>>(new Map());
onMounted(() => {
  for (let n = 0; n < 100; n++) {
    const id = Math.round(Math.random() * 10000).toString();
    const x = Math.random() * stageSize.width;
    const y = Math.random() * stageSize.height;
    const width = 30 + Math.random() * 30;
    const height = 30 + Math.random() * 30;
    const rotation = Math.random() * 180;

    objMap.set(id, {
      type: "Rect",
      config: {
        width: width,
        height: height,
        rotation: rotation,
        fill: "grey",
        id: id,
      },
      id: id,
      x: x,
      y: y,
    });
  }
});

const layer = useTemplateRef<VLayer>("layer");
const transformer = useTemplateRef<VTransformer>("transformer");
const selectRect = useTemplateRef<VNode>("selectRect");
const stage = useTemplateRef<VStage>("stage");

const isDragging = ref(false);
const dragItemId = ref<string | null>(null);

const isSelecting = ref(false);
const selectedIds = ref<string[]>([]);
const selectionRectangle = reactive({
  visible: false,
  x1: 0,
  y1: 0,
  x2: 0,
  y2: 0,
});

onMounted(() => {
  if (isNull(transformer.value)) return;

  const selectedBox = transformer.value.getNode();
  selectedBox.resizeEnabled(false);
  selectedBox.rotateEnabled(false);
});

function handleCacheChange(e: Event) {
  const target = e.target as HTMLInputElement;
  const shouldCache = isNull(target) ? false : target.checked;

  if (isNull(layer.value)) return;

  if (shouldCache) {
    layer.value.getNode().cache();
  } else {
    layer.value.getNode().clearCache();
  }
}

// Drag event handlers
function handleDragStart(e: Event) {
  isDragging.value = true;

  // save drag element:
  const target = e.target as unknown as Konva.Node;
  dragItemId.value = target.id();

  // move current element to the top:
  // const item = list.value.find((i) => i.id === dragItemId.value);
  // if (isUndefined(item)) return;
  //
  // const index = list.value.indexOf(item);
  // list.value.splice(index, 1);
  // list.value.push(item);
}

function handleDragEnd() {
  isDragging.value = false;
  dragItemId.value = null;
}

// Update transformer nodes when selection changes
watch(selectedIds, () => {
  if (!transformer.value) return;

  const nodes = selectedIds.value
    .map((id) => {
      return layer.value
        ?.getNode()
        .find((ref: Konva.Node) => ref.attrs.id === id);
    })
    .flat()
    .filter(Boolean) as Konva.Node[];

  if (!isUndefined(nodes)) transformer.value.getNode().nodes(nodes);
});

// Mouse event handlers
function handleStageClick(e: Event) {
  if (isNull(e.target)) return;
  const target = e.target as unknown as Konva.Container;

  // if we are selecting with rect, do nothing
  if (selectionRectangle.visible) {
    return;
  }

  // if click on empty area - remove all selections
  if ((e.target as unknown) === target.getStage()) {
    selectedIds.value = [];
    return;
  }

  // do nothing if clicked NOT on our rectangles
  if (!target.hasName("rect")) {
    return;
  }

  const clickedId = target.attrs.id;

  // do we pressed shift or ctrl?
  const metaPressed =
    (e as any).evt?.shiftKey ||
    (e as any).evt?.ctrlKey ||
    (e as any).evt?.metaKey;
  const isSelected = selectedIds.value.includes(clickedId);

  if (!metaPressed && !isSelected) {
    // if no key pressed and the node is not selected
    // select just one
    selectedIds.value = [clickedId];
  } else if (metaPressed && isSelected) {
    // if we pressed keys and node was selected
    // we need to remove it from selection:
    selectedIds.value = selectedIds.value.filter((id) => id !== clickedId);
  } else if (metaPressed && !isSelected) {
    // add the node into selection
    selectedIds.value = [...selectedIds.value, clickedId];
  }
}

function handleMouseDown(e: Event) {
  if (isNull(e.target)) return;
  const target = e.target as unknown as Konva.Container;

  // do nothing if we mousedown on any shape
  if ((e.target as unknown) !== target.getStage()) {
    return;
  }

  // start selection rectangle
  isSelecting.value = true;
  const pos = target.getStage()?.getPointerPosition();
  if (!isNull(pos) && !isUndefined(pos)) {
    selectionRectangle.visible = true;
    selectionRectangle.x1 = pos.x;
    selectionRectangle.y1 = pos.y;
    selectionRectangle.x2 = pos.x;
    selectionRectangle.y2 = pos.y;
  }
}

function handleMouseMove(e: Event) {
  if (isNull(e.target)) return;
  const target = e.target as unknown as Konva.Container;

  // do nothing if we didn't start selection
  if (!isSelecting.value) {
    return;
  }

  const pos = target.getStage()?.getPointerPosition();
  if (!isNull(pos) && !isUndefined(pos)) {
    selectionRectangle.x2 = pos.x;
    selectionRectangle.y2 = pos.y;
  }
}

function handleMouseUp() {
  // do nothing if we didn't start selection
  if (!isSelecting.value) {
    return;
  }

  isSelecting.value = false;

  // update visibility in timeout, so we can check it in click event
  setTimeout(() => {
    selectionRectangle.visible = false;
  });

  const selBox = {
    x: Math.min(selectionRectangle.x1, selectionRectangle.x2),
    y: Math.min(selectionRectangle.y1, selectionRectangle.y2),
    width: Math.abs(selectionRectangle.x2 - selectionRectangle.x1),
    height: Math.abs(selectionRectangle.y2 - selectionRectangle.y1),
  };

  if (isNull(layer.value)) return;
  const selected = layer.value.getNode().children.filter((node: Konva.Node) => {
    // Check if rectangle intersects with selection box
    if (
      node === transformer.value?.getNode() ||
      node === selectRect.value?.getNode()
    )
      return false;
    return Konva.Util.haveIntersection(selBox);
  });

  selectedIds.value = selected.map((node: Konva.Node) => node.id());
}

function handleCanvasObjectMouseOver(evt: Event) {
  if (isNull(evt.target)) return;
  const target = evt.target;

  let object = null;
  if (target instanceof Konva.Group) {
    if (!target.hasChildren()) return;
    object = target.children[0];
  } else if (target instanceof Konva.Rect) {
    object = target;
  } else {
    console.trace(`Not Konva class: ${target}`);
    return;
  }

  // Get client rect
  const objectConfig = objMap.get(object.id());
  if (isUndefined(objectConfig)) {
    console.error(`Not found object id: ${object.id()}`);
    return;
  }
  if (isUndefined(objectConfig.box)) {
    if (
      objectConfig.config.width &&
      objectConfig.config.height &&
      objectConfig.config.rotation
    ) {
      objectConfig.box = calculateRectBounding(
        objectConfig.config.width,
        objectConfig.config.height,
        objectConfig.config.rotation,
        5,
      );
    } else console.error("Could not calculate rect bounding");
  }
}
</script>

<style>
@import "../assets/main.css";
</style>
