import { constants } from '../helper/constants.js';
import {
    type EncodeCursor,
    I8_MASK,
    type TypedArrayType,
    writeNumber,
    writeTypedArray,
    writeTypedArrayData,
    writeTypedArrayHeader,
} from '../helper/encode.js';
import { unsupportedType } from '../helper/errors.js';
import { stringByteLength, encodeInto } from '../helper/string-encoder.js';

const LARGE_DATA_LENGTH = 65536;
const { isArray } = Array;
// eslint-disable-next-line @typescript-eslint/unbound-method
const { isView } = ArrayBuffer;
const { keys: objectKeys } = Object;

/** 编码至 ubjson */
export abstract class EncoderBase {
    /** 序列化对象时排序属性 */
    sortObjectKeys = false;
    /** 当前写指针位置 */
    protected length = 0;
    /** 数据 */
    protected data!: Uint8Array;
    /** buffer 的 DataView */
    protected view!: DataView;
    /**
     * 确保 buffer 还有 capacity 的空闲空间
     */
    protected abstract ensureCapacity(capacity: number): void;

    /** 编码至 ubjson，对于 `undefined` 写入 NOOP */
    protected writeValue(value: unknown): void {
        if (value === undefined) {
            this.ensureCapacity(1);
            this.data[this.length++] = constants.NO_OP;
            return;
        }
        this.write(value);
    }
    /** 写入一个对象 */
    private write(value: unknown): void {
        switch (typeof value) {
            case 'string':
                if (value.length === 1 && value.charCodeAt(0) < 0x80) {
                    // 1 byte ascii char
                    this.ensureCapacity(2);
                    this.data[this.length++] = constants.CHAR;
                    this.data[this.length++] = value.charCodeAt(0);
                } else {
                    this.ensureCapacity(2);
                    this.data[this.length++] = constants.STRING;
                    this.writeStringData(value);
                }
                return;
            case 'number':
                return writeNumber(this as this & EncodeCursor, value);
            case 'object': {
                if (value === null) {
                    this.ensureCapacity(1);
                    this.data[this.length++] = constants.NULL;
                    return;
                }
                if (isArray(value)) {
                    this.ensureCapacity(1);
                    this.data[this.length++] = constants.ARRAY;
                    const size = value.length;
                    for (let index = 0; index < size; index++) {
                        const element = value[index] as unknown;
                        // 在数组中 undefined 和 function 也被视作 null 进行序列化
                        if (element == null || typeof element == 'function') {
                            this.ensureCapacity(1);
                            this.data[this.length++] = constants.NULL;
                        } else {
                            this.write(element);
                        }
                    }
                    this.ensureCapacity(1);
                    this.data[this.length++] = constants.ARRAY_END;
                    return;
                }
                if (isView(value)) {
                    if (value.byteLength <= LARGE_DATA_LENGTH) {
                        writeTypedArray(this as this & EncodeCursor, value);
                        return;
                    }
                    const type = writeTypedArrayHeader(this as this & EncodeCursor, value);
                    this.writeLargeTypedArrayData(type, value);
                    return;
                }
                const { toJSON } = value as Record<string, unknown>;
                if (typeof toJSON == 'function') {
                    this.write(toJSON.call(value));
                    return;
                }
                // 生成稳定的结果以便 hash 计算
                const keys = objectKeys(value);
                const size = keys.length;
                if (this.sortObjectKeys && size > 1) {
                    keys.sort();
                }
                this.ensureCapacity(2 + size);
                this.data[this.length++] = constants.OBJECT;
                for (let index = 0; index < size; index++) {
                    const key = keys[index]!;
                    const element = (value as Record<string, unknown>)[key];
                    if (element === undefined || typeof element == 'function') continue;
                    this.writeStringData(key);
                    this.write(element);
                }
                this.ensureCapacity(1);
                this.data[this.length++] = constants.OBJECT_END;
                return;
            }
            case 'boolean':
                this.ensureCapacity(1);
                this.data[this.length++] = value ? constants.TRUE : constants.FALSE;
                return;
            case 'bigint':
                // int32 range
                if (value >= -2_147_483_648n && value <= 2_147_483_647n) {
                    this.write(Number(value));
                }
                // int64 range
                else if (value >= -9_223_372_036_854_775_808n && value <= 9_223_372_036_854_775_807n) {
                    this.ensureCapacity(9);
                    this.data[this.length++] = constants.INT64;
                    this.view.setBigInt64(this.length, value);
                    this.length += 8;
                } else {
                    throw new RangeError(`BigInt value out of range: ${value}`);
                }
                return;
            default:
                unsupportedType(value);
        }
    }

    /** writeStringData */
    private writeStringData(value: string): void {
        const strLength = value.length;
        if (strLength > LARGE_DATA_LENGTH) {
            return this.writeLargeStringData(value);
        }
        // 对于短字符串，直接计算最大使用空间
        const maxUsage = strLength * 3;
        // 一次性分配 setLength 和 encodeInto 的空间，避免无法回溯
        // 额外分配 3 字节，避免 encodeInto 无法写入最后一个字符
        this.ensureCapacity(maxUsage + 5 + 3);

        // 预估头部大小
        const headerSize = strLength < 0x80 ? 2 : strLength < 0x8000 ? 3 : 5;
        const { length: headerPos, data, view } = this;
        const bufLength = encodeInto(value, data, headerPos + headerSize);
        if (bufLength < 0x80) {
            view.setInt16(headerPos, I8_MASK | bufLength);
            this.length = headerPos + 2 + bufLength;
        } else if (bufLength < 0x8000) {
            if (headerSize < 3) {
                data.copyWithin(headerPos + 3, headerPos + headerSize, headerPos + headerSize + bufLength);
            }
            data[headerPos] = constants.INT16;
            view.setInt16(headerPos + 1, bufLength);
            this.length = headerPos + 3 + bufLength;
        } else {
            if (headerSize < 5) {
                data.copyWithin(headerPos + 5, headerPos + headerSize, headerPos + headerSize + bufLength);
            }
            data[headerPos] = constants.INT32;
            view.setInt32(headerPos + 1, bufLength);
            this.length = headerPos + 5 + bufLength;
        }
    }

    /** 写入大字符串 */
    protected writeLargeStringData(value: string): void {
        const binLen = stringByteLength(value);
        this.ensureCapacity(5);
        this.data[this.length++] = constants.INT32;
        this.view.setInt32(this.length, binLen);
        this.length += 4;
        this.ensureCapacity(binLen);
        encodeInto(value, this.data, this.length);
        this.length += binLen;
    }

    /** 写入数组 */
    protected writeLargeTypedArrayData(type: TypedArrayType, value: ArrayBufferView): void {
        writeTypedArrayData(this as this & EncodeCursor, type, value);
    }
}
