/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed 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 { float32, int32, int64, float32FromBits } from "@koalaui/common"
import { pointer, KUint8ArrayPtr } from "./InteropTypes"
import { KBuffer } from "./buffer"
import { NativeBuffer } from "./NativeBuffer"
import { InteropNativeModule } from "./InteropNativeModule"
import { Tags, CallbackResource } from "./SerializerBase";

export class DeserializerBase {
    private position = 0
    private readonly buffer: KBuffer
    private readonly length: int32
    private static customDeserializers: CustomDeserializer | undefined = new DateDeserializer()

    static registerCustomDeserializer(deserializer: CustomDeserializer) {
        let current = DeserializerBase.customDeserializers
        if (current == undefined) {
            DeserializerBase.customDeserializers = deserializer
        } else {
            while (current!.next != undefined) {
                current = current!.next
            }
            current!.next = deserializer
        }
    }

    constructor(buffer: KUint8ArrayPtr, length: int32) {
        this.buffer = new KBuffer(buffer)
        this.length = length
    }

    static get<T extends DeserializerBase>(
        factory: (args: Uint8Array, length: int32) => T,
        args: Uint8Array, length: int32): T {

        // TBD: Use cache
        return factory(args, length);
    }

    asArray(): KUint8ArrayPtr {
        return this.buffer.buffer
    }

    currentPosition(): int32 {
        return this.position
    }

    resetCurrentPosition(): void {
        this.position = 0
    }

    private checkCapacity(value: int32) {
        if (value > this.length) {
            throw new Error(`${value} is less than remaining buffer length`)
        }
    }

    readInt8(): int32 {
        this.checkCapacity(1)
        const value = this.buffer.get(this.position)
        this.position += 1
        return value
    }

    readInt32(): int32 {
        this.checkCapacity(4)
        let res: int32 = 0;
        for (let i = 0; i < 4; i++) {
            let byteVal = this.buffer.get(this.position + i) as int32;
            byteVal &= 0xff
            res = (res | byteVal << (8 * i)) as int32;
        }
        this.position += 4
        return res
    }

    readPointer(): pointer {
        this.checkCapacity(8)
        let res: int64 = 0;
        for (let i = 0; i < 8; i++) {
            let byteVal = this.buffer.get(this.position + i) as int64;
            byteVal &= 0xff
            res = (res | byteVal << (8 * i)) as int64;
        }
        this.position += 8
        return res
    }

    readInt64(): int64 {
        this.checkCapacity(8)
        let res: int64 = 0;
        for (let i = 0; i < 8; i++) {
            let byteVal = this.buffer.get(this.position + i) as int64;
            byteVal &= 0xff
            res = (res | byteVal << (8 * i)) as int64;
        }
        this.position += 8
        return res
    }

    readFloat32(): float32 {
        this.checkCapacity(4)
        let res: int32 = 0;
        for (let i = 0; i < 4; i++) {
            let byteVal = this.buffer.get(this.position + i) as int32;
            byteVal &= 0xff
            res = (res | byteVal << (8 * i)) as int32;
        }
        this.position += 4
        return float32FromBits(res)
    }

    readBoolean(): boolean {
        this.checkCapacity(1)
        const value = this.buffer.get(this.position)
        this.position += 1
        return value == 1
    }

    readFunction(): int32 {
        // TODO: not exactly correct.
        const id = this.readInt32()
        return id
    }

    // readMaterialized(): object {
    //     const ptr = this.readPointer()
    //     return { ptr: ptr }
    // }

    readCallbackResource(): CallbackResource {
        return ({
            resourceId: this.readInt32(),
            hold: this.readPointer(),
            release: this.readPointer(),
        } as CallbackResource)
    }

    readString(): string {
        const length = this.readInt32()
        this.checkCapacity(length)
        // read without null-terminated byte
        const value = InteropNativeModule._Utf8ToString(this.buffer.buffer, this.position, length)
        this.position += length
        return value
    }

    readCustomObject(kind: string): object {
        let current = DeserializerBase.customDeserializers
        while (current) {
            if (current!.supports(kind)) {
                return current!.deserialize(this, kind)
            }
            current = current!.next
        }
        // consume tag
        const tag = this.readInt8()
        throw Error(`${kind} is not supported`)
    }

    readNumber(): number | undefined {
        const tag = this.readInt8()
        if (tag == Tags.UNDEFINED) {
            return undefined
        } else if (tag == Tags.INT32) {
            return this.readInt32()
        } else if (tag == Tags.FLOAT32) {
            return this.readFloat32()
        } else {
            throw new Error(`Unknown number tag: ${tag}`)
        }
    }

    static lengthUnitFromInt(unit: int32): string {
        let suffix: string
        switch (unit) {
            case 0:
                suffix = "px"
                break
            case 1:
                suffix = "vp"
                break
            case 3:
                suffix = "%"
                break
            case 4:
                suffix = "lpx"
                break
            default:
                suffix = "<unknown>"
        }
        return suffix
    }

    readBuffer(): NativeBuffer {
        /* not implemented */
        const resource = this.readCallbackResource()
        const data = this.readPointer()
        const length = this.readInt64()
        return NativeBuffer.wrap(data, length, resource.resourceId, resource.hold, resource.release)
    }

    readUint8ClampedArray(): Uint8ClampedArray {
        throw new Error("Not implemented")
    }
}

export abstract class CustomDeserializer {
    protected supported: string
    protected constructor(supported_: string) {
        this.supported = supported_
    }

    supports(kind: string): boolean {
        return this.supported.includes(kind)
    }

    abstract deserialize(serializer: DeserializerBase, kind: string): object

    next: CustomDeserializer | undefined = undefined
}

class DateDeserializer extends CustomDeserializer {
    constructor() {
        super("Date")
    }

    deserialize(serializer: DeserializerBase, kind: string): Date {
        return new Date(serializer.readString())
    }
}