<script lang="tsx" setup>
import { renderMarkdownText, renderMermaidProcess } from './plugins/markdown'

import type { CrossTransformFunction, TransformFunction } from './models'
import { defaultMockModelName } from './models'
const pdfPreview = ref(false)
const iframeTitle = ref('')
const iframeURL = ref('')

interface Props {
  reader?: ReadableStreamDefaultReader<Uint8Array> | ReadableStreamDefaultReader<string> | null | undefined
  model: string | null| undefined
  transformStreamFn: TransformFunction | null | undefined
}
const fileLoading = ref(false)

const props = withDefaults(
  defineProps<Props>(),
  {
    reader: null
  }
)


// 定义响应式变量
const displayText = ref('')
const textBuffer = ref('')
const readerLoading = ref(false)

const isAbort = ref(false)

const isCompleted = ref(false)

const emit = defineEmits([
  'failed',
  'completed',
  'update:reader'
])


const refWrapperContent = ref<HTMLElement>()

let typingAnimationFrame: number | null = null

const renderedMarkdown = computed(() => {
  return renderMarkdownText(displayText.value)
})

// 接口响应是否正在排队等待
const waitingForQueue = ref(false)

const WaitTextRender = defineComponent({
  render() {
    return (
      <n-empty
        size="large"
        class="font-bold [&_.n-empty\_\_icon]:flex [&_.n-empty\_\_icon]:justify-center"
      >
        {{
          default: () => (
            <div
              whitespace-break-spaces
              text-center
            >请求排队处理中，请耐心等待...</div>
          ),
          icon: () => (
            <n-icon class="text-30">
              <div class="i-svg-spinners:clock"></div>
            </n-icon>
          )
        }}
      </n-empty>
    )
  }
})

const abortReader = () => {
  if (props.reader) {
    props.reader.cancel()
  }

  isAbort.value = true
  readIsOver.value = false
  emit('update:reader', null)
  initializeEnd()
  isCompleted.value = true
}

const resetStatus = () => {
  isAbort.value = false
  isCompleted.value = false
  readIsOver.value = false

  emit('update:reader', null)

  initializeEnd()
  displayText.value = ''
  textBuffer.value = ''
  readerLoading.value = false
  if (typingAnimationFrame) {
    cancelAnimationFrame(typingAnimationFrame)
    typingAnimationFrame = null
  }
}

/**
 * 检查是否有实际内容
 */
function hasActualContent(html) {
  const text = html.replace(/<[^>]*>/g, '')
  return /\S/.test(text)
}

const showCopy = computed(() => {
  if (!isCompleted.value) return false

  if (hasActualContent(displayText.value)) {
    return true
  }
  return false
})

const renderedContent = computed(() => {
  // 在 renderedMarkdown 末尾插入光标标记
  return `${ renderedMarkdown.value }`
})


const initialized = ref(false)

const initializeStart = () => {
  initialized.value = true
}

const initializeEnd = () => {
  initialized.value = false
}

/**
 * reader 读取是否结束
 */
const readIsOver = ref(false)
const readTextStream = async () => {
  if (!props.reader) return


  const textDecoder = new TextDecoder('utf-8')
  readerLoading.value = true

  while (true) {
    if (isAbort.value) {
      break
    }
    try {
      if (!props.reader) {
        readIsOver.value = true
        break
      }
      const { value, done } = await props.reader.read()
      if (!props.reader) {
        readIsOver.value = true
        break
      }
      if (done) {
        readIsOver.value = true
        break
      }

      const transformer = props.transformStreamFn as CrossTransformFunction
      if (!transformer) {
        break
      }

      const stream = transformer(value, textDecoder)
      if (stream.done) {
        readIsOver.value = true
        break
      }

      if (stream.isWaitQueuing) {
        waitingForQueue.value = stream.isWaitQueuing
      }
      if (stream.content) {
        waitingForQueue.value = false
        textBuffer.value += stream.content
      }

      if (typingAnimationFrame === null) {
        showText()
      }
    } catch (error) {
      readIsOver.value = true
      emit('failed', error)
      resetStatus()
      break
    } finally {
      initializeEnd()
    }
  }
}

const scrollToBottom = async () => {
  await nextTick()
  if (!refWrapperContent.value) return

  refWrapperContent.value.scrollTop = refWrapperContent.value.scrollHeight
  const chatContainer = document.querySelector('.chat-scroll__black')
  if (chatContainer) {
    chatContainer.scrollTop = chatContainer.scrollHeight
  }
}
const scrollToBottomByThreshold = async () => {
  if (!refWrapperContent.value) return

  const threshold = 100
  const distanceToBottom = refWrapperContent.value.scrollHeight - refWrapperContent.value.scrollTop - refWrapperContent.value.clientHeight
  if (distanceToBottom <= threshold) {
    scrollToBottom()
  }
}

/**
 * 通用文件预览函数
 * @param fileUrl - 文件的公网 URL 地址
 */
async function previewFile(fileUrl: string, file_name: string) {
  const ext = fileUrl.split('.').pop()?.toLowerCase().split('?')[0] || ''
  const open = (url: string) => {
    location.href = url
  }

  const isOffice = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext)
  const isText = ['txt', 'csv'].includes(ext)

  if (ext === 'pdf') {
    iframeTitle.value = file_name
    iframeURL.value = `${ location.origin }/pdfJS/web/viewer.html?file=${ encodeURIComponent(fileUrl) }`
    pdfPreview.value = true
  } else if (isOffice) {
    const encoded = encodeURIComponent(fileUrl)
    open(`https://view.officeapps.live.com/op/view.aspx?src=${ encoded }`)
  } else if (isText) {
    open(fileUrl)
  } else {
    // fallback：不支持的格式可以提示或触发下载
    const a = document.createElement('a')
    a.href = fileUrl
    a.download = ''
    a.click()
  }
}

const closePreview = () => {
  iframeTitle.value = ''
  iframeURL.value = ''
  pdfPreview.value = false
}

const scrollToBottomIfAtBottom = async () => {
  // TODO: 需要同时支持手动向上滚动
  scrollToBottomByThreshold()

  const MARGIN = 20

  interface Note {
    content: string
    file_url: string
    file_name: string
  }

  document.querySelectorAll<HTMLElement>('.markdown-wrapper .trigger').forEach(trigger => {
    trigger.addEventListener('click', (e: MouseEvent) => {
      e.stopPropagation()

      const target = e.target as HTMLElement | null
      const position = target?.getAttribute('position')

      const storageData = localStorage.getItem('chatNotes')
      if (!storageData || !position) return

      const notes: Record<string, Note> = JSON.parse(storageData)
      if (!notes[position]) return

      document.querySelector('.note-black')?.remove()

      const html = `
      <div class="note-black">
        <div class="note-black__popover-bg"></div> 
        <div class="note-black__popover">
          <div class="popover-icon">
            <svg t="1751424254143" class="icon" viewBox="0 0 1024 1024" version="1.1"
              xmlns="http://www.w3.org/2000/svg" p-id="4596" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
              <path d="M156.09136 606.57001a457.596822 457.596822 0 0 1 221.680239-392.516385 50.844091 50.844091 0 1 1 50.844091 86.943396 355.90864 355.90864 0 0 0-138.804369 152.532274h16.77855a152.532274 152.532274 0 1 1-152.532274 152.532274z m406.752731 0a457.596822 457.596822 0 0 1 221.680239-392.007944 50.844091 50.844091 0 1 1 50.844091 86.943396 355.90864 355.90864 0 0 0-138.804369 152.532274h16.77855a152.532274 152.532274 0 1 1-152.532274 152.532274z"
                fill="#f0f0f0" p-id="4597"></path>
            </svg>
          </div>
          <div class="popover-content">${ notes[position].content }</div>
          <div class="popover-footer">
            <a href="javascript:;" class="preview-file" data-url="${ notes[position].file_url }" data-name="${ notes[position].file_name }">
              ${ notes[position].file_name }
            </a>
          </div>
        </div>
      </div>`

      const wrapper = document.createElement('div')
      wrapper.innerHTML = html

      const node = wrapper.firstElementChild as HTMLElement | null
      if (!node) return

      document.body.appendChild(node)

      document.querySelector('.note-black__popover-bg')?.addEventListener('click', () => {
        document.querySelector('.note-black')?.remove()
      })
      document.querySelector('.note-black .preview-file')?.addEventListener('click', (event) => {
        const targetA = event.target as HTMLElement | null
        const link = targetA?.getAttribute('data-url') || ''
        const fileName = targetA?.getAttribute('data-name') || ''
        previewFile(link?.split('?')[0], fileName)
        document.querySelector('.note-black')?.remove()
      })

      const container = document.querySelector('.note-black') as HTMLElement | null
      const popover = container?.querySelector('.note-black__popover') as HTMLElement | null
      if (!container || !popover) return

      // 关闭其他弹窗
      document.querySelectorAll<HTMLElement>('.note-black__popover').forEach(p => {
        if (p !== popover) p.style.display = 'none'
      })

      const isVisible = popover.style.display === 'block'
      if (isVisible) {
        popover.style.display = 'none'
        return
      }

      popover.style.display = 'block'
      popover.style.top = ''
      popover.style.bottom = ''
      popover.style.left = ''
      popover.style.right = ''

      const rect = target?.getBoundingClientRect()
      if (!rect) return

      const triggerTop = rect.top
      const triggerLeft = rect.left - 20
      const triggerHeight = trigger.offsetHeight
      const triggerWidth = trigger.offsetWidth
      const popoverWidth = popover.offsetWidth
      const popoverHeight = popover.offsetHeight
      const containerHeight = container.offsetHeight

      // 垂直定位
      if (containerHeight - (triggerTop + triggerHeight) >= popoverHeight + MARGIN) {
      // 下方显示
        popover.style.top = `${ triggerTop + 30 }px`
        popover.style.bottom = 'auto'
      } else if (triggerTop >= popoverHeight + MARGIN) {
      // 上方显示
        popover.style.bottom = `${ containerHeight - triggerTop + 5 }px`
        popover.style.top = 'auto'
      } else {
      // 默认下方
        popover.style.top = `${ triggerTop + 30 }px`
        popover.style.bottom = 'auto'
      }

      // 水平居中定位
      let left = triggerLeft + triggerWidth / 2 - popoverWidth / 2
      if (left < MARGIN) left = MARGIN

      const maxLeft = container.offsetWidth - popoverWidth - MARGIN
      if (left > maxLeft) left = maxLeft

      popover.style.left = `${ left }px`
    })
  })

}

/**
 * 读取 buffer 内容，逐字追加到 displayText
 */
const runReadBuffer = (readCallback = () => {}, endCallback = () => {}) => {
  if (textBuffer.value.length > 0) {
    const nextChunk = textBuffer.value.substring(0, 10)
    displayText.value += nextChunk
    textBuffer.value = textBuffer.value.substring(10)
    readCallback()
  } else {
    endCallback()
  }
}

const showText = () => {
  if (isAbort.value && typingAnimationFrame) {
    cancelAnimationFrame(typingAnimationFrame)
    typingAnimationFrame = null
    readerLoading.value = false
    renderMermaidProcess(scrollToBottom)
    return
  }

  // 若 reader 还没结束，则保持打字行为
  if (!readIsOver.value) {
    runReadBuffer()
    renderMermaidProcess(scrollToBottom)
    typingAnimationFrame = requestAnimationFrame(showText)
  } else {
    // 读取剩余的 buffer
    runReadBuffer(
      () => {
        renderMermaidProcess(scrollToBottom)
        typingAnimationFrame = requestAnimationFrame(showText)
      },
      () => {
        renderMermaidProcess(scrollToBottom)

        // window.$ModalNotification.success({
        //   title: '生成完毕',
        //   duration: 1500
        // })
        emit('update:reader', null)
        emit('completed')
        readerLoading.value = false
        isCompleted.value = true
        nextTick(() => {
          readIsOver.value = false
        })
        typingAnimationFrame = null
      }
    )
  }
  scrollToBottomIfAtBottom()
}
watch(
  () => props.reader,
  () => {
    if (props.reader) {
      readTextStream()
    }
  },
  {
    immediate: true,
    deep: true
  }
)


onUnmounted(() => {
  resetStatus()
})

defineExpose({
  abortReader,
  resetStatus,
  initializeStart,
  initializeEnd
})

const showLoading = computed(() => {
  if (initialized.value) {
    return true
  }

  if (!props.reader) {
    return false
  }

  if (!readerLoading) {
    return false
  }
  if (displayText.value) {
    return false
  }

  return false
})

const refClipBoard = ref()
const handlePassClip = () => {
  if (refClipBoard.value) {
    refClipBoard.value.copyText()
  }
}
</script>

<template>
  <n-spin
    relative
    flex="1 ~"
    min-h-0
    w-full
    h-full
    content-class="w-full h-full flex"
    :show="false"
    :rotate="false"
    class="bg-#fff:30"
    :style="{
      '--n-opacity-spinning': '0.3'
    }"
  >
    <transition name="fade">
      <n-float-button
        v-if="showCopy"
        position="absolute"
        :top="0"
        :right="0"
        color
        class="c-warning bg-#fff/80 hover:bg-#fff/90 transition-all-200 z-2"
        @click="handlePassClip()"
      >
        <clip-board
          ref="refClipBoard"
          :auto-color="false"
          no-copy
          :text="displayText ? displayText.replace(/<[^>]*>/g, '') : ''"
        />
      </n-float-button>
    </transition>
    <template #icon>
      <div class="i-svg-spinners:3-dots-rotate"></div>
    </template>
    <!-- b="~ solid #ddd" -->
    <div
      flex="1 ~"
      min-w-0
      min-h-0
      :class="[
        reader
          ? ''
          : 'justify-center items-center'
      ]"
    >
      <div
        text-16
        class="w-full h-full overflow-hidden"
        :class="[
          !displayText && 'flex items-center justify-center'
        ]"
      >
        <WaitTextRender
          v-if="waitingForQueue && !displayText"
        />
        <template v-else>
          <!-- <n-empty
            v-if="!displayText"
            size="medium"
            :show-icon="false"
          >
            <div
              whitespace-break-spaces
              text-center
              v-html="emptyPlaceholder"
            ></div>
          </n-empty> -->
          <div
            ref="refWrapperContent"
            text-16
            class="w-full h-full overflow-y-auto"
          >
            <div
              class="markdown-wrapper"
              v-html="renderedContent"
            ></div>
            <WaitTextRender
              v-if="waitingForQueue"
            />
            <div
              v-if="readerLoading"
              size-24
              class="i-svg-spinners:pulse-3"
            ></div>
          </div>
        </template>
      </div>
    </div>
    <div
      v-if="fileLoading"
      class="loading-block"
    >
      <div class="loading-items">
        <div class="loading-container">
          <div class="loading-overlay__spinner"></div>
        </div>
        <span class="loading-text">文件加载中...</span>
      </div>
    </div>
    <div
      v-if="pdfPreview"
      class="pdf-black"
    >
      <div
        class="header-block-div"
        @click="closePreview()"
      >
        <span class="back"><svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          aria-hidden="true"
          focusable="false"
          role="presentation"
          class="icon icon-caret"
        >
          <path d="M 7.75 1.34375 L 6.25 2.65625 L 14.65625 12 L 6.25 21.34375 L 7.75 22.65625 L 16.75 12.65625 L 17.34375 12 L 16.75 11.34375 Z" />
        </svg></span>
        <div class="title-black">
          <span class="title">{{ iframeTitle }}</span>
          <span class="subtitle">文件预览</span>
        </div>
      </div>
      <iframe
        :src="iframeURL"
        frameborder="0"
      ></iframe>
    </div>
  </n-spin>
</template>

<style lang="scss">
.markdown-wrapper {

  * {
    padding: 0;
    margin: 0;
  }

  h1 {
    font-size: 2em;
  }

  h2 {
    font-size: 1.5em;
  }

  h3 {
    font-size: 1.25em;
  }

  h4 {
    font-size: 1em;
  }

  h5 {
    font-size: 0.875em;
  }

  h6 {
    font-size: 0.85em;
  }

  h1,h2,h3,h4,h5,h6 {
    margin: 0 auto;
    line-height: 1.25;
  }

  & ul,ol {
    padding-left: 1.5em;
    line-height: 0.8;
  }

  & ul,li,ol {
    list-style-position: outside;
    white-space: normal;
  }

  li {
    line-height: 1.7;

    & > code {
      --at-apply: 'bg-#e5e5e5';
      --at-apply: whitespace-pre m-2px px-6px py-2px rounded-5px;
    }
  }

  ol ol {
    padding-left: 20px;
  }

  ul ul {
    padding-left: 20px;
  }

  hr {
    margin: 16px 0;
  }

  a {
    color: $color-default;
    font-weight: bolder;
    text-decoration: underline;
    padding: 0 3px;
  }

  p {
    line-height: 1.4;

    & > code {
      --at-apply: 'bg-#e5e5e5';
      --at-apply: whitespace-pre mx-4px px-6px py-3px rounded-5px;
    }


    img {
      display: inline-block;
    }
  }

  li > p {
    line-height: 2
  }

  blockquote {
    padding: 10px;
    margin: 20px 0;
    border-left: 5px solid #ccc;
    background-color: #f9f9f9;
    color: #555;

    & > p {
      margin: 0;
    }
  }

  .katex {
    --at-apply: c-primary;
  }

  kbd {
    --at-apply: inline-block align-middle p-0.1em p-0.3em;
    --at-apply: bg-#fcfcfc text-#555;
    --at-apply: border border-solid border-#ccc border-b-#bbb;
    --at-apply: rounded-0.2em shadow-[inset_0_-1px_0_#bbb] text-0.9em;
  }

  table {
    --at-apply: w-fit border-collapse my-16;
  }

  th, td {
    --at-apply: p-7 text-left border border-solid border-#ccc;
  }

  th {
    --at-apply: bg-#f2f2f2 font-bold;
  }

  tr:nth-child(even) {
    --at-apply: bg-#f9f9f9;
  }

  tr:hover {
    --at-apply: bg-#f1f1f1;
  }

  // Deepseek 深度思考 Wrapper

  .think-wrapper {
    --at-apply: pl-13 text-14 c-#8b8b8b;
    --at-apply: b-l-2 b-l-solid b-#e5e5e5;

    p {
      --at-apply: line-height-26;
    }
  }
  .trigger {
    cursor: pointer;
    color: #3BB279;
    margin-left: 5px;
  }
}
.note-black {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 999;
  display: flex;
  justify-content: center;
  align-items: center;

  .note-black__popover-bg {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
  .note-black__popover {
    position: absolute;
    background: #fff;
    border-radius: 6px;
    z-index: 99;
    width: 250px;
    padding: 10px;
    box-shadow: 0 0 10px #ccc;
    span {
      display: block;
    }
    .popover-icon {
      height: 20px;
      svg {
        width: 20px;
        height: 20px;
      }
    }
    .popover-content {
      font-size: 14px;
      margin-bottom: 12px;
    }
    .popover-footer {
      border-top: 1px solid #f0f0f0;
      padding-top: 12px;
      .preview-file {
        color: #3BB279;
        font-size: 14px;
        text-decoration: none;
        padding: 0;
        display: block;
        font-weight: normal;
      }
    }
  }
}
.pdf-black {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 999;
  background: #fff;
  iframe {
    display: block;
    width: 100%;
    height: calc(100% - 44px);
  }
  .header-block-div {
    display: flex;
    align-items: center;
    gap: 5px;
    cursor: pointer;
    width: 100%;
    display: flex;
    padding-left: 16px;
    padding-right: 16px;
    min-height: 44px;
    align-items: center;

    .title-black {
      display: flex;
      flex-direction: column;
      width: calc(100% - 25px);
    }
  }
  .back {
    display: flex;
    align-items: center;

    svg {
      width: 16px;
      height: 16px;
      transform: rotate(180deg);
    }
  }
  .title {
    font-weight: bold;
    font-size: 14px;
    line-height: 1.5;
    white-space: nowrap;
    text-overflow: ellipsis;
    overflow: hidden;
    width: 100%;
    display: block;
  }
  .subtitle {
    font-size: 12px;
    color: #999;
    line-height: 1.5;
  }
  }

</style>
