<template>
  <MapTiles ref="mapTiles" @createCommand="addCommand" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
  <PlacedMapObjects ref="mapObjects" @update="updateMapObjects" @updateAndCommit="updateAndCommit" @pauseObjectTracking="pause" @resumeObjectTracking="resume" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
  <MapEventTiles ref="eventTiles" @createCommand="addCommand" v-if="tileMap" :tileMap />
</template>

<script setup lang="ts">
import type { MapEventTile, Map as MapT, PlacedMapObject as PlacedMapObjectT } from '@/application/types'
import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue'
import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue'
import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { cloneArray, createTileArray, createTileLayer, createTileMap, placeTiles } from '@/services/mapService'
import { TileStorage } from '@/storage/storages'
import { useManualRefHistory, useRefHistory } from '@vueuse/core'
import { useScene } from 'phavuer'
import { onBeforeUnmount, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue'

const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()

const mapEditor = useMapEditorComposable()
const scene = useScene()

const mapTiles = useTemplateRef('mapTiles')
const mapObjects = useTemplateRef('mapObjects')
const eventTiles = useTemplateRef('eventTiles')

//Record of commands
let commandStack: (EditorCommand | number)[] = []
let commandIndex = ref(0)

let originTiles: string[][] = []
let originEventTiles: MapEventTile[] = []
let originObjects = ref<PlacedMapObjectT[]>(mapEditor.currentMap.value.placedMapObjects)

const { undo, redo, commit, pause, resume, canUndo, canRedo } = useRefHistory(originObjects, { deep: true, capacity: 9 })

//Command Pattern basic interface, extended to store what elements have been changed by each edit
export interface EditorCommand {
  apply: (elements: any[]) => any[]
  type: 'tile' | 'map_object' | 'event_tile'
  operation: 'draw' | 'erase' | 'clear'
}

function applyCommands(tiles: any[], ...commands: EditorCommand[]): any[] {
  let tileVersion = cloneArray(tiles)
  for (let command of commands) {
    tileVersion = command.apply(tileVersion)
  }
  return tileVersion
}

watch(
  () => mapEditor.shouldClearTiles.value,
  (shouldClear) => {
    if (shouldClear && mapEditor.currentMap.value) {
      mapTiles.value!.clearTiles()
      eventTiles.value!.clearTiles()
      mapEditor.currentMap.value.placedMapObjects = []
      updateAndCommit(mapEditor.currentMap.value)
      mapEditor.resetClearTilesFlag()
    }
  }
)

function update(commands: (EditorCommand | number)[]) {
  if (!mapEditor.currentMap.value) return

  if (commandStack.length >= 9) {
    if (typeof commandStack[0] !== 'number') {
      const base = commandStack.shift() as EditorCommand
      if (base.operation !== 'clear') {
        switch (base.type) {
          case 'tile':
            originTiles = base.apply(originTiles) as string[][]
            break
          case 'event_tile':
            originEventTiles = base.apply(originEventTiles) as MapEventTile[]
            break
        }
      } else {
        commandStack.shift()
      }
    } else if (typeof commandStack[0] === 'number') {
      commandStack.shift()
    }
  }

  let tileCommands = commands.filter((item) => typeof item !== 'number' && item.type === 'tile') as EditorCommand[]
  let eventTileCommands = commands.filter((item) => typeof item !== 'number' && item.type === 'event_tile') as EditorCommand[]

  let modifiedTiles = applyCommands(originTiles, ...tileCommands)
  placeTiles(tileMap.value!, tileMapLayer.value!, modifiedTiles)

  let eventTiles = applyCommands(originEventTiles, ...eventTileCommands)

  mapEditor.currentMap.value.tiles = modifiedTiles
  mapEditor.currentMap.value.mapEventTiles = eventTiles
  mapEditor.currentMap.value.placedMapObjects = originObjects.value
}

function updateMapObjects(map: MapT) {
  originObjects.value = map.placedMapObjects
}

function updateAndCommit(map?: MapT) {
  commandStack = commandStack.slice(0, commandIndex.value)
  if (map) updateMapObjects(map)
  commit()
  commandStack.push(0)
  commandIndex.value = commandStack.length
}

function addCommand(command: EditorCommand) {
  commandStack = commandStack.slice(0, commandIndex.value)
  commandStack.push(command)
  commandIndex.value = commandStack.length
}

function undoEdit() {
  if (commandIndex.value > 0) {
    if (typeof commandStack[--commandIndex.value] === 'number' && canUndo) {
      undo()
    }
    update(commandStack.slice(0, commandIndex.value))
  }
}

function redoEdit() {
  if (commandIndex.value <= 9 && commandIndex.value < commandStack.length) {
    if (typeof commandStack[commandIndex.value++] === 'number' && canRedo) {
      redo()
    }
    update(commandStack.slice(0, commandIndex.value))
  }
}

function handlePointerDown(pointer: Phaser.Input.Pointer) {
  if (!mapTiles.value || !mapObjects.value || !eventTiles.value) return

  // Check if left mouse button is pressed
  if (!pointer.isDown) return

  // Check if shift is not pressed, this means we are moving the camera
  if (pointer.event.shiftKey) return

  // Check if draw mode is tile
  switch (mapEditor.drawMode.value) {
    case 'tile':
      mapTiles.value.handlePointer(pointer)
      break
    case 'map_object':
      mapObjects.value.handlePointer(pointer)
      break
    case 'teleport':
      eventTiles.value.handlePointer(pointer)
      break
    case 'blocking tile':
      eventTiles.value.handlePointer(pointer)
      break
  }
}

function handleKeyDown(event: KeyboardEvent) {
  //CTRL+Y
  if (event.key === 'y' && event.ctrlKey) {
    redoEdit()
  }

  //CTRL+Z
  if (event.key === 'z' && event.ctrlKey) {
    undoEdit()
  }
}

function handlePointerMove(pointer: Phaser.Input.Pointer) {
  if (mapEditor.inputMode.value === 'hold' && pointer.isDown) {
    handlePointerDown(pointer)
  }
}

function handlePointerUp(pointer: Phaser.Input.Pointer) {
  switch (mapEditor.drawMode.value) {
    case 'tile':
      mapTiles.value!.finalizeCommand()
      break
    case 'map_object':
      if (mapEditor.tool.value === 'pencil' || mapEditor.tool.value === 'eraser') {
        resume()
        updateAndCommit()
      }
      break
    case 'teleport':
      eventTiles.value!.finalizeCommand()
      break
    case 'blocking tile':
      eventTiles.value!.finalizeCommand()
      break
  }
}

onMounted(async () => {
  let mapValue = mapEditor.currentMap.value
  if (!mapValue) return

  //Clone
  originTiles = cloneArray(mapValue.tiles)
  originEventTiles = cloneArray(mapValue.mapEventTiles)

  const tileStorage = new TileStorage()
  const allTiles = await tileStorage.getAll()
  const allTileIds = allTiles.map((tile) => tile.id)

  tileMap.value = createTileMap(scene, mapValue)
  tileMapLayer.value = createTileLayer(tileMap.value, allTileIds)

  addEventListener('keydown', handleKeyDown)
  scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
  scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
  scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
})

onUnmounted(() => {
  if (tileMap.value) {
    tileMap.value.destroyLayer('tiles')
    tileMap.value.removeAllLayers()
    tileMap.value.destroy()
  }

  scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
  scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
  scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
  mapEditor.reset()
})

onBeforeUnmount(() => {
  removeEventListener('keydown', handleKeyDown)
})
</script>
