<template>
  <div>
    <v-stage
      class="h-full w-full"
      ref="stageRef"
      :config="stageSize"
      @mousedown="handleMouseDown"
      @mousemove="handleMouseMove"
      @mouseup="handleMouseUp"
      @wheel="handleWheel"
      @click="handleStageClick"
    >
      <v-layer ref="layerRef">
        <template
          ref="canvasObjects"
          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"
            @mouseout="handleCanvasObjectMouseOut"
          >
            <v-rect
              v-show="!isUndefined(item.box)"
              :config="{
                ...item.box,
                visible:
                  !isUndefined(item.box) &&
                  item.isHoverring &&
                  !isDragging &&
                  selectedIds.length == 0,
                stroke: 'rgb(125,193,239)',
                strokeWidth: 2.5,
                dash: [10, 5],
                cornerRadius: 10,
              }"
            >
            </v-rect>
            <v-rect :config="item.config" />
          </v-group>
        </template>

        <v-transformer
          ref="transformerRef"
          :config="{
            borderStroke: 'rgb(125,193,239)',
            borderStrokeWidth: 3,
          }"
        />
        <v-rect
          ref="selectRectRef"
          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>
</template>

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

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;
  isHoverring: boolean;
  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,
      isHoverring: false,
    });
  }
});

const layerRef = useTemplateRef<VLayer>("layer");
const canvasObjectsRef = useTemplateRef<HTMLTemplateElement[]>("canvasObjects");
const transformerRef = useTemplateRef<VTransformer>("transformer");
const selectRectRef = useTemplateRef<VNode>("selectRect");
const stageRef = 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,
});

const stageScale = ref(1);

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

  const selectedBox = transformerRef.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(layerRef.value)) return;

  if (shouldCache) {
    layerRef.value.getNode().cache();
  } else {
    layerRef.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 (isNull(transformerRef.value)) return;

  const nodes = selectedIds.value.map((id) => {
    if (isNull(layerRef.value)) {
      console.error("layer is null");
      return null;
    }

    for (let node of layerRef.value.getNode().children) {
      if (node instanceof Konva.Group && node.id() === `group-${id}`)
        return node;
    }
  }) as Konva.Node[];

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

// Mouse event handlers
function handleStageClick(e: { target: any; evt: MouseEvent }) {
  if (isNull(e.target)) return;
  const target = e.target as unknown as Konva.Shape | Konva.Group;
  // if we are selecting with rect, do nothing
  if (selectionRectangle.visible) {
    return;
  }

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

  const clickedId = target.attrs.id;

  // do we pressed shift or ctrl?
  const metaPressed = e.evt.shiftKey || e.evt.ctrlKey || e.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 stage = target.getStage();
  const pos = stage?.getPointerPosition();
  if (!isNull(pos) && !isUndefined(pos) && !isNull(stage)) {
    // Convert pointer position to relative coordinates considering scale and position
    const relativePos = {
      x: (pos.x - stage.x()) / stage.scaleX(),
      y: (pos.y - stage.y()) / stage.scaleY(),
    };
    
    selectionRectangle.visible = true;
    selectionRectangle.x1 = relativePos.x;
    selectionRectangle.y1 = relativePos.y;
    selectionRectangle.x2 = relativePos.x;
    selectionRectangle.y2 = relativePos.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 stage = target.getStage();
  const pos = stage?.getPointerPosition();
  if (!isNull(pos) && !isUndefined(pos) && !isNull(stage)) {
    // Convert pointer position to relative coordinates considering scale and position
    const relativePos = {
      x: (pos.x - stage.x()) / stage.scaleX(),
      y: (pos.y - stage.y()) / stage.scaleY(),
    };
    
    selectionRectangle.x2 = relativePos.x;
    selectionRectangle.y2 = relativePos.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),
  };

  let currentSelectedIds = [];
  for (let [key, shape] of objMap) {
    const shapeConfig = objMap.get(shape.id);
    if (isUndefined(shapeConfig)) {
      return;
    }

    if (isUndefined(shapeConfig.box)) {
      if (isUndefined(shapeConfig.box)) {
        if (
          shapeConfig.config.width &&
          shapeConfig.config.height &&
          shapeConfig.config.rotation
        ) {
          shapeConfig.box = calculateRectBounding(
            shapeConfig.config.width,
            shapeConfig.config.height,
            shapeConfig.config.rotation,
            5,
          );
        } else {
          console.error("Could not calculate rect bounding");
          return;
        }
      }
    }

    if (
      Konva.Util.haveIntersection(selBox, {
        x: shapeConfig.box.x + shapeConfig.x,
        y: shapeConfig.box.y + shapeConfig.y,
        width: shapeConfig.box.width,
        height: shapeConfig.box.height,
      })
    )
      currentSelectedIds.push(shapeConfig.id);
  }

  selectedIds.value = currentSelectedIds;
}
function handleWheel(e: { target: any; evt: MouseEvent & { deltaY: number } }) {
  e.evt.preventDefault();

  const stage = e.target as Stage;
  if (stage === null || stage === undefined) {
    console.error("Stage is not defined");
    return;
  }

  const oldScale = stage.scaleX();
  const pointer = stage.getPointerPosition();
  if (pointer === null || pointer === undefined) {
    console.error("Pointer position is not defined");
    return;
  }

  const mousePointTo = {
    x: (pointer.x - stage.x()) / oldScale,
    y: (pointer.y - stage.y()) / oldScale,
  };

  // how to scale? Zoom in? Or zoom out?
  let direction = e.evt.deltaY < 0 ? 1 : -1;

  // when we zoom on trackpad, e.evt.ctrlKey is true
  // in that case lets revert direction
  if (e.evt.ctrlKey) {
    direction = -direction;
  }

  const scaleBy = 1.05;
  const newScale = direction > 0 ? oldScale * scaleBy : oldScale / scaleBy;

  stage.scale({ x: newScale, y: newScale });
  stageScale.value = newScale;

  const newPos = {
    x: pointer.x - mousePointTo.x * newScale,
    y: pointer.y - mousePointTo.y * newScale,
  };
  stage.position(newPos);
}

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)) {
    return;
  }

  // Get clientBox for first time
  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");
  }

  objectConfig.isHoverring = true;
}

function handleCanvasObjectMouseOut(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)) {
    return;
  }

  objectConfig.isHoverring = false;
}
</script>
