<template>
  <div
    v-if="!schema.hidden"
    v-bind="$attrs"
    ref="nodeRef"
    :style="getStyle"
    :class="schema.type === 'page' ? '' : 'ignore-click widget-node'"
    :widget-id="schema.id"
    :widget-type="schema.type"
    @click="handleSelect"
    @contextmenu.stop="handleContextmenu"
    :ignore-drag="ignoreDrag && !schema.props?.flags?.includes('LV_OBJ_FLAG_IGNORE_LAYOUT')"
  >
    <component
      v-bind="widgetProps"
      :is="widget"
      :style="{ 'pointer-events': schema.type === 'page' ? 'none' : '' }"
    />
    <div
      v-if="schema.children"
      class="absolute left-0 top-0 w-full h-full"
      :style="{ ...layoutStyle, ...dropStyle }"
    >
      <NodeItem
        v-for="(child, index) in children"
        :key="child.id"
        :ignore-drag="ignoreChildrenDrag"
        :zIndex="schema.children.length - Number(index)"
        :schema="child"
        :rootContainer="nodeRef!"
        :style="childStyle"
      />
    </div>
  </div>
  <ContextMenu
    ref="contextMenuRef"
    :id="schema.id"
    :virtualRef="triggerRef"
    :widgetType="schema.type === 'page' ? 'page' : 'widget'"
  />
</template>

<script setup lang="ts">
import type { BaseWidget } from '@/types/baseWidget'
import type { Page } from '@/types/page'
import type { StageState } from './type'
import type { CSSProperties } from 'vue'

import { computed, inject, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { useDrop, useMouse } from 'vue-hooks-plus'
import { createWidget } from '@/model'
import LvglWidgets from '@/lvgl-widgets'
import { useProjectStore } from '@/store/modules/project'
import { useAppStore } from '@/store/modules/app'
import { useActionStore } from '@/store/modules/action'
import { get, has, isEmpty } from 'lodash-es'

import ContextMenu from './ContextMenu.vue'
import { getAddWidgetIndex, isDescendant } from '@/utils'
import { klona } from 'klona'

defineOptions({
  name: 'NodeItem'
})

const page = inject<Page>('page')
const pageState = inject<StageState>('state')

const props = defineProps<{
  rootContainer: HTMLElement
  schema: BaseWidget | Page
  style?: CSSProperties
  zIndex?: number
  ignoreDrag?: boolean
}>()

const projectStore = useProjectStore()
const appStore = useAppStore()
const actionStore = useActionStore()

const widget = computed(() => LvglWidgets[props.schema.type]?.component)
const nodeRef = ref<HTMLDivElement>()
const zIndex = ref(props.zIndex)
const dropStyle = ref<CSSProperties>({})

const children = computed(() => {
  const { schema } = props

  return has(schema.props, 'activeIndex')
    ? get(schema.children, schema.props.activeIndex)?.children || []
    : schema.children
})

const widgetProps = computed(() => {
  const { schema } = props

  return {
    ...(schema?.props || {}),
    part: schema?.part,
    state: schema?.state,
    styles: schema?.style,
    id: schema?.id,
    ...(has(schema.props, 'activeIndex') && schema.children ? { children: schema.children } : {})
  }
})

const ignoreChildrenDrag = computed(() => {
  const { schema } = props
  return schema.props?.layout === 'flex'
})

const shouldIgnoreLibraryDrop = computed(() => {
  const { schema } = props
  return appStore.libraryDragging && schema.type !== 'page' && !schema.children
})

const getStyle = computed((): CSSProperties => {
  const { style = {}, schema } = props

  const other: CSSProperties = {}
  if (pageState?.showBorder && schema.type !== 'page') {
    other.border = '1px dashed #000'
    other.transform = `translate(${schema.props.x - 1}px, ${schema.props.y - 1}px)`
  }

  let rotate = ''
  if (schema.props?.rotation) {
    rotate = `rotate(${schema.props.rotation}deg)`
    if (schema.props?.pivot) {
      other.transformOrigin = `${schema.props.pivot.x}px ${schema.props.pivot.y}px`
    }

    if (other.transform) {
      other.transform += ` ${rotate}`
    }
  }

  let scale = 1
  if (schema.props?.openScale) {
    scale = (schema.props?.scale ?? 256) / 256
  }

  const parentStyle = klona(style)

  if (schema.props.flags?.includes('LV_OBJ_FLAG_IGNORE_LAYOUT')) {
    delete parentStyle.position
    delete parentStyle.transform
  }

  return {
    position: 'absolute',
    left: 0,
    top: 0,
    transform: `translate(${schema.props.x}px, ${schema.props.y}px) ${rotate}`,
    width: `${schema.props?.width * scale}px`,
    height: `${schema.props?.height * scale}px`,
    zIndex: zIndex.value,
    pointerEvents: shouldIgnoreLibraryDrop.value ? 'none' : undefined,
    ...parentStyle,
    ...other
  }
})

const layoutStyle = ref<CSSProperties>({})
watch(
  () => props.schema.props,
  async () => {
    await nextTick()
    const { schema } = props
    let style: CSSProperties = {}
    layoutStyle.value = {}

    if (schema.props?.layout === 'flex' && schema.props?.flex) {
      const { flex } = schema.props
      const directionMap = {
        row: 'row',
        column: 'column',
        'row-wrap': 'row',
        'column-wrap': 'column',
        'row-reverse': 'row-reverse',
        'column-reverse': 'column-reverse',
        'row-wrap-reverse': 'row-reverse',
        'column-wrap-reverse': 'column-reverse'
      }
      style = {
        display: 'flex',
        overflow: 'hidden',
        flexDirection: directionMap?.[flex?.direction],
        flexWrap: flex?.direction?.includes('wrap') ? 'wrap' : 'nowrap',
        justifyContent: flex?.mainAxisAlign,
        alignItems: flex?.crossAxisAlign,
        alignContent: flex?.trackAxisAlign,
        'row-gap': flex?.rowGap ? `${flex.rowGap}px` : 0,
        'column-gap': flex?.columnGap ? `${flex.columnGap}px` : 0
      }
    }

    const getChildStyle = LvglWidgets[props.schema.type]?.getChildStyle
    if (typeof getChildStyle === 'function') {
      style = {
        ...style,
        ...getChildStyle(props.schema)
      }
    }
    layoutStyle.value = style
  },
  {
    deep: true,
    immediate: true
  }
)

const childStyle = computed((): CSSProperties => {
  const { schema } = props
  if (schema.props?.layout === 'flex') {
    return {
      transform: 'none',
      position: 'relative',
      flexShrink: 0
    }
  }
  return {}
})

const hasLockedWidgetTree = (widget?: BaseWidget | Page): boolean => {
  return (
    !!widget &&
    (!!widget.locked || !!widget.children?.some((child) => hasLockedWidgetTree(child)))
  )
}

const getDropPosition = (event?: DragEvent) => {
  const element = nodeRef.value
  const rect = element?.getBoundingClientRect()
  const logicalWidth = props.schema.props?.width ?? element?.offsetWidth ?? element?.clientWidth
  const logicalHeight = props.schema.props?.height ?? element?.offsetHeight ?? element?.clientHeight
  if (!rect || !logicalWidth || !logicalHeight || !event) {
    return { x: 0, y: 0 }
  }

  const scaleX = rect.width / logicalWidth || 1
  const scaleY = rect.height / logicalHeight || 1

  return {
    x: Math.round((event.clientX - rect.left) / scaleX),
    y: Math.round((event.clientY - rect.top) / scaleY)
  }
}

useDrop(nodeRef, {
  onDom: (content, event) => {
    const { schema } = props

    dropStyle.value = {}
    if (hasLockedWidgetTree(schema)) return
    if (!content || !schema?.children) return

    const { x, y } = getDropPosition(event)
    const index = getAddWidgetIndex(page!, content?.key)
    const newWidget = createWidget(content, index, !!content?.id)
    newWidget.props.x = x
    newWidget.props.y = y

    if (has(schema.props, 'activeIndex')) {
      const parent = get(schema.children, schema.props.activeIndex)
      parent?.children?.push(newWidget)
    } else {
      schema.children?.push(newWidget)
    }

    projectStore.setSelectWidgets([newWidget])
    event?.stopPropagation()
  },
  onDragOver: () => {
    if (props.schema.type !== 'page' && props.schema.children) {
      dropStyle.value = {
        boxShadow: '0px 0px 10px #00fcfc'
      }
    }
  },
  onDragLeave: () => {
    dropStyle.value = {}
  }
})

const nodeState = useMouse(nodeRef)
let dropFlag = false
let offsetX = 0
let offsetY = 0
let mouseupTarget: Element | null = null

const handleDropMouseup = (e: Event) => {
  const mouseEvent = e as MouseEvent
  offsetX = mouseEvent.offsetX
  offsetY = mouseEvent.offsetY
  mouseupTarget = null
}

const clearDropMouseupListener = () => {
  if (!mouseupTarget) return

  mouseupTarget.removeEventListener('mouseup', handleDropMouseup, true)
  mouseupTarget = null
}

const bindDropMouseupListener = (target?: Element) => {
  if (!target || mouseupTarget === target) return

  clearDropMouseupListener()
  mouseupTarget = target
  target.addEventListener('mouseup', handleDropMouseup, {
    once: true,
    capture: true
  })
}

onBeforeUnmount(() => {
  clearDropMouseupListener()
})

watch(nodeState, (state) => {
  const { schema } = props
  if (schema?.children && schema.type !== 'page') {
    const { elementW, elementH, elementX, elementY, clientX, clientY } = state

    if (dropFlag && !appStore.draging) {
      if (hasLockedWidgetTree(schema) || hasLockedWidgetTree(projectStore.activeWidgets[0])) {
        dropFlag = false
        clearDropMouseupListener()
        dropStyle.value = {}
        return
      }

      dropFlag = false
      clearDropMouseupListener()
      const scale = elementW / schema.props.width
      const x = elementX / scale - offsetX
      const y = elementY / scale - offsetY

      projectStore.activeWidgets[0].props.x = x
      projectStore.activeWidgets[0].props.y = y

      const node = klona(projectStore.activeWidgets[0])
      actionStore.onDeleteById(node.id)
      if (has(schema.props, 'activeIndex')) {
        const parent = get(schema.children, schema.props.activeIndex)
        parent?.children?.push(node)
      } else {
        schema.children?.push(node)
      }
      projectStore.activeWidgets = [node]
    }

    if (
      elementX > 0 &&
      elementX < elementW &&
      elementY > 0 &&
      elementY < elementH &&
      appStore.draging &&
      projectStore.activeWidgets.length === 1 &&
      !hasLockedWidgetTree(projectStore.activeWidgets[0]) &&
      !hasLockedWidgetTree(schema) &&
      projectStore.activeWidgets[0].id !== schema.id &&
      !isDescendant(schema, projectStore.activeWidgets[0].id) &&
      schema.type !== 'page'
    ) {
      const elements = document
        .elementsFromPoint(clientX, clientY)
        ?.filter((el) => el.className?.includes?.('widget-node'))
      const targetEl = elements.find((el) => {
        const id = el.attributes.getNamedItem('widget-id')?.value
        return id !== projectStore.activeWidget?.id
      })
      const targetId = targetEl?.attributes.getNamedItem('widget-id')?.value

      if (targetId === schema.id) {
        dropStyle.value = {
          boxShadow: '0px 0px 10px #00fcfc'
        }
        dropFlag = true
        bindDropMouseupListener(elements[0])
      } else if (!isEmpty(dropStyle.value)) {
        dropFlag = false
        clearDropMouseupListener()
        dropStyle.value = {}
      }
    } else if (!isEmpty(dropStyle.value)) {
      dropFlag = false
      clearDropMouseupListener()
      dropStyle.value = {}
    }
  }
})

const handleSelect = (e: MouseEvent) => {
  if (props.schema.type !== 'page') {
    e.stopPropagation()
    if (e.ctrlKey) {
      projectStore.activeWidgets.push(props.schema as BaseWidget)
    } else {
      projectStore.setSelectWidgets([props.schema as BaseWidget])
    }
  } else {
    projectStore.setSelectWidgets([])
  }
  contextMenuRef.value?.handleClose()
}

const position = ref({
  top: 0,
  left: 0,
  bottom: 0,
  right: 0
} as DOMRect)

const contextMenuRef = ref<InstanceType<typeof ContextMenu>>()

const triggerRef = ref({
  getBoundingClientRect: () => position.value
})

const handleContextmenu = (event: MouseEvent) => {
  const { clientX, clientY } = event
  position.value = DOMRect.fromRect({
    x: clientX,
    y: clientY
  })
  event.preventDefault()
  contextMenuRef.value?.handleOpen()

  if (!projectStore.activeWidgets.map((item) => item.id).includes(props.schema.id)) {
    if (props.schema.type !== 'page') {
      if (event.ctrlKey) {
        projectStore.activeWidgets.push(props.schema as BaseWidget)
      } else {
        projectStore.setSelectWidgets([props.schema as BaseWidget])
      }
    }
  }
}
</script>
