/*
 * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
 *
 * 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.
 *
 */

package net.sergeych.lynon

import net.sergeych.bintools.ByteChunk
import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.*

enum class LynonType(val objClass: ObjClass, val defaultFrequency: Int = 1) {
    Null(ObjNull.objClass, 80),
    Int0(ObjInt.type, 70),
    IntNegative(ObjInt.type, 50),
    IntPositive(ObjInt.type, 100),
    IntSigned(ObjInt.type, 30),
    String(ObjString.type, 100),
    Real(ObjReal.type),
    Bool(ObjBool.type, 80),
    List(ObjList.type, 70),
    Map(ObjMap.type, 40),
    Set(ObjSet.type),
    Buffer(ObjBuffer.type, 50),
    Instant(ObjInstant.type, 30),
    Duration(ObjDuration.type),
    Other(Obj.rootObjectType, 60),
    Date(ObjDate.type, 20);

    fun generalizeTo(other: LynonType): LynonType? {
        if (this == other) return this
        return (if (this.isInt && other.isInt) {
            when {
                this == Int0 -> other // upgrade 0 to some other int
                other == Int0 -> this // 0 is member of our class, ignore
                // different signum propagate to signed
                else -> IntSigned
            }
        } else
        // impossible to generalize
            null
                ).also { println("Gen $this + $other -> $it") }
    }

    val isInt by lazy {
        when (this) {
            Int0, IntSigned, IntPositive, IntNegative -> true
            else -> false
        }
    }

}

open class LynonEncoder(val bout: BitOutput, val settings: LynonSettings = LynonSettings.default) {

    val cache = mutableMapOf<Any, Int>()

    suspend fun encodeCached(item: Any, packer: suspend LynonEncoder.() -> Unit) {

        suspend fun serializeAndCache(key: Any = item) {
            cache[key]?.let { cacheId ->
//                println("encode: Cache hit: ${cacheId}: $item: ${item::class.simpleName}")
                val size = sizeInBits(cache.size)
                bout.putBit(1)
                bout.putBits(cacheId.toULong(), size)
            } ?: run {
                bout.putBit(0)
                if (settings.shouldCache(item)) {
//                    println("encode add cache: ${cache.size}: $item: ${item::class.simpleName}")
                    packer()
                    cache[key] = cache.size
                } else {
//                    println("encode but not cache $item")
                    packer()
                }
            }
        }

        when (item) {
            is ByteArray -> serializeAndCache(ByteChunk(item.asUByteArray()))
            is UByteArray -> serializeAndCache(ByteChunk(item))
            else -> serializeAndCache(item)
        }
    }

    /**
     * Encode any Lyng object [Obj], which can be serialized, using type record. This allow to
     * encode any object with the overhead of type record.
     *
     * Caching is used automatically.
     */
    suspend fun encodeAny(scope: Scope, obj: Obj) {
        if (obj.objClass.isAnonymous) scope.raiseError("Cannot serialize anonymous object")
        encodeCached(obj) {
            val type = putTypeRecord(scope, obj, obj.lynonType())
            obj.serialize(scope, this, type)
        }
    }

    private suspend fun putTypeRecord(scope: Scope, obj: Obj, type: LynonType): LynonType {
        putType(type)
        if( type == LynonType.Other) {
            encodeObject(scope, obj.objClass.classNameObj)
        }
        return type
    }

    private fun putType(type: LynonType) {
        bout.putBits(type.ordinal.toULong(), 4)
    }

    /**
     * AnyList could be homogenous (first bit=1) and heterogeneous (first bit=0). Homogenous list
     * has a single type record that precedes the list, heterogeneous hash typed record
     * for each item.
     *
     */
    suspend fun encodeAnyList(scope: Scope, list: List<Obj>,fixedSize: Boolean = false) {
        if( list.isEmpty()) {
            // Empty list: we still need to write the heterogenous/homogenous flag bit
            // but must NOT write the size when fixedSize is provided, as decoder will
            // use the provided size and won't read it from the stream.
            putBit(0) // treat empty lists as heterogeneous
            if (!fixedSize) encodeUnsigned(0u)
        }
        else {
            val objClass = list[0].objClass
            var type = list[0].lynonType()
            var isHomogeneous = true
            for (i in list.drop(1))
                if (i.objClass != objClass) {
                    isHomogeneous = false
                    break
                } else {
                    // same class but type might need generalization
                    type = type.generalizeTo(i.lynonType())
                        ?: scope.raiseError("inner error: can't generalize lynon type $type to ${i.lynonType()}")
                }
            if (isHomogeneous) {
                putBit(1)
                putTypeRecord(scope, list[0], type)
                if (!fixedSize)
                    encodeUnsigned(list.size.toULong())
                for (i in list) encodeObject(scope, i, type)
            } else {
                putBit(0)
                if (!fixedSize)
                    encodeUnsigned(list.size.toULong())
                for (i in list) encodeAny(scope, i)
            }
        }
    }

    /**
     * Write object _with no type record_: type is known
     */
    suspend fun encodeObject(scope: Scope, obj: Obj,overrideType: LynonType? = null) {
        encodeCached(obj) {
            obj.serialize(scope, this, overrideType)
        }
    }

    suspend fun encodeCachedBytes(bytes: ByteArray) {
        encodeCached(bytes) {
            bout.compress(bytes)
        }
    }

    fun encodeBinaryData(data: ByteArray) {
        bout.compress(data)
    }

    fun encodeSigned(value: Long) {
        bout.packSigned(value)
    }

    @Suppress("unused")
    fun encodeUnsigned(value: ULong) {
        bout.packUnsigned(value)
    }

    @Suppress("unused")
    fun encodeBool(value: Boolean) {
        bout.putBit(if (value) 1 else 0)
    }

    fun encodeReal(value: Double) {
        bout.putBits(value.toRawBits().toULong(), 64)
    }

    fun encodeBoolean(value: Boolean) {
        bout.putBit(if (value) 1 else 0)
    }

    fun putBits(value: Int, sizeInBits: Int) {
        bout.putBits(value.toULong(), sizeInBits)
    }

    fun putBit(bit: Int) {
        bout.putBit(bit)
    }

}
