/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import { HalfMaxInt32, HalfMinInt32, Hps, LATIN1, UTF16, UTF8 } from "../type";
import { PlatformBuffer, alloc, strByteLength } from "../platformBuffer";
import { OwnershipError } from "../error";
import { toFloat16, toBFloat16 } from "./number";
import { BFloat16 } from "../bfloat16";

const MAX_POOL_SIZE = 1024 * 1024 * 3; // 3MB

function getInternalStringDetector() {
  try {
    const { isStringOneByteRepresentation } = require("node:v8");
    if (typeof isStringOneByteRepresentation === "function") {
      return isStringOneByteRepresentation;
    }
  } catch { /* not available */ }
  return null;
}

export class BinaryWriter {
  private cursor = 0;
  private byteLength = 0;
  private platformBuffer!: PlatformBuffer;
  private dataView!: DataView;
  private reserved = 0;
  private locked = false;
  private config: {
    hps?: Hps;
  };

  private hpsEnable = false;
  private internalStringDetector: (((content: string) => boolean) | null) = null;

  constructor(config: {
    hps?: Hps;
  } = {}) {
    this.initPoll();
    this.config = config;
    this.hpsEnable = Boolean(config?.hps);
    this.internalStringDetector = getInternalStringDetector();
  }

  private initPoll() {
    this.byteLength = 1024 * 100;
    this.platformBuffer = alloc(this.byteLength);
    this.dataView = new DataView(this.platformBuffer.buffer, this.platformBuffer.byteOffset);
  }

  reserve(len: number) {
    this.reserved += len;
    if (this.byteLength - this.cursor <= this.reserved) {
      const newAb = alloc(this.byteLength * 2 + len);
      this.platformBuffer.copy(newAb, 0);
      this.platformBuffer = newAb;
      this.byteLength = this.platformBuffer.byteLength;
      this.dataView = new DataView(this.platformBuffer.buffer, this.platformBuffer.byteOffset);
    }
  }

  reset() {
    if (this.locked) {
      throw new OwnershipError("Ownership of writer was held by dumpAndOwn, but not released");
    }
    this.cursor = 0;
    this.reserved = 0;
  }

  bool(bool: boolean) {
    this.dataView.setUint8(this.cursor, bool ? 1 : 0);
    this.cursor++;
  }

  writeUint8(v: number) {
    this.dataView.setUint8(this.cursor, v);
    this.cursor++;
  }

  writeInt8(v: number) {
    this.dataView.setInt8(this.cursor, v);
    this.cursor++;
  }

  writeInt24(v: number) {
    this.dataView.setUint32(this.cursor, v, true);
    this.cursor += 3;
  }

  writeUint16(v: number) {
    this.dataView.setUint16(this.cursor, v, true);
    this.cursor += 2;
  }

  writeInt16(v: number) {
    this.dataView.setInt16(this.cursor, v, true);
    this.cursor += 2;
  }

  writeSkip(len: number) {
    this.cursor += len;
  }

  writeInt32(v: number) {
    this.dataView.setInt32(this.cursor, v, true);
    this.cursor += 4;
  }

  writeUint32(v: number) {
    this.dataView.setUint32(this.cursor, v, true);
    this.cursor += 4;
  }

  writeInt64(v: bigint) {
    if (typeof v !== "bigint") {
      this.dataView.setBigInt64(this.cursor, BigInt(v), true);
    } else {
      this.dataView.setBigInt64(this.cursor, v, true);
    }
    this.cursor += 8;
  }

  writeSliInt64(v: bigint | number) {
    if (v <= HalfMaxInt32 && v >= HalfMinInt32) {
      // write:
      // 00xxx -> 0xxx
      // 11xxx -> 1xxx
      // read:
      // 0xxx -> 00xxx
      // 1xxx -> 11xxx
      this.dataView.setUint32(this.cursor, Number(v) << 1, true);
      this.cursor += 4;
    } else {
      const BIG_LONG_FLAG = 0b1; // bit 0 set, means big long.
      this.dataView.setUint8(this.cursor, BIG_LONG_FLAG);
      this.cursor += 1;
      this.writeVarInt64(BigInt(v));
    }
  }

  /**
   * Write signed long using fory Tagged(Small long as int) encoding.
   * If long is in [0xc0000000, 0x3fffffff], encode as 4 bytes int: | little-endian: ((int) value) << 1 |
   * Otherwise write as 9 bytes: | 0b1 | little-endian 8bytes long |
   */
  writeTaggedInt64(value: bigint | number): number {
    if (typeof value !== "bigint") {
      value = BigInt(value);
    }

    const halfMaxInt32 = 0x3fffffffn; // 0x3fffffff
    const halfMinInt32 = -0x40000000n; // 0xc0000000 as signed

    if (value >= halfMinInt32 && value <= halfMaxInt32) {
      // Small long encoded as int
      const v = Number(value) << 1; // bit 0 unset, means int
      this.dataView.setInt32(this.cursor, v, true);
      this.cursor += 4;
      return 4;
    } else {
      // Big long encoded as 8 bytes
      const BIG_LONG_FLAG = 0b1; // bit 0 set, means big long
      this.dataView.setUint8(this.cursor, BIG_LONG_FLAG);
      this.dataView.setBigInt64(this.cursor + 1, value, true);
      this.cursor += 9;
      return 9;
    }
  }

  /**
   * Write unsigned long using fory Tagged(Small long as int) encoding.
   * If long is in [0, 0x7fffffff], encode as 4 bytes int: | little-endian: ((int) value) << 1 |
   * Otherwise write as 9 bytes: | 0b1 | little-endian 8bytes long |
   */
  writeTaggedUInt64(value: bigint | number): number {
    if (typeof value !== "bigint") {
      value = BigInt(value);
    }

    const maxUInt32 = 0x7fffffffn; // 0x7fffffff

    if (value >= 0n && value <= maxUInt32) {
      // Small ulong encoded as uint
      const v = Number(value) << 1; // bit 0 unset, means int
      this.dataView.setUint32(this.cursor, v, true);
      this.cursor += 4;
      return 4;
    } else {
      // Big ulong encoded as 8 bytes
      const BIG_LONG_FLAG = 0b1; // bit 0 set, means big long
      this.dataView.setUint8(this.cursor, BIG_LONG_FLAG);
      this.dataView.setBigUint64(this.cursor + 1, value, true);
      this.cursor += 9;
      return 9;
    }
  }

  writeFloat32(v: number) {
    this.dataView.setFloat32(this.cursor, v, true);
    this.cursor += 4;
  }

  writeFloat64(v: number) {
    this.dataView.setFloat64(this.cursor, v, true);
    this.cursor += 8;
  }

  arrayBuffer(v: ArrayBuffer, byteOffset: number, byteLength: number) {
    this.reserve(byteLength);
    this.platformBuffer.set(new Uint8Array(v, byteOffset, byteLength), this.cursor);
    this.cursor += byteLength;
  }

  buffer(v: ArrayLike<number>) {
    this.reserve(v.length);
    this.platformBuffer.set(v, this.cursor);
    this.cursor += v.length;
  }

  writeUint64(v: bigint) {
    this.dataView.setBigUint64(this.cursor, v, true);
    this.cursor += 8;
  }

  bufferWithoutMemCheck(bf: PlatformBuffer, byteLen: number) {
    bf.copy(this.platformBuffer, this.cursor);
    this.cursor += byteLen;
  }

  fastWriteStringUtf8(string: string, buffer: Uint8Array, offset: number) {
    let c1: number;
    let c2: number;
    for (let i = 0; i < string.length; ++i) {
      c1 = string.charCodeAt(i);
      if (c1 < 128) {
        buffer[offset++] = c1;
      } else if (c1 < 2048) {
        const u1 = (c1 >> 6) | 192;
        const u2 = (c1 & 63) | 128;
        this.dataView.setUint16(offset, (u1 << 8) | u2);
        offset += 2;
      } else if (
        (c1 & 0xfc00) === 0xd800
        && ((c2 = string.charCodeAt(i + 1)) & 0xfc00) === 0xdc00
      ) {
        c1 = 0x10000 + ((c1 & 0x03ff) << 10) + (c2 & 0x03ff);
        ++i;
        const u1 = (c1 >> 18) | 240;
        const u2 = ((c1 >> 12) & 63) | 128;
        const u3 = ((c1 >> 6) & 63) | 128;
        const u4 = (c1 & 63) | 128;
        this.dataView.setUint32(offset, (u1 << 24) | (u2 << 16) | (u3 << 8) | u4);
        offset += 4;
      } else {
        const u1 = (c1 >> 12) | 224;
        const u2 = ((c1 >> 6) & 63) | 128;
        this.dataView.setUint16(offset, (u1 << 8) | u2);
        offset += 2;
        buffer[offset++] = (c1 & 63) | 128;
      }
    }
  }

  stringWithHeaderFast(v: string) {
    const { serializeString } = this.config.hps!;
    this.cursor = serializeString(v, this.platformBuffer, this.cursor);
  }

  //   const header = this.readVarUint36Small();
  // const type = header & 0b11;
  // const len = header >>> 2;

  stringWithHeaderWithDetector(v: string) {
    const isLatin1 = this.internalStringDetector!(v);
    if (isLatin1) {
      const len = v.length;
      this.writeVarUInt32((len << 2) | LATIN1);
      this.reserve(len);
      if (len < 40) {
        for (let index = 0; index < v.length; index++) {
          this.platformBuffer[this.cursor + index] = v.charCodeAt(index);
        }
      } else {
        this.platformBuffer.write(v, this.cursor, "latin1");
      }
      this.cursor += len;
    } else {
      const len = v.length * 2;
      this.writeVarUInt32((len << 2) | UTF16);
      this.reserve(len);
      this.platformBuffer.write(v, this.cursor, "utf16le");
      this.cursor += len;
    }
  }

  stringWithHeaderCompatibly(v: string) {
    const strLen = v.length;
    // Fast inline ASCII check — avoids expensive strByteLength for the common case.
    let isLatin1 = true;
    for (let i = 0; i < strLen; i++) {
      if (v.charCodeAt(i) >= 128) {
        isLatin1 = false;
        break;
      }
    }
    if (isLatin1) {
      this.writeVarUInt32((strLen << 2) | LATIN1);
      this.reserve(strLen);
      if (strLen < 40) {
        for (let index = 0; index < strLen; index++) {
          this.platformBuffer[this.cursor + index] = v.charCodeAt(index);
        }
      } else {
        this.platformBuffer.write(v, this.cursor, "latin1");
      }
      this.cursor += strLen;
    } else {
      const len = strByteLength(v);
      this.writeVarUInt32((len << 2) | UTF8);
      this.reserve(len);
      if (len < 40) {
        this.fastWriteStringUtf8(v, this.platformBuffer, this.cursor);
      } else {
        this.platformBuffer.write(v, this.cursor, "utf8");
      }
      this.cursor += len;
    }
  }

  writeVarInt32(v: number) {
    return this.writeVarUInt32((v << 1) ^ (v >> 31));
  }

  writeVarUInt32(value: number) {
    value = (value >>> 0) & 0xFFFFFFFF; // keep only the lower 32 bits

    if (value >>> 7 == 0) {
      this.platformBuffer[this.cursor++] = value;
      return;
    }
    const rawCursor = this.cursor;
    let u32 = 0;
    if (value >>> 14 == 0) {
      u32 = ((value & 0x7f | 0x80) << 24) | ((value >>> 7) << 16);
      this.cursor += 2;
    } else if (value >>> 21 == 0) {
      u32 = ((value & 0x7f | 0x80) << 24) | ((value >>> 7 & 0x7f | 0x80) << 16) | ((value >>> 14) << 8);
      this.cursor += 3;
    } else if (value >>> 28 == 0) {
      u32 = ((value & 0x7f | 0x80) << 24) | ((value >>> 7 & 0x7f | 0x80) << 16) | ((value >>> 14 & 0x7f | 0x80) << 8) | (value >>> 21);
      this.cursor += 4;
    } else {
      u32 = ((value & 0x7f | 0x80) << 24) | ((value >>> 7 & 0x7f | 0x80) << 16) | ((value >>> 14 & 0x7f | 0x80) << 8) | (value >>> 21 & 0x7f | 0x80);
      this.platformBuffer[rawCursor + 4] = value >>> 28;
      this.cursor += 5;
    }
    this.dataView.setUint32(rawCursor, u32);
  }

  writeVarUint32Small7(value: number) {
    if (value >>> 7 === 0) {
      this.platformBuffer[this.cursor++] = value;
      return;
    }
    this.cursor += this.continueWriteVarUint32Small7(value);
  }

  private continueWriteVarUint32Small7(value: number) {
    let encoded = (value & 0x7F);
    encoded |= (((value & 0x3f80) << 1) | 0x80);
    const writerIdx = this.cursor;
    if (value >>> 14 === 0) {
      this.dataView.setUint32(writerIdx, encoded, true);
      return 2;
    }
    return this.continuePutVarInt36(writerIdx, encoded, value);
  }

  private continuePutVarInt36(index: number, encoded: number, value: number): number {
    // 0x1fc000: 0b1111111 << 14
    encoded |= (((value & 0x1fc000) << 2) | 0x8000);
    if (value >>> 21 === 0) {
      this.dataView.setUint32(index, encoded, true);
      return 3;
    }
    // 0xfe00000: 0b1111111 << 21
    encoded |= ((value & 0xfe00000) << 3) | 0x800000;
    if (value >>> 28 === 0) {
      this.dataView.setUint32(index, encoded, true);
      return 4;
    }
    // 5-byte case: bits 28-31 go to the 5th byte
    const encodedLong = (encoded >>> 0) | 0x80000000;
    const highByte = value >>> 28;
    this.dataView.setUint32(index, encodedLong, true);
    this.dataView.setUint8(index + 4, highByte);
    return 5;
  }

  writeVarInt64(v: bigint) {
    if (typeof v !== "bigint") {
      v = BigInt(v);
    }
    return this.writeVarUInt64((v << 1n) ^ (v >> 63n));
  }

  writeVarUInt64(val: bigint | number) {
    if (typeof val !== "bigint") {
      val = BigInt(val);
    }
    val = val & 0xFFFFFFFFFFFFFFFFn; // keep only the lower 64 bits

    // Match Java's 1-9 byte varuint64 encoding:
    // - 7 bits per byte for the first 8 bytes
    // - the 9th byte (if present) uses full 8 bits, allowing values with the 64th bit set
    for (let i = 0; i < 8; i++) {
      if ((val >> 7n) === 0n) {
        this.platformBuffer[this.cursor++] = Number(val);
        return;
      }
      this.platformBuffer[this.cursor++] = Number((val & 127n) | 128n);
      val >>= 7n;
    }
    this.platformBuffer[this.cursor++] = Number(val & 255n);
  }

  tryFreePool() {
    if (this.byteLength > MAX_POOL_SIZE) {
      this.initPoll();
    }
  }

  dump() {
    const result = alloc(this.cursor);
    this.platformBuffer.copy(result, 0, 0, this.cursor);
    this.tryFreePool();
    return result;
  }

  dumpAndOwn() {
    this.locked = true;
    return {
      get: () => {
        return this.platformBuffer.subarray(0, this.cursor);
      },
      dispose: () => {
        this.locked = false;
      },
    };
  }

  writeFloat16(value: number) {
    this.writeUint16(toFloat16(value));
  }

  writeBfloat16(value: BFloat16 | number) {
    const bits
      = value instanceof BFloat16 ? value.toBits() : toBFloat16(value);
    this.writeUint16(bits);
  }

  writeGetCursor() {
    return this.cursor;
  }

  setUint32Position(offset: number, v: number) {
    this.dataView.setUint32(offset, v, true);
  }

  setUint8Position(offset: number, v: number) {
    this.dataView.setUint8(offset, v);
  }

  setUint16Position(offset: number, v: number) {
    this.dataView.setUint16(offset, v, true);
  }

  getByteLen() {
    return this.byteLength;
  }

  getReserved() {
    return this.reserved;
  }

  stringWithHeader(v: string) {
    if (this.hpsEnable) {
      return this.stringWithHeaderFast(v);
    }
    if (this.internalStringDetector !== null) {
      return this.stringWithHeaderWithDetector(v);
    }
    return this.stringWithHeaderCompatibly(v);
  }
}
