<!--
 * @Author: LiZhiWei
 * @Date: 2025-12-26 14:41:26
 * @LastEditors: LiZhiWei
 * @LastEditTime: 2025-12-30 10:22:41
 * @Description: 
-->
<template>
  <div ref="rootRef" class="pptx-preview">
    <div v-if="!slides.length" class="pptx-empty">暂无可渲染的幻灯片</div>
    <div v-else class="pptx-slides">
      <div
        v-for="(slide, slideIndex) in slides"
        :key="slideIndex"
        class="pptx-slide-shell"
        :style="getSlideShellStyle()"
      >
        <div
          class="pptx-slide"
          :style="getSlideStyle(slide)"
        >
          <div
            v-for="(el, elementIndex) in slide.elements"
            :key="getElementKey(el, elementIndex)"
            class="pptx-element"
            :style="getElementStyle(el, elementIndex)"
          >
            <template v-if="isImageElement(el)">
              <div class="pptx-img-wrap" :style="getImageWrapStyle(el)">
                <img
                  class="pptx-img"
                  :src="getMediaSrc(el)"
                  :style="getImageStyle(el)"
                  alt=""
                  draggable="false"
                />
              </div>
            </template>

            <template v-else-if="isVideoElement(el)">
              <video class="pptx-media" :src="getMediaSrc(el)" controls preload="metadata" />
            </template>

            <template v-else-if="isAudioElement(el)">
              <audio class="pptx-media" :src="getMediaSrc(el)" controls preload="metadata" />
            </template>

            <template v-else-if="isMathElement(el)">
              <img class="pptx-img" :src="getMediaSrc(el)" alt="" draggable="false" />
            </template>

            <template v-else-if="isChartElement(el)">
              <svg
                class="pptx-chart"
                :viewBox="`0 0 ${getNumber(el.width)} ${getNumber(el.height)}`"
                preserveAspectRatio="none"
              >
                <template v-if="getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).kind === 'bar'">
                  <rect
                    v-for="(b, i) in getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).bars"
                    :key="i"
                    :x="b.x"
                    :y="b.y"
                    :width="b.w"
                    :height="b.h"
                    :fill="b.fill"
                    :fill-opacity="b.opacity"
                  />
                </template>

                <template v-else-if="getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).kind === 'line'">
                  <path
                    v-for="(p, i) in getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).paths"
                    :key="i"
                    :d="p.d"
                    fill="none"
                    :stroke="p.stroke"
                    :stroke-width="p.strokeWidth"
                    stroke-linejoin="round"
                    stroke-linecap="round"
                  />
                  <circle
                    v-for="(pt, i) in getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).points"
                    :key="i"
                    :cx="pt.cx"
                    :cy="pt.cy"
                    :r="pt.r"
                    :fill="pt.fill"
                    :fill-opacity="pt.opacity"
                  />
                </template>

                <template v-else-if="getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).kind === 'pie'">
                  <path
                    v-for="(s, i) in getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).slices"
                    :key="i"
                    :d="s.d"
                    :fill="s.fill"
                    :fill-opacity="s.opacity"
                  />
                  <circle
                    v-if="getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).holeR"
                    :cx="getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).cx"
                    :cy="getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).cy"
                    :r="getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).holeR"
                    fill="#fff"
                  />
                </template>

                <template v-else-if="getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).kind === 'scatter'">
                  <circle
                    v-for="(pt, i) in getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).points"
                    :key="i"
                    :cx="pt.cx"
                    :cy="pt.cy"
                    :r="pt.r"
                    :fill="pt.fill"
                    :fill-opacity="pt.opacity"
                  />
                </template>
              </svg>
            </template>

            <template v-else-if="isGroupElement(el) || isDiagramElement(el)">
              <PptxGroup
                :el="el"
                :slide-index="slideIndex"
                :cache-id="getElementCacheId(el, elementIndex)"
                :abs-left="getNumber(el.left)"
                :abs-top="getNumber(el.top)"
              />
            </template>

            <template v-else-if="isTableElement(el)">
              <div class="pptx-table-wrap">
                <table class="pptx-table">
                  <colgroup v-if="getTableModel(el, slideIndex, getElementCacheId(el, elementIndex)).colWidths.length">
                    <col
                      v-for="(w, i) in getTableModel(el, slideIndex, getElementCacheId(el, elementIndex)).colWidths"
                      :key="i"
                      :style="{ width: w ? `${w}px` : undefined }"
                    />
                  </colgroup>
                  <tbody>
                    <tr
                      v-for="(row, ri) in getTableModel(el, slideIndex, getElementCacheId(el, elementIndex)).rows"
                      :key="ri"
                      :style="getTableRowStyle(el, slideIndex, getElementCacheId(el, elementIndex), ri)"
                    >
                      <template v-for="(cell, ci) in row" :key="`${ri}-${ci}`">
                        <td
                          v-if="!cell.skip"
                          :colspan="cell.colspan"
                          :rowspan="cell.rowspan"
                          :style="cell.style"
                        >
                          <div class="pptx-table-html" v-html="sanitizeHtml(cell.html)" />
                        </td>
                      </template>
                    </tr>
                  </tbody>
                </table>
              </div>
            </template>

            <template v-else-if="isShapeElement(el)">
              <svg
                class="pptx-shape-svg"
                :viewBox="`0 0 ${getNumber(el.width)} ${getNumber(el.height)}`"
                preserveAspectRatio="none"
              >
                <defs v-if="hasShapeDefs(el, slideIndex, getElementCacheId(el, elementIndex))">
                  <linearGradient
                    v-if="getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))?.kind === 'linear'"
                    :id="getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))!.id"
                    :x1="getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))!.x1"
                    :y1="getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))!.y1"
                    :x2="getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))!.x2"
                    :y2="getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))!.y2"
                  >
                    <stop
                      v-for="(s, i) in getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))!.stops"
                      :key="i"
                      :offset="s.offset"
                      :stop-color="s.color"
                      :stop-opacity="s.opacity"
                    />
                  </linearGradient>

                  <radialGradient
                    v-else-if="getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))?.kind === 'radial'"
                    :id="getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))!.id"
                    cx="50%"
                    cy="50%"
                    r="50%"
                  >
                    <stop
                      v-for="(s, i) in getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))!.stops"
                      :key="i"
                      :offset="s.offset"
                      :stop-color="s.color"
                      :stop-opacity="s.opacity"
                    />
                  </radialGradient>

                  <pattern
                    v-if="getShapePattern(el, slideIndex, getElementCacheId(el, elementIndex))"
                    :id="getShapePattern(el, slideIndex, getElementCacheId(el, elementIndex))!.id"
                    patternUnits="userSpaceOnUse"
                    :width="getShapePattern(el, slideIndex, getElementCacheId(el, elementIndex))!.width"
                    :height="getShapePattern(el, slideIndex, getElementCacheId(el, elementIndex))!.height"
                  >
                    <rect
                      x="0"
                      y="0"
                      :width="getShapePattern(el, slideIndex, getElementCacheId(el, elementIndex))!.width"
                      :height="getShapePattern(el, slideIndex, getElementCacheId(el, elementIndex))!.height"
                      :fill="getShapePattern(el, slideIndex, getElementCacheId(el, elementIndex))!.background"
                    />
                    <template
                      v-for="(shape, i) in getShapePattern(el, slideIndex, getElementCacheId(el, elementIndex))!.shapes"
                      :key="i"
                    >
                      <circle
                        v-if="shape.kind === 'circle'"
                        :cx="shape.cx"
                        :cy="shape.cy"
                        :r="shape.r"
                        :fill="shape.fill"
                      />
                      <polygon
                        v-else-if="shape.kind === 'polygon'"
                        :points="shape.points"
                        :fill="shape.fill"
                      />
                      <rect
                        v-else-if="shape.kind === 'rect'"
                        :x="shape.x"
                        :y="shape.y"
                        :width="shape.width"
                        :height="shape.height"
                        :fill="shape.fill"
                        :transform="shape.transform"
                      />
                    </template>
                  </pattern>

                  <pattern
                    v-if="getShapeImagePaint(el, slideIndex, getElementCacheId(el, elementIndex))"
                    :id="getShapeImagePaint(el, slideIndex, getElementCacheId(el, elementIndex))!.id"
                    patternUnits="objectBoundingBox"
                    width="1"
                    height="1"
                  >
                    <image
                      x="0"
                      y="0"
                      width="100%"
                      height="100%"
                      preserveAspectRatio="none"
                      :href="getShapeImagePaint(el, slideIndex, getElementCacheId(el, elementIndex))!.href"
                    />
                  </pattern>
                </defs>

                <path
                  :d="getShapePath(el)"
                  :fill="getShapeFill(el, slideIndex, getElementCacheId(el, elementIndex))"
                  :stroke="getShapeStroke(el)"
                  :stroke-width="getShapeStrokeWidth(el)"
                  :stroke-dasharray="getShapeDasharray(el)"
                />
              </svg>

              <div
                v-if="typeof el.content === 'string' && el.content.trim()"
                class="pptx-html"
                :style="getHtmlBoxStyle(el)"
              >
                <div
                  class="pptx-html-inner"
                  :style="getHtmlInnerStyle(el)"
                  v-html="sanitizeHtml(el.content)"
                />
              </div>
            </template>

            <template v-else>
              <div
                v-if="typeof el.content === 'string' && el.content.trim()"
                class="pptx-html"
                :style="getHtmlBoxStyle(el)"
              >
                <div
                  class="pptx-html-inner"
                  :style="getHtmlInnerStyle(el)"
                  v-html="sanitizeHtml(el.content)"
                />
              </div>
            </template>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from "vue"
import type { CSSProperties } from "vue"

import type { PptxElement, PptxJson, PptxSize, PptxSlide } from "./elements/pptxTypes"
import { getElementCacheId, getElementKey, getNumber, getPlaceholderKey, isPlaceholderHint, sanitizeHtml } from "./elements/pptxUtils"
import { getElementStyle, getSlideBackground } from "./elements/pptxStyle"
import { getHtmlBoxStyle, getHtmlInnerStyle } from "./elements/pptxText"
import { getImageStyle, getImageWrapStyle, getMediaSrc } from "./elements/pptxMedia"
import { getChartModel } from "./elements/pptxChart"
import { getTableModel, getTableRowStyle } from "./elements/pptxTable"
import { getShapeDasharray, getShapeFill, getShapeGradient, getShapeImagePaint, getShapePath, getShapePattern, getShapeStroke, getShapeStrokeWidth, hasShapeDefs } from "./elements/pptxShape"
import { PptxGroup } from "./elements/pptxGroup"
import { isAudioElement, isChartElement, isDiagramElement, isGroupElement, isImageElement, isMathElement, isShapeElement, isTableElement, isVideoElement } from "./elements/pptxPredicates"

const props = defineProps<{ pptxJson: PptxJson }>()

const rootRef = ref<HTMLDivElement | null>(null)
const containerWidth = ref(0)

let resizeObserver: ResizeObserver | null = null

const size = computed<PptxSize>(() => {
  const w = (props.pptxJson?.size?.width ?? 960) as number
  const h = (props.pptxJson?.size?.height ?? 540) as number
  return {
    width: Number.isFinite(w) ? w : 960,
    height: Number.isFinite(h) ? h : 540,
  }
})

const scale = computed(() => {
  const w = containerWidth.value
  if (!w) return 1
  const raw = w / size.value.width
  return Math.min(1, Math.max(0.1, raw))
})

const slides = computed(() => {
  const rawSlides = Array.isArray(props.pptxJson?.slides) ? props.pptxJson.slides : []
  return rawSlides.map((slide) => {
    const slideElements = (Array.isArray(slide.elements) ? slide.elements : []).map((el) => ({
      ...(el as any),
      __source: "slide" as const,
    }))

    const slidePlaceholderKeys = new Set(slideElements.map(getPlaceholderKey).filter(Boolean))

    const layoutElements = (Array.isArray(slide.layoutElements) ? slide.layoutElements : [])
      .map((el) => {
        const next = { ...(el as any), __source: "layout" as const } as PptxElement
        const key = getPlaceholderKey(next)
        if (key && slidePlaceholderKeys.has(key)) return null
        if (key && typeof next.content === "string" && isPlaceholderHint(next.content)) {
          return { ...(next as any), content: "" } as PptxElement
        }
        return next
      })
      .filter(Boolean) as PptxElement[]

    const merged = [...layoutElements, ...slideElements]

    const elements = merged
      .filter((el) => el && typeof el === "object")
      .map((el, idx) => ({ el: el as PptxElement, idx }))
      .sort((a, b) => {
        const ao = getNumber(a.el.order)
        const bo = getNumber(b.el.order)
        if (ao !== bo) return ao - bo
        return a.idx - b.idx
      })
      .map((it) => it.el)

    return {
      ...slide,
      elements,
    }
  })
})

onMounted(() => {
  if (!rootRef.value) return
  console.log(props.pptxJson)
  const updateWidth = () => {
    if (!rootRef.value) return
    containerWidth.value = rootRef.value.clientWidth
  }
  updateWidth()
  resizeObserver = new ResizeObserver(() => updateWidth())
  resizeObserver.observe(rootRef.value)
})

onBeforeUnmount(() => {
  resizeObserver?.disconnect()
  resizeObserver = null
})

function getSlideShellStyle() {
  return {
    width: `${Math.round(size.value.width * scale.value)}px`,
    height: `${Math.round(size.value.height * scale.value)}px`,
  } as CSSProperties
}

function getSlideStyle(slide: PptxSlide) {
  const background = getSlideBackground(
    (slide as any)?.fill ?? (slide as any)?.backgroundFill ?? (slide as any)?.layoutFill ?? (slide as any)?.layout?.fill ?? (slide as any)?.masterFill
  )
  return {
    width: `${size.value.width}px`,
    height: `${size.value.height}px`,
    transform: `scale(${scale.value})`,
    background,
  } as CSSProperties
}
</script>

<style scoped lang="less">
.pptx-preview {
  width: 100%;
}

.pptx-empty {
  padding: 40px 12px;
  color: #909399;
  text-align: center;
}

.pptx-slides {
  display: flex;
  flex-direction: column;
  gap: 16px;
  align-items: center;
}

.pptx-slide-shell {
  position: relative;
  display: flex;
  align-items: flex-start;
  justify-content: center;
}

.pptx-slide {
  position: absolute;
  left: 0;
  top: 0;
  transform-origin: top left;
  border-radius: 6px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
  overflow: hidden;
}

.pptx-element {
  overflow: visible;
}

.pptx-img,
.pptx-media {
  width: 100%;
  height: 100%;
}

.pptx-media {
  object-fit: contain;
}

.pptx-shape-svg {
  width: 100%;
  height: 100%;
  display: block;
}

.pptx-chart {
  width: 100%;
  height: 100%;
  display: block;
}

.pptx-group-inner {
  width: 100%;
  height: 100%;
  position: relative;
}

.pptx-table-wrap {
  width: 100%;
  height: 100%;
}

.pptx-table {
  width: 100%;
  height: 100%;
  table-layout: fixed;
  border-collapse: collapse;
}

.pptx-table-html {
  width: 100%;
  height: 100%;
  overflow: hidden;
  pointer-events: none;
}

.pptx-html {
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

.pptx-html-inner {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
}

.pptx-html :deep(p) {
  margin: 0;
  padding: 0;
  word-break: break-word;
  text-justify: inter-character;
}

.pptx-html :deep(p[style*="text-align: justify"]),
.pptx-html :deep(p[style*="text-align:justify"]) {
  text-align-last: justify;
}

.pptx-html :deep(ul),
.pptx-html :deep(ol) {
  margin: 0;
  padding: 0 0 0 20px;
  word-break: break-word;
}

.pptx-table-html :deep(p) {
  margin: 0;
  padding: 0;
  word-break: break-word;
  text-justify: inter-character;
}

.pptx-table-html :deep(p[style*="text-align: justify"]),
.pptx-table-html :deep(p[style*="text-align:justify"]) {
  text-align-last: justify;
}

.pptx-table-html :deep(ul),
.pptx-table-html :deep(ol) {
  margin: 0;
  padding: 0 0 0 20px;
  word-break: break-word;
}


:deep(p) {
  margin: 0 !important; 
  padding: 0 !important;
  line-height: 1!important;
  word-break: break-word;
}
</style>
