<!--
  会议视频块（Task 11）

  职责：
  - 挂载单个成员的 video/audio MediaStreamTrack（H5 下用原生 <video>/<audio>，因 uni <video> 不支持 srcObject）
  - 视频关闭时回落到头像占位
  - 右下角角标：昵称 + 麦克风静音图标 + 主持人徽章
  - 说话者流光边框（通过 isSpeaking prop 控制，实际探测留给上层）

  平台说明：主要支持 H5 / Web 浏览器，条件编译指令保留但本组件核心能力仅在 H5 生效
-->
<template>
  <view class="tile" :class="{ speaking: isSpeaking, local: isLocal, compact }">
    <view ref="mediaBox" class="media-slot"></view>

    <view v-if="!hasVideo" class="avatar-fallback">
      <view class="avatar-circle" :style="avatarStyle">{{ avatarInitial }}</view>
    </view>

    <view v-if="isLocal" class="tag tag-self">你</view>
    <view v-if="isHost" class="tag tag-host" title="主持人">主持人</view>

    <view class="footer">
      <text class="name">{{ name }}</text>
      <view class="icons">
        <view v-if="!audioEnabled" class="icon icon-mic-off" title="已静音">
          <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <line x1="1" y1="1" x2="23" y2="23"></line>
            <path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"></path>
            <path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"></path>
            <line x1="12" y1="19" x2="12" y2="23"></line>
          </svg>
        </view>
      </view>
    </view>
  </view>
</template>

<script setup>
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'

const props = defineProps({
  userId: { type: [Number, String], required: true },
  name: { type: String, default: '' },
  isLocal: { type: Boolean, default: false },
  isHost: { type: Boolean, default: false },
  audioEnabled: { type: Boolean, default: true },
  videoEnabled: { type: Boolean, default: true },
  audioTrack: { type: Object, default: null },
  videoTrack: { type: Object, default: null },
  isSpeaking: { type: Boolean, default: false },
  compact: { type: Boolean, default: false },
  videoFit: { type: String, default: 'cover' }
})

const mediaBox = ref(null)

const hasVideo = computed(() => !!props.videoTrack && props.videoEnabled)
const normalizedVideoFit = computed(() => props.videoFit === 'contain' ? 'contain' : 'cover')

const avatarInitial = computed(() => {
  const s = (props.name || '').trim()
  return s ? s.charAt(0).toUpperCase() : '?'
})

/** 基于 userId 稳定生成头像背景色（HSL 色相） */
const avatarStyle = computed(() => {
  const seed = String(props.userId || '0')
  let h = 0
  for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) % 360
  return { background: `hsl(${h}, 60%, 48%)` }
})

/** 获取或创建容器下的原生 media 元素 */
const ensureMediaEl = (tagName) => {
  // #ifdef H5
  const parent = resolveDom(mediaBox.value)
  if (!parent) return null
  let el = parent.querySelector(`:scope > ${tagName}`)
  if (!el) {
    el = document.createElement(tagName)
    el.autoplay = true
    el.playsInline = true
    if (tagName === 'video') {
      el.style.width = '100%'
      el.style.height = '100%'
      el.style.objectFit = normalizedVideoFit.value
      el.style.background = '#0B1220'
      el.style.display = 'block'
    }
    parent.appendChild(el)
  }
  return el
  // #endif
  // #ifndef H5
  return null
  // #endif
}

const removeMediaEl = (tagName) => {
  // #ifdef H5
  const parent = resolveDom(mediaBox.value)
  if (!parent) return
  const el = parent.querySelector(`:scope > ${tagName}`)
  if (el) {
    try { el.srcObject = null } catch {}
    parent.removeChild(el)
  }
  // #endif
}

const resolveDom = (refValue) => {
  if (!refValue) return null
  // #ifdef H5
  if (refValue instanceof HTMLElement) return refValue
  if (refValue.$el) return refValue.$el
  if (refValue.exposed && refValue.exposed.$el) return refValue.exposed.$el
  // #endif
  return null
}

// 当前已经贴到 video / audio 元素上的 track，用于幂等判断，避免重复 setSrcObject 触发闪烁
let boundVideoTrack = null
let boundAudioTrack = null

const applyVideo = (retry = 0) => {
  // #ifdef H5
  if (props.videoTrack && props.videoEnabled) {
    const parent = resolveDom(mediaBox.value)
    if (!parent) {
      // DOM 尚未挂载，最多重试 10 次（≈500ms）
      if (retry < 10) {
        setTimeout(() => applyVideo(retry + 1), 50)
      }
      return
    }
    const el = ensureMediaEl('video')
    if (!el) return
    el.style.objectFit = normalizedVideoFit.value
    // 关键：<video> 必须 muted 才能在 Chrome / 微信内核里自动播放。
    // 音频在独立的 <audio> 元素上播放，所以此处 muted 不会让对方静音，
    // 同时彻底解决"进会黑屏，必须点切换按钮才出图"的问题。
    el.muted = true
    el.setAttribute('muted', '')
    el.playsInline = true
    if (boundVideoTrack !== props.videoTrack) {
      el.srcObject = new MediaStream([props.videoTrack])
      boundVideoTrack = props.videoTrack
    }
    const playPromise = el.play && el.play()
    if (playPromise && typeof playPromise.catch === 'function') {
      playPromise.catch(() => {})
    }
  } else {
    removeMediaEl('video')
    boundVideoTrack = null
  }
  // #endif
}

const applyAudio = (retry = 0) => {
  // #ifdef H5
  // 本地 tile 不挂载 audio（避免回声）
  if (!props.isLocal && props.audioTrack && props.audioEnabled) {
    const parent = resolveDom(mediaBox.value)
    if (!parent) {
      if (retry < 10) {
        setTimeout(() => applyAudio(retry + 1), 50)
      }
      return
    }
    const el = ensureMediaEl('audio')
    if (!el) return
    if (boundAudioTrack !== props.audioTrack) {
      el.srcObject = new MediaStream([props.audioTrack])
      boundAudioTrack = props.audioTrack
    }
    const playPromise = el.play && el.play()
    if (playPromise && typeof playPromise.catch === 'function') {
      playPromise.catch(() => {})
    }
  } else {
    removeMediaEl('audio')
    boundAudioTrack = null
  }
  // #endif
}

watch(() => [props.videoTrack, props.videoEnabled, props.videoFit], () => nextTick(() => applyVideo()), { immediate: true })
watch(() => [props.audioTrack, props.audioEnabled, props.isLocal], () => nextTick(() => applyAudio()), { immediate: true })

// 修复刚进入会议时画面不显示、必须点切换才出图的问题：
// 1) watch 的 immediate:true 在 setup 阶段就触发 applyVideo，此时 mediaBox ref 尚未绑定真实 DOM；
// 2) 即便包了 nextTick，在 uni-h5 下 ref 偶发未就绪；
// 现在 applyVideo/applyAudio 在 DOM 未就绪时会自动重试，并在 onMounted 再强制兜底一次。
onMounted(() => {
  nextTick(() => {
    applyVideo()
    applyAudio()
  })
})

onBeforeUnmount(() => {
  removeMediaEl('video')
  removeMediaEl('audio')
})
</script>

<style scoped>
.tile {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 16rpx;
  overflow: hidden;
  background: #0B1220;
  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.25);
  transition: box-shadow 0.18s ease, transform 0.18s ease;
  border: 2rpx solid transparent;
}
/* Fallback：不支持 @property 的浏览器（Safari 17-）降级为静态蓝色描边 */
.tile.speaking {
  border-color: #3B82F6;
  box-shadow: 0 0 0 2rpx rgba(59, 130, 246, 0.18), 0 0 28rpx rgba(59, 130, 246, 0.35);
}

/* EchoChat 原创特色：说话者流光轮廓
   通过 @property 声明可动画化的自定义属性 --flow-angle，
   驱动 conic-gradient 环绕旋转；双层 background 方案：
   - padding-box：实色填充视频块内容
   - border-box：渐变仅生效于 border 区域，形成霓虹光圈效果 */
@supports (background: conic-gradient(from 0deg, red, blue)) and (--flow-angle: 0deg) {
  @property --flow-angle {
    syntax: '<angle>';
    initial-value: 0deg;
    inherits: false;
  }
  .tile.speaking {
    border-color: transparent;
    background:
      linear-gradient(#0B1220, #0B1220) padding-box,
      conic-gradient(from var(--flow-angle), #10B981, #2563EB, #6366F1, #F59E0B, #10B981) border-box;
    box-shadow: 0 0 24rpx rgba(99, 102, 241, 0.45);
    animation: flow-rotate 3s linear infinite;
  }
}
@keyframes flow-rotate {
  to { --flow-angle: 360deg; }
}

/* 可访问性：尊重系统减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
  .tile.speaking { animation: none; }
}
.tile.local::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: 16rpx;
  box-shadow: inset 0 0 0 2rpx rgba(59, 130, 246, 0.45);
}

.media-slot {
  position: absolute;
  inset: 0;
}

.avatar-fallback {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.avatar-circle {
  width: 128rpx;
  height: 128rpx;
  border-radius: 50%;
  color: #FFFFFF;
  font-size: 52rpx;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
  letter-spacing: 0;
  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.3);
}
.tile.compact .avatar-circle {
  width: 80rpx;
  height: 80rpx;
  font-size: 36rpx;
}

.tag {
  position: absolute;
  top: 16rpx;
  padding: 4rpx 12rpx;
  border-radius: 999rpx;
  font-size: 20rpx;
  line-height: 1;
  font-weight: 600;
  letter-spacing: 0.5rpx;
  color: #FFFFFF;
  backdrop-filter: blur(8px);
}
.tag-self {
  left: 16rpx;
  background: rgba(59, 130, 246, 0.7);
}
.tag-host {
  right: 16rpx;
  background: rgba(245, 158, 11, 0.85);
}

.footer {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 14rpx 18rpx;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8rpx;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.65), rgba(0, 0, 0, 0));
}
.name {
  color: #FFFFFF;
  font-size: 24rpx;
  font-weight: 500;
  text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.5);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1;
  min-width: 0;
}
.icons { display: flex; align-items: center; gap: 8rpx; flex-shrink: 0; }
.icon {
  width: 36rpx;
  height: 36rpx;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
}
.icon-mic-off {
  background: rgba(239, 68, 68, 0.9);
  color: #FFFFFF;
}

.tile.compact .footer { padding: 8rpx 12rpx; }
.tile.compact .name { font-size: 20rpx; }
.tile.compact .tag { font-size: 18rpx; padding: 2rpx 8rpx; }

@media (max-width: 750px) {
  .footer {
    left: 12rpx;
    right: 12rpx;
    bottom: 12rpx;
    padding: 8rpx 12rpx;
    border-radius: 14rpx;
    background: rgba(2, 6, 23, 0.62);
    backdrop-filter: blur(10px);
  }
  .name {
    font-size: 20rpx;
  }
  .tile.compact .footer {
    left: 8rpx;
    right: 8rpx;
    bottom: 8rpx;
    padding: 6rpx 10rpx;
  }
}
</style>
