import type { DecodeOptions } from '../options.js';
import { constants } from './constants.js';
import { UnexpectedEofError, unsupportedType } from './errors.js';
import { decode } from './string-decoder.js';
import { toUint8Array } from './utils.js';
const { defineProperty } = Object;
const { fromCharCode } = String;

/** 数据包装 */
export interface DecodeCursor {
    /** 数据 */
    readonly view: DataView;
    /** 数据 */
    readonly data: Uint8Array;
    /** 当前读指针位置 */
    offset: number;
    /** 读取到末尾时调用 */
    eof(): never;
    /** 选项 */
    readonly options: DecodeOptions | undefined;
}

/** 创建数据包装 */
export function DecodeCursor(data: BufferSource, options?: DecodeOptions): DecodeCursor {
    const d = toUint8Array(data);
    const v = new DataView(d.buffer, d.byteOffset, d.byteLength);
    return {
        data: d,
        view: v,
        offset: 0,
        eof() {
            throw new UnexpectedEofError();
        },
        options,
    };
}

/**
 * 读取第一个非 NOOP 的字节
 * @returns 返回读取到的字节，如果读取到末尾则返回 undefined
 */
export function readMarkerOrUndefined(cursor: DecodeCursor): number | undefined {
    let marker: number | undefined;
    do {
        marker = cursor.data[cursor.offset++];
    } while (marker === constants.NO_OP);
    return marker;
}

/**
 * 读取第一个非 NOOP 的字节
 */
export function readMarker(cursor: DecodeCursor): number {
    let marker: number | undefined;
    do {
        marker = cursor.data[cursor.offset++];
    } while (marker === constants.NO_OP);
    if (marker === undefined) return cursor.eof();
    return marker;
}

/** 读取一个大于 0 的整数 */
export function readLength(cursor: DecodeCursor): number {
    const marker = readMarker(cursor);
    let length: number;
    switch (marker) {
        case constants.INT8:
            length = readInt8Data(cursor);
            break;
        case constants.UINT8:
            length = readUint8Data(cursor);
            break;
        case constants.INT16:
            length = readInt16Data(cursor);
            break;
        case constants.INT32:
            length = readInt32Data(cursor);
            break;
        case constants.INT64: {
            const l = readInt64Data(cursor);
            if (l < 0 || l > Number.MAX_SAFE_INTEGER) {
                throw new Error('Invalid length');
            }
            length = Number(l);
            break;
        }
        default:
            throw new Error(`Unexpected marker '${fromCharCode(marker)}'(${marker}) for int length`);
    }
    if (length < 0) {
        throw new Error('Invalid length');
    }
    return length;
}

/** readInt8Data */
export function readInt8Data(cursor: DecodeCursor): number {
    try {
        return cursor.view.getInt8(cursor.offset++);
    } catch {
        return cursor.eof();
    }
}
/** readUint8Data */
export function readUint8Data(cursor: DecodeCursor): number {
    const result = cursor.data[cursor.offset++];
    if (result === undefined) return cursor.eof();
    return result;
}
/** readInt16Data */
export function readInt16Data(cursor: DecodeCursor): number {
    try {
        const result = cursor.view.getInt16(cursor.offset);
        cursor.offset += 2;
        return result;
    } catch {
        return cursor.eof();
    }
}
/** readInt32Data */
export function readInt32Data(cursor: DecodeCursor): number {
    try {
        const result = cursor.view.getInt32(cursor.offset);
        cursor.offset += 4;
        return result;
    } catch {
        return cursor.eof();
    }
}
/** readInt64Data */
export function readInt64Data(cursor: DecodeCursor): bigint {
    try {
        const result = cursor.view.getBigInt64(cursor.offset);
        cursor.offset += 8;
        return result;
    } catch {
        return cursor.eof();
    }
}
/** readFloat32Data */
export function readFloat32Data(cursor: DecodeCursor): number {
    try {
        const result = cursor.view.getFloat32(cursor.offset);
        cursor.offset += 4;
        return result;
    } catch {
        return cursor.eof();
    }
}
/** readFloat64Data */
export function readFloat64Data(cursor: DecodeCursor): number {
    try {
        const result = cursor.view.getFloat64(cursor.offset);
        cursor.offset += 8;
        return result;
    } catch {
        return cursor.eof();
    }
}

/** 读取数据 */
export function read(cursor: DecodeCursor): unknown {
    const marker = readMarker(cursor);
    return readData(cursor, marker);
}

/** 处理 `__proto__` */
export function protoAction(cursor: Pick<DecodeCursor, 'options'>, obj: Record<string, unknown>, value: unknown): void {
    if (cursor.options?.protoAction === 'error') {
        throw new Error('Unexpected "__proto__"');
    } else if (cursor.options?.protoAction === 'allow') {
        defineProperty(obj, '__proto__', {
            value,
            enumerable: true,
            configurable: true,
            writable: true,
        });
    }
}

/** 处理 `constructor` */
export function constructorAction(
    cursor: Pick<DecodeCursor, 'options'>,
    obj: Record<string, unknown>,
    value: unknown,
): void {
    if (cursor.options?.constructorAction === 'error') {
        throw new Error('Unexpected "constructor"');
    } else if (cursor.options?.constructorAction === 'remove') {
        return;
    }
    // eslint-disable-next-line @typescript-eslint/dot-notation
    obj['constructor'] = value;
}

/** 读取优化对象数据 */
function readObjectOptimizedData(cursor: DecodeCursor, marker: OptimizedFormatMarkers): unknown {
    const { count, type } = marker;
    const object: Record<string, unknown> = {};
    for (let i = 0; i < count; i++) {
        const key = readKey(cursor);
        const value = readData(cursor, type ?? readMarker(cursor));
        if (key === '__proto__') {
            protoAction(cursor, object, value);
            continue;
        }
        if (key === 'constructor') {
            constructorAction(cursor, object, value);
            continue;
        }
        object[key] = value;
    }
    return object;
}

/** 根据标签读取后续数据 */
export function readData(cursor: DecodeCursor, marker: number): unknown {
    // 按照出现频率排序
    switch (marker) {
        case constants.STRING: {
            const length = readLength(cursor);
            const begin = cursor.offset;
            const end = begin + length;
            cursor.offset = end;
            const { data } = cursor;
            if (end > data.length) cursor.eof();
            return decode(data, begin, end);
        }
        case constants.OBJECT: {
            const markers = readOptimizedFormatMarkers(cursor);
            if (markers == null) {
                // 直到 '}'
                const object: Record<string, unknown> = {};
                while (readMarker(cursor) !== constants.OBJECT_END) {
                    cursor.offset--;
                    const key = readKey(cursor);
                    const value = read(cursor);
                    if (key === '__proto__') {
                        protoAction(cursor, object, value);
                        continue;
                    }
                    if (key === 'constructor') {
                        constructorAction(cursor, object, value);
                        continue;
                    }
                    object[key] = value;
                }
                return object;
            }
            return readObjectOptimizedData(cursor, markers);
        }
        case constants.ARRAY: {
            const markers = readOptimizedFormatMarkers(cursor);
            if (markers == null) {
                const array = [];
                for (;;) {
                    const marker = readMarker(cursor);
                    // 直到 ']'
                    if (marker === constants.ARRAY_END) break;
                    array.push(readData(cursor, marker));
                }
                return array;
            }

            const { count, type } = markers;
            switch (type) {
                case constants.UINT8:
                    try {
                        const buf = new Uint8Array(
                            cursor.data.buffer,
                            cursor.data.byteOffset + cursor.offset,
                            count,
                        ).slice();
                        cursor.offset += count;
                        return buf;
                    } catch {
                        return cursor.eof();
                    }
                case constants.INT8:
                    try {
                        const buf = new Int8Array(
                            cursor.data.buffer,
                            cursor.data.byteOffset + cursor.offset,
                            count,
                        ).slice();
                        cursor.offset += count;
                        return buf;
                    } catch {
                        return cursor.eof();
                    }
                case constants.INT16: {
                    const result = new Int16Array(count);
                    for (let i = 0; i < count; i++) {
                        result[i] = readInt16Data(cursor);
                    }
                    return result;
                }
                case constants.INT32: {
                    const result = new Int32Array(count);
                    for (let i = 0; i < count; i++) {
                        result[i] = readInt32Data(cursor);
                    }
                    return result;
                }
                case constants.FLOAT32: {
                    const result = new Float32Array(count);
                    for (let i = 0; i < count; i++) {
                        result[i] = readFloat32Data(cursor);
                    }
                    return result;
                }
                case constants.FLOAT64: {
                    const result = new Float64Array(count);
                    for (let i = 0; i < count; i++) {
                        result[i] = readFloat64Data(cursor);
                    }
                    return result;
                }
                case constants.INT64: {
                    const result = new BigInt64Array(count);
                    for (let i = 0; i < count; i++) {
                        result[i] = readInt64Data(cursor);
                    }
                    return result;
                }
                case constants.NULL:
                    return Array.from({ length: count }).fill(null);
                case constants.TRUE:
                    return Array.from({ length: count }).fill(true);
                case constants.FALSE:
                    return Array.from({ length: count }).fill(false);
                case undefined:
                default:
                    break;
            }
            const array: unknown[] = [];
            array.length = count;
            for (let i = 0; i < count; i++) {
                array[i] = type === undefined ? read(cursor) : readData(cursor, type);
            }
            return array;
        }
        case constants.FLOAT64:
            return readFloat64Data(cursor);
        case constants.UINT8:
            return readUint8Data(cursor);
        case constants.INT16:
            return readInt16Data(cursor);
        case constants.FLOAT32:
            return readFloat32Data(cursor);
        case constants.CHAR:
            return fromCharCode(readUint8Data(cursor));
        case constants.INT32:
            return readInt32Data(cursor);
        case constants.INT8:
            return readInt8Data(cursor);
        case constants.NULL:
            return null;
        case constants.TRUE:
            return true;
        case constants.FALSE:
            return false;
        case constants.INT64: {
            const n = readInt64Data(cursor);
            if (n < Number.MIN_SAFE_INTEGER || n > Number.MAX_SAFE_INTEGER) {
                return n;
            }
            return Number(n);
        }
        case constants.HIGH_PRECISION_NUMBER: {
            const length = readLength(cursor);
            try {
                const _buffer = new Uint8Array(cursor.data.buffer, cursor.offset, length).slice();
                cursor.offset += length;
                // return buffer;
            } catch {
                return cursor.eof();
            }
            unsupportedType('high precision number');
        }
    }
    if (typeof marker != 'number') return marker;
    throw new Error(`Unexpected marker '${fromCharCode(marker)}'(${marker})`);
}

/** readKey */
export function readKey(cursor: DecodeCursor): string {
    const length = readLength(cursor);
    const begin = cursor.offset;
    const end = begin + length;
    cursor.offset = end;
    const { data } = cursor;
    if (end > data.length) cursor.eof();
    return decode(data, begin, end);
}

/** Optimized Format 数据 */
export type OptimizedFormatMarkers = { type?: number; count: number };
/** 读取 Optimized Format 数据 */
export function readOptimizedFormatMarkers(cursor: DecodeCursor): OptimizedFormatMarkers | undefined {
    let type;
    let count;
    switch (readMarker(cursor)) {
        case constants.TYPE_MARKER:
            type = readMarker(cursor);
            if (readMarker(cursor) !== constants.COUNT_MARKER) {
                throw new Error('Expected count marker');
            }
        /* fall through */
        case constants.COUNT_MARKER:
            count = readLength(cursor);
            break;
        default:
            // 不是 '$' 或 '#'，回溯
            cursor.offset--;
            return undefined;
    }
    return { type, count };
}
