import { defineComponent, h } from "vue"
import type { CSSProperties } from "vue"
import type { PptxElement } from "./pptxTypes"
import { getElementCacheId, getNumber, parsePx, sanitizeHtml } from "./pptxUtils"
import { getImageCropRect, getImageStyle, getImageWrapStyle, getMediaSrc } from "./pptxMedia"
import { getHtmlBoxStyle, getHtmlInnerStyle } from "./pptxText"
import { hasImageFill, isAudioElement, isChartElement, isDiagramElement, isGroupElement, isImageElement, isMathElement, isShapeElement, isTableElement, isVideoElement } from "./pptxPredicates"
import { getElementBackground, getElementBorderCss, getElementShadowFilter } from "./pptxStyle"
import { getChartModel } from "./pptxChart"
import { getTableModel, getTableRowStyle } from "./pptxTable"
import { getShapeDasharray, getShapeFill, getShapeGradient, getShapeImagePaint, getShapePath, getShapePattern, getShapeStroke, getShapeStrokeWidth, hasShapeDefs } from "./pptxShape"

function getGroupChildren(el: PptxElement) {
  const candidates = [
    (el as any)?.elements,
    (el as any)?.children,
    (el as any)?.childrens,
    (el as any)?.items,
    (el as any)?.spTree,
    (el as any)?.spTree?.elements,
  ]
  const out: PptxElement[] = []
  candidates.forEach((raw) => {
    if (!Array.isArray(raw)) return
    raw.forEach((it) => {
      if (it && typeof it === "object") out.push(it as PptxElement)
    })
  })
  return out
}

function inferViewBoxFromPath(pathData: string) {
  const nums = String(pathData || "").match(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi)
  if (!nums || nums.length < 2) return null
  let maxX = 0
  let maxY = 0
  for (let i = 0; i + 1 < nums.length; i += 2) {
    const x = Number(nums[i])
    const y = Number(nums[i + 1])
    if (Number.isFinite(x)) maxX = Math.max(maxX, Math.abs(x))
    if (Number.isFinite(y)) maxY = Math.max(maxY, Math.abs(y))
  }
  if (!maxX || !maxY) return null
  return { vbW: maxX, vbH: maxY }
}

function getGroupChildCoordSpace(parent: PptxElement) {
  const off = (parent as any)?.chOff ?? (parent as any)?.childOff ?? (parent as any)?.childOffset ?? null
  const ext = (parent as any)?.chExt ?? (parent as any)?.childExt ?? (parent as any)?.childExtent ?? null
  const offX = getNumber(off?.x ?? off?.left ?? off?.l ?? (parent as any)?.chOffX ?? (parent as any)?.childOffX)
  const offY = getNumber(off?.y ?? off?.top ?? off?.t ?? (parent as any)?.chOffY ?? (parent as any)?.childOffY)
  const extW = getNumber(ext?.cx ?? ext?.w ?? ext?.width ?? (parent as any)?.chExtW ?? (parent as any)?.childExtW)
  const extH = getNumber(ext?.cy ?? ext?.h ?? ext?.height ?? (parent as any)?.chExtH ?? (parent as any)?.childExtH)
  if (!extW || !extH) return null
  return { offX, offY, extW, extH }
}

function getGroupChildStyle(parent: PptxElement, child: PptxElement, childIndex: number, parentAbsLeft: number, parentAbsTop: number) {
  const parentW = Math.max(0, getNumber(parent.width))
  const parentH = Math.max(0, getNumber(parent.height))

  const rawLeft = getNumber(child.left)
  const rawTop = getNumber(child.top)
  const childW = Math.max(0, getNumber(child.width))
  const childH = Math.max(0, getNumber(child.height))

  const isRelative = rawLeft >= -0.5 && rawTop >= -0.5 && rawLeft + childW <= parentW + 0.5 && rawTop + childH <= parentH + 0.5

  const coordSpace = getGroupChildCoordSpace(parent)
  const scaleX = coordSpace && parentW ? parentW / coordSpace.extW : 1
  const scaleY = coordSpace && parentH ? parentH / coordSpace.extH : 1

  const left = isRelative ? rawLeft : coordSpace ? (rawLeft - coordSpace.offX) * scaleX : rawLeft - parentAbsLeft
  const top = isRelative ? rawTop : coordSpace ? (rawTop - coordSpace.offY) * scaleY : rawTop - parentAbsTop

  const width = coordSpace && !isRelative ? childW * scaleX : childW
  const height = coordSpace && !isRelative ? childH * scaleY : childH

  const rotate = getNumber(child.rotate)
  const flipH = !!child.isFlipH
  const flipV = !!child.isFlipV
  const zIndex = getNumber(child.order) || childIndex
  const opacity = child.opacity == null ? 1 : Math.max(0, Math.min(1, getNumber(child.opacity)))

  const transforms: string[] = []
  if (rotate) transforms.push(`rotate(${rotate}deg)`)
  if (flipH) transforms.push(`scaleX(-1)`)
  if (flipV) transforms.push(`scaleY(-1)`)

  const background = getElementBackground(child)
  const border = getElementBorderCss(child)
  const shadowFilter = getElementShadowFilter(child)

  return {
    position: "absolute" as const,
    left: `${left}px`,
    top: `${top}px`,
    width: `${width}px`,
    height: `${height}px`,
    zIndex,
    opacity,
    background,
    border,
    filter: shadowFilter,
    transformOrigin: "center center",
    transform: transforms.length ? transforms.join(" ") : undefined,
  } as CSSProperties
}

type GroupRenderProps = {
  el: PptxElement
  slideIndex: number
  cacheId: string
  absLeft: number
  absTop: number
}

export const PptxGroup = defineComponent<GroupRenderProps>({
  name: "PptxGroup",
  props: {
    el: { type: Object as any, required: true },
    slideIndex: { type: Number, required: true },
    cacheId: { type: String, required: true },
    absLeft: { type: Number, required: true },
    absTop: { type: Number, required: true },
  },
  setup(groupProps) {
    const renderShapeDefs = (el: PptxElement, slideIndex: number, cacheId: string, vbW: number, vbH: number) => {
      if (!hasShapeDefs(el, slideIndex, cacheId)) return null

      const grad = getShapeGradient(el, slideIndex, cacheId)
      const pattern = getShapePattern(el, slideIndex, cacheId)
      const paint = getShapeImagePaint(el, slideIndex, cacheId)

      const nodes: any[] = []
      if (grad?.kind === "linear") {
        nodes.push(
          h(
            "linearGradient",
            { id: grad.id, x1: grad.x1, y1: grad.y1, x2: grad.x2, y2: grad.y2 },
            grad.stops.map((s, i) => h("stop", { key: i, offset: s.offset, "stop-color": s.color, "stop-opacity": s.opacity }))
          )
        )
      } else if (grad?.kind === "radial") {
        nodes.push(
          h(
            "radialGradient",
            { id: grad.id, cx: "50%", cy: "50%", r: "50%" },
            grad.stops.map((s, i) => h("stop", { key: i, offset: s.offset, "stop-color": s.color, "stop-opacity": s.opacity }))
          )
        )
      }

      if (pattern) {
        nodes.push(
          h(
            "pattern",
            { id: pattern.id, patternUnits: "userSpaceOnUse", width: pattern.width, height: pattern.height },
            [
              h("rect", { x: 0, y: 0, width: pattern.width, height: pattern.height, fill: pattern.background }),
              ...pattern.shapes.map((shape, i) => {
                if (shape.kind === "circle") return h("circle", { key: i, cx: shape.cx, cy: shape.cy, r: shape.r, fill: shape.fill })
                if (shape.kind === "polygon") return h("polygon", { key: i, points: shape.points, fill: shape.fill })
                return h("rect", {
                  key: i,
                  x: (shape as any).x,
                  y: (shape as any).y,
                  width: (shape as any).width,
                  height: (shape as any).height,
                  fill: (shape as any).fill,
                  transform: (shape as any).transform,
                })
              }),
            ]
          )
        )
      }

      if (paint) {
        const crop = getImageCropRect(el)
        const cropScaleX = crop ? 1 / (1 - crop.l - crop.r) : 1
        const cropScaleY = crop ? 1 / (1 - crop.t - crop.b) : 1
        const cropTranslateX = crop ? -crop.l * cropScaleX : 0
        const cropTranslateY = crop ? -crop.t * cropScaleY : 0

        nodes.push(
          h(
            "pattern",
            {
              id: paint.id,
              patternUnits: "userSpaceOnUse",
              patternContentUnits: "userSpaceOnUse",
              x: 0,
              y: 0,
              width: vbW,
              height: vbH,
            },
            [
              h("image", {
                x: cropTranslateX * vbW,
                y: cropTranslateY * vbH,
                width: cropScaleX * vbW,
                height: cropScaleY * vbH,
                href: paint.href,
                "xlink:href": paint.href,
                preserveAspectRatio: "none",
              }),
            ]
          )
        )
      }

      return nodes.length ? h("defs", null, nodes) : null
    }

    const renderElement = (child: PptxElement, childIndex: number, parentEl: PptxElement, parentAbsLeft: number, parentAbsTop: number, parentCacheId: string) => {
      const style = getGroupChildStyle(parentEl, child, childIndex, parentAbsLeft, parentAbsTop)
      const cacheId = getElementCacheId(child, childIndex, parentCacheId)
      const left = parsePx((style as any)?.left)
      const top = parsePx((style as any)?.top)
      const absLeft = parentAbsLeft + left
      const absTop = parentAbsTop + top

      const coordSpace = getGroupChildCoordSpace(parentEl)
      const parentW = Math.max(0, getNumber(parentEl.width))
      const parentH = Math.max(0, getNumber(parentEl.height))
      const scaleX = coordSpace && parentW ? parentW / coordSpace.extW : 1
      const scaleY = coordSpace && parentH ? parentH / coordSpace.extH : 1

      if (isImageElement(child)) {
        return h("div", { key: cacheId, class: "pptx-element", style }, [
          h("div", { class: "pptx-img-wrap", style: getImageWrapStyle(child) }, [
            h("img", { class: "pptx-img", src: getMediaSrc(child), style: getImageStyle(child), alt: "", draggable: false }),
          ]),
        ])
      }

      if (isVideoElement(child)) {
        return h("div", { key: cacheId, class: "pptx-element", style }, [h("video", { class: "pptx-media", src: getMediaSrc(child), controls: true, preload: "metadata" })])
      }

      if (isAudioElement(child)) {
        return h("div", { key: cacheId, class: "pptx-element", style }, [h("audio", { class: "pptx-media", src: getMediaSrc(child), controls: true, preload: "metadata" })])
      }

      if (isMathElement(child)) {
        return h("div", { key: cacheId, class: "pptx-element", style }, [h("img", { class: "pptx-img", src: getMediaSrc(child), alt: "", draggable: false })])
      }

      if (isChartElement(child)) {
        const model = getChartModel(child, groupProps.slideIndex, cacheId)
        const nodes: any[] = []

        if (model.kind === "bar") {
          nodes.push(...model.bars.map((b, i) => h("rect", { key: i, x: b.x, y: b.y, width: b.w, height: b.h, fill: b.fill, "fill-opacity": b.opacity })))
        } else if (model.kind === "line") {
          nodes.push(
            ...model.paths.map((p, i) =>
              h("path", { key: i, d: p.d, fill: "none", stroke: p.stroke, "stroke-width": p.strokeWidth, "stroke-linejoin": "round", "stroke-linecap": "round" })
            )
          )
          nodes.push(...model.points.map((pt, i) => h("circle", { key: i, cx: pt.cx, cy: pt.cy, r: pt.r, fill: pt.fill, "fill-opacity": pt.opacity })))
        } else if (model.kind === "pie") {
          nodes.push(...model.slices.map((s, i) => h("path", { key: i, d: s.d, fill: s.fill, "fill-opacity": s.opacity })))
          if (model.holeR) nodes.push(h("circle", { cx: model.cx, cy: model.cy, r: model.holeR, fill: "#fff" }))
        } else if (model.kind === "scatter") {
          nodes.push(...model.points.map((pt, i) => h("circle", { key: i, cx: pt.cx, cy: pt.cy, r: pt.r, fill: pt.fill, "fill-opacity": pt.opacity })))
        }

        return h("div", { key: cacheId, class: "pptx-element", style }, [h("svg", { class: "pptx-chart", viewBox: `0 0 ${getNumber(child.width)} ${getNumber(child.height)}`, preserveAspectRatio: "none" }, nodes)])
      }

      if (isTableElement(child)) {
        const model = getTableModel(child, groupProps.slideIndex, cacheId)
        return h("div", { key: cacheId, class: "pptx-element", style }, [
          h("div", { class: "pptx-table-wrap" }, [
            h("table", { class: "pptx-table" }, [
              model.colWidths.length
                ? h("colgroup", null, model.colWidths.map((w, i) => h("col", { key: i, style: { width: w ? `${w}px` : undefined } })))
                : null,
              h(
                "tbody",
                null,
                model.rows.map((row, ri) =>
                  h(
                    "tr",
                    { key: ri, style: getTableRowStyle(child, groupProps.slideIndex, cacheId, ri) },
                    row
                      .map((cell, ci) => {
                        if (cell.skip) return null
                        return h(
                          "td",
                          { key: `${ri}-${ci}`, colspan: cell.colspan, rowspan: cell.rowspan, style: cell.style },
                          [h("div", { class: "pptx-table-html", innerHTML: sanitizeHtml(cell.html) })]
                        )
                      })
                      .filter(Boolean)
                  )
                )
              ),
            ].filter(Boolean) as any),
          ]),
        ])
      }

      if (isShapeElement(child) || hasImageFill(child)) {
        const sw = Math.max(0, getNumber(child.width))
        const sh = Math.max(0, getNumber(child.height))
        const pathData = getShapePath(child)
        const inferred = (!sw || !sh) && pathData ? inferViewBoxFromPath(pathData) : null
        const vbW = sw || inferred?.vbW || 1
        const vbH = sh || inferred?.vbH || 1

        const defs = renderShapeDefs(child, groupProps.slideIndex, cacheId, vbW, vbH)

        const svg = h(
          "svg",
          {
            class: "pptx-shape-svg",
            viewBox: `0 0 ${vbW} ${vbH}`,
            preserveAspectRatio: "none",
            style: {
              overflow: "visible",
              width: "100%",
              height: "100%",
              display: "block",
            },
          },
          [
            defs,
            h("path", {
              d: pathData,
              fill: getShapeFill(child, groupProps.slideIndex, cacheId),
              stroke: getShapeStroke(child),
              "stroke-width": getShapeStrokeWidth(child),
              "stroke-dasharray": getShapeDasharray(child),
              style: { vectorEffect: "non-scaling-stroke" },
            }),
          ].filter(Boolean) as any
        )

        const html = typeof child.content === "string" && child.content.trim()
        const overlay = html
          ? h(
              "div",
              {
                class: "pptx-html",
                style: {
                  ...getHtmlBoxStyle(child),
                  overflow: "visible",
                  width: `${sw}px`,
                  height: `${sh}px`,
                  transform: scaleX !== 1 || scaleY !== 1 ? `scale(${scaleX}, ${scaleY})` : undefined,
                  transformOrigin: "top left",
                  position: "absolute",
                  left: 0,
                  top: 0,
                },
              },
              [h("div", { class: "pptx-html-inner", style: getHtmlInnerStyle(child), innerHTML: sanitizeHtml(child.content as string) })]
            )
          : null

        return h("div", { key: cacheId, class: "pptx-element", style: { ...style, overflow: "visible" } }, [svg, overlay].filter(Boolean) as any)
      }

      if (isGroupElement(child) || isDiagramElement(child)) {
        return h("div", { key: cacheId, class: "pptx-element", style }, [
          h(PptxGroup as any, { el: child, slideIndex: groupProps.slideIndex, cacheId, absLeft, absTop }),
        ])
      }

      const html = typeof child.content === "string" && child.content.trim()
      return h(
        "div",
        { key: cacheId, class: "pptx-element", style },
        html
          ? [
              h("div", { class: "pptx-html", style: getHtmlBoxStyle(child) }, [
                h("div", { class: "pptx-html-inner", style: getHtmlInnerStyle(child), innerHTML: sanitizeHtml(child.content as string) }),
              ]),
            ]
          : []
      )
    }


    return () => {
      const children = getGroupChildren(groupProps.el)
      return h(
        "div",
        { class: "pptx-group-inner" },
        children.map((child, idx) => renderElement(child, idx, groupProps.el, groupProps.absLeft, groupProps.absTop, groupProps.cacheId))
      )
    }
  },
})
