class Crc32 {
  constructor() {
    this.crc = -1
  }

  append(data) {
    var crc = this.crc | 0; var table = this.table
    for (var offset = 0, len = data.length | 0; offset < len; offset++) {
      crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF]
    }
    this.crc = crc
  }

  get() {
    return ~this.crc
  }
}
Crc32.prototype.table = (() => {
  var i; var j; var t; var table = []
  for (i = 0; i < 256; i++) {
    t = i
    for (j = 0; j < 8; j++) {
      t = (t & 1)
        ? (t >>> 1) ^ 0xEDB88320
        : t >>> 1
    }
    table[i] = t
  }
  return table
})()

const getDataHelper = byteLength => {
  var uint8 = new Uint8Array(byteLength)
  return {
    array: uint8,
    view: new DataView(uint8.buffer)
  }
}

const ZIP_SIGNATURE_LOCAL = 0x04034b50
const ZIP_SIGNATURE_CENTRAL = 0x02014b50
const ZIP_SIGNATURE_EOCD = 0x06054b50
const ZIP_SIGNATURE_ZIP64_EOCD = 0x06064b50
const ZIP_SIGNATURE_ZIP64_LOCATOR = 0x07064b50

const ZIP64_MAGIC = 0xFFFFFFFF

function u16(view, offset, value) { view.setUint16(offset, value, true) }
function u32(view, offset, value) { view.setUint32(offset, value >>> 0, true) }

function concatUint8(chunks, total) {
  const out = new Uint8Array(total)
  let off = 0
  for (const c of chunks) {
    out.set(c, off)
    off += c.length
  }
  return out
}

function createWriter(underlyingSource) {
  const files = Object.create(null)
  const filenames = []
  const encoder = new TextEncoder() // 保持UTF-8编码

  let offset = 0 // bytes written to output so far
  let activeZipIndex = 0
  let ctrl
  let activeZipObject
  let closed = false

  function processNextChunk() {
    if (!activeZipObject) return

    // directory entry: just local header + immediate footer
    if (activeZipObject.directory) {
      if (!activeZipObject.headerWritten) {
        activeZipObject.writeLocalHeader()
        activeZipObject.headerWritten = true
      }
      if (!activeZipObject.footerWritten) {
        activeZipObject.writeDataDescriptor() // will be zeros
        activeZipObject.footerWritten = true
        next()
      }
      return
    }

    if (!activeZipObject.reader) {
      if (!activeZipObject.fileLike || !activeZipObject.fileLike.stream) {
        next()
        return
      }
      activeZipObject.crc = new Crc32()
      activeZipObject.reader = activeZipObject.fileLike.stream().getReader()
      activeZipObject.writeLocalHeader()
      activeZipObject.headerWritten = true
      return
    }

    return activeZipObject.reader.read().then(({ done, value }) => {
      if (done) {
        activeZipObject.writeDataDescriptor()
        activeZipObject.footerWritten = true
        next()
        return
      }
      const chunk = value instanceof Uint8Array ? value : new Uint8Array(value)
      activeZipObject.crc.append(chunk)
      activeZipObject.uncompressedLength += chunk.length
      activeZipObject.compressedLength += chunk.length
      ctrl.enqueue(chunk)
      offset += chunk.length
    })
  }

  function next() {
    activeZipIndex++
    activeZipObject = files[filenames[activeZipIndex]]
    if (activeZipObject) {
      processNextChunk()
    } else if (closed) {
      closeZip()
    }
  }

  function dosDateTime(date) {
    const dt = new Date(date)
    const time = ((dt.getHours() << 11) | (dt.getMinutes() << 5) | (dt.getSeconds() / 2)) & 0xFFFF
    const d = (((dt.getFullYear() - 1980) << 9) | ((dt.getMonth() + 1) << 5) | dt.getDate()) & 0xFFFF
    return { time, date: d }
  }

  const zipWriter = {
    enqueue(fileLike) {
      if (closed) throw new TypeError('Cannot enqueue after close()')

      let name = String(fileLike.name || '').trim()
      if (!name) throw new Error('Missing file name')

      if (fileLike.directory && !name.endsWith('/')) name += '/'
      if (files[name]) throw new Error('File already exists.')

      const nameBuf = encoder.encode(name) // 保持UTF-8编码
      const commentBuf = encoder.encode(fileLike.comment || '')
      const { time, date } = dosDateTime(typeof fileLike.lastModified === 'undefined' ? Date.now() : fileLike.lastModified)

      const zipObject = (files[name] = {
        fileLike,
        directory: !!fileLike.directory,
        nameBuf,
        comment: commentBuf,
        time,
        date,

        // tracked
        offset: 0,
        crc: null,
        compressedLength: 0,
        uncompressedLength: 0,
        headerWritten: false,
        footerWritten: false,
        reader: null,

        // whether sizes/offset require ZIP64
        needsZip64() {
          return (
            this.uncompressedLength > ZIP64_MAGIC ||
            this.compressedLength > ZIP64_MAGIC ||
            this.offset > ZIP64_MAGIC
          )
        },

        writeLocalHeader() {
          this.offset = offset

          // 关键修改：设置通用位标记，第11位（0x0800）表示文件名使用UTF-8编码
          // 保留原0x0008（数据描述符存在），新增0x0800（UTF-8编码）
          const generalPurposeBitFlag = 0x0808 
          const compressionMethod = 0 // store
          const versionNeeded = this.needsZip64() ? 45 : 20 // 适配ZIP64

          // We always use data descriptor; write zeros for crc/sizes in local header
          const local = getDataHelper(30 + this.nameBuf.length)
          u32(local.view, 0, ZIP_SIGNATURE_LOCAL)
          u16(local.view, 4, versionNeeded)
          u16(local.view, 6, generalPurposeBitFlag) // 应用UTF-8标记
          u16(local.view, 8, compressionMethod)
          u16(local.view, 10, this.time)
          u16(local.view, 12, this.date)
          u32(local.view, 14, 0)
          u32(local.view, 18, 0)
          u32(local.view, 22, 0)
          u16(local.view, 26, this.nameBuf.length)
          u16(local.view, 28, 0) // extra length
          local.array.set(this.nameBuf, 30)

          ctrl.enqueue(local.array)
          offset += local.array.length
        },

        writeDataDescriptor() {
          // data descriptor: optional signature + crc32 + sizes (32-bit or 64-bit)
          const crc = this.crc ? this.crc.get() >>> 0 : 0
          const useZip64 = this.needsZip64()

          if (!useZip64) {
            const dd = getDataHelper(16)
            u32(dd.view, 0, 0x08074b50)
            u32(dd.view, 4, crc)
            u32(dd.view, 8, this.compressedLength)
            u32(dd.view, 12, this.uncompressedLength)
            ctrl.enqueue(dd.array)
            offset += dd.array.length
          } else {
            // 24 bytes: sig + crc32 + compSize(8) + uncompSize(8)
            const dd = getDataHelper(24)
            u32(dd.view, 0, 0x08074b50)
            u32(dd.view, 4, crc)
            // BigInt writes
            dd.view.setBigUint64(8, BigInt(this.compressedLength), true)
            dd.view.setBigUint64(16, BigInt(this.uncompressedLength), true)
            ctrl.enqueue(dd.array)
            offset += dd.array.length
          }
        }
      })

      filenames.push(name)

      if (!activeZipObject) {
        activeZipObject = zipObject
        processNextChunk()
      }
    },

    close() {
      if (closed) throw new TypeError('Cannot close twice')
      closed = true
      if (!activeZipObject) closeZip()
    }
  }

  function closeZip() {
    // Build central directory in memory then enqueue once.
    const cdChunks = []
    let cdSize = 0

    for (const name of filenames) {
      const file = files[name]
      const useZip64 = file.needsZip64()

      const versionMadeBy = useZip64 ? 45 : 20
      const versionNeeded = useZip64 ? 45 : 20
      // 关键修改：中央目录也需要设置UTF-8标记
      const generalPurposeBitFlag = 0x0808 
      const compressionMethod = 0

      const crc = file.crc ? file.crc.get() >>> 0 : 0
      const compressed32 = useZip64 ? ZIP64_MAGIC : file.compressedLength
      const uncompressed32 = useZip64 ? ZIP64_MAGIC : file.uncompressedLength
      const offset32 = useZip64 ? ZIP64_MAGIC : file.offset

      // ZIP64 extra field if needed
      let extra = new Uint8Array(0)
      if (useZip64) {
        // headerId(2)=0x0001, dataSize(2)=24, uncompressed(8), compressed(8), offset(8)
        extra = new Uint8Array(4 + 24)
        const ev = new DataView(extra.buffer)
        u16(ev, 0, 0x0001)
        u16(ev, 2, 24)
        ev.setBigUint64(4, BigInt(file.uncompressedLength), true)
        ev.setBigUint64(12, BigInt(file.compressedLength), true)
        ev.setBigUint64(20, BigInt(file.offset), true)
      }

      const headerLen = 46 + file.nameBuf.length + extra.length + file.comment.length
      const cd = getDataHelper(headerLen)

      u32(cd.view, 0, ZIP_SIGNATURE_CENTRAL)
      u16(cd.view, 4, versionMadeBy)
      u16(cd.view, 6, versionNeeded)
      u16(cd.view, 8, generalPurposeBitFlag) // 中央目录应用UTF-8标记
      u16(cd.view, 10, compressionMethod)
      u16(cd.view, 12, file.time)
      u16(cd.view, 14, file.date)
      u32(cd.view, 16, crc)
      u32(cd.view, 20, compressed32)
      u32(cd.view, 24, uncompressed32)
      u16(cd.view, 28, file.nameBuf.length)
      u16(cd.view, 30, extra.length)
      u16(cd.view, 32, file.comment.length)
      u16(cd.view, 34, 0) // disk number start
      u16(cd.view, 36, 0) // internal attrs

      // external file attrs: mark directory
      u32(cd.view, 38, file.directory ? 0x10 : 0)

      u32(cd.view, 42, offset32)

      cd.array.set(file.nameBuf, 46)
      if (extra.length) cd.array.set(extra, 46 + file.nameBuf.length)
      if (file.comment.length) cd.array.set(file.comment, 46 + file.nameBuf.length + extra.length)

      cdChunks.push(cd.array)
      cdSize += cd.array.length
    }

    const centralDirectoryOffset = offset
    const centralDirectory = concatUint8(cdChunks, cdSize)
    ctrl.enqueue(centralDirectory)
    offset += centralDirectory.length

    const entries = filenames.length

    const needsZip64ForArchive =
      entries > 0xFFFF ||
      centralDirectoryOffset > ZIP64_MAGIC ||
      cdSize > ZIP64_MAGIC ||
      offset > ZIP64_MAGIC

    const tailChunks = []
    let tailSize = 0

    if (needsZip64ForArchive) {
      // ZIP64 EOCD
      const zip64eocd = getDataHelper(56)
      u32(zip64eocd.view, 0, ZIP_SIGNATURE_ZIP64_EOCD)
      zip64eocd.view.setBigUint64(4, BigInt(44), true) // remaining size
      u16(zip64eocd.view, 12, 45)
      u16(zip64eocd.view, 14, 45)
      u32(zip64eocd.view, 16, 0)
      u32(zip64eocd.view, 20, 0)
      zip64eocd.view.setBigUint64(24, BigInt(entries), true)
      zip64eocd.view.setBigUint64(32, BigInt(entries), true)
      zip64eocd.view.setBigUint64(40, BigInt(cdSize), true)
      zip64eocd.view.setBigUint64(48, BigInt(centralDirectoryOffset), true)

      // ZIP64 locator
      const locator = getDataHelper(20)
      u32(locator.view, 0, ZIP_SIGNATURE_ZIP64_LOCATOR)
      u32(locator.view, 4, 0)
      locator.view.setBigUint64(8, BigInt(centralDirectoryOffset + cdSize), true) // offset of zip64eocd
      u32(locator.view, 16, 1)

      tailChunks.push(zip64eocd.array, locator.array)
      tailSize += zip64eocd.array.length + locator.array.length
    }

    // EOCD
    const eocd = getDataHelper(22)
    u32(eocd.view, 0, ZIP_SIGNATURE_EOCD)
    u16(eocd.view, 4, 0)
    u16(eocd.view, 6, 0)
    u16(eocd.view, 8, Math.min(entries, 0xFFFF))
    u16(eocd.view, 10, Math.min(entries, 0xFFFF))
    u32(eocd.view, 12, needsZip64ForArchive ? ZIP64_MAGIC : cdSize)
    u32(eocd.view, 16, needsZip64ForArchive ? ZIP64_MAGIC : centralDirectoryOffset)
    u16(eocd.view, 20, 0) // comment length

    tailChunks.push(eocd.array)
    tailSize += eocd.array.length

    ctrl.enqueue(concatUint8(tailChunks, tailSize))
    ctrl.close()
  }

  return new ReadableStream({
    start: c => {
      ctrl = c
      underlyingSource.start && Promise.resolve(underlyingSource.start(zipWriter))
    },
    pull() {
      return processNextChunk() || (
        underlyingSource.pull &&
        Promise.resolve(underlyingSource.pull(zipWriter))
      )
    }
  })
}

window.ZIP = createWriter