/*
 * Copyright 2026 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.lyng.obj

import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import net.sergeych.lyng.InteropOperator
import net.sergeych.lyng.OperatorInteropRegistry
import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.addConstDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
import kotlin.math.floor
import kotlin.math.roundToLong

data class ObjReal(val value: Double) : Obj(), Numeric {
    override val longValue: Long get() = floor(value).toLong()
    override val doubleValue: Double get() = value
    override val toObjInt: ObjInt get() = ObjInt.of(longValue)
    override val toObjReal: ObjReal get() = this

    override val objClass: ObjClass get() = type

    override fun byValueCopy(): Obj = this

    override suspend fun compareTo(scope: Scope, other: Obj): Int {
        if (other is ObjReal) return value.compareTo(other.value)
        ObjDecimalSupport.mixedRealDecimalCompareFallback(this, other)?.let { return it }
        OperatorInteropRegistry.invokeCompare(scope, this, other)?.let { return it }
        if (other !is Numeric) return -2
        return value.compareTo(other.doubleValue)
    }

    override fun toString(): String {
        // Normalize scientific notation to match tests across platforms.
        // Kotlin/JVM prints 1e-6 as "1.0E-6" by default; tests accept "1E-6" (or a plain decimal).
        val s = value.toString()
        val ePos = s.indexOf('E').let { if (it >= 0) it else s.indexOf('e') }
        if (ePos >= 0) {
            val mantissa = s.substring(0, ePos)
            val exponent = s.substring(ePos + 1) // skip the 'E'/'e'
            val mantissaNorm = if (mantissa.endsWith(".0")) mantissa.dropLast(2) else mantissa
            return mantissaNorm + "E" + exponent
        }
        return s
    }

    override fun hashCode(): Int {
        return value.hashCode()
    }

    override suspend fun plus(scope: Scope, other: Obj): Obj =
        ObjDecimalSupport.mixedRealDecimalArithmeticFallback(this, other, InteropOperator.Plus)
            ?: OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Plus)
            ?: of(this.value + other.toDouble())

    override suspend fun minus(scope: Scope, other: Obj): Obj =
        ObjDecimalSupport.mixedRealDecimalArithmeticFallback(this, other, InteropOperator.Minus)
            ?: OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Minus)
            ?: of(this.value - other.toDouble())

    override suspend fun mul(scope: Scope, other: Obj): Obj =
        ObjDecimalSupport.mixedRealDecimalArithmeticFallback(this, other, InteropOperator.Mul)
            ?: OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Mul)
            ?: of(this.value * other.toDouble())

    override suspend fun div(scope: Scope, other: Obj): Obj =
        ObjDecimalSupport.mixedRealDecimalArithmeticFallback(this, other, InteropOperator.Div)
            ?: OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Div)
            ?: of(this.value / other.toDouble())

    override suspend fun mod(scope: Scope, other: Obj): Obj =
        ObjDecimalSupport.mixedRealDecimalArithmeticFallback(this, other, InteropOperator.Mod)
            ?: OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Mod)
            ?: of(this.value % other.toDouble())

    /**
     * Returns unboxed Double value
     */
    override suspend fun toKotlin(scope: Scope): Any {
        return value
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || this::class != other::class) return false

        other as ObjReal

        return value == other.value
    }

    override suspend fun negate(scope: Scope): Obj {
        return of(-value)
    }

    override suspend fun lynonType(): LynonType = LynonType.Real

    override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
        encoder.encodeReal(value)
    }

    override suspend fun toJson(scope: Scope): JsonElement {
        return JsonPrimitive(value)
    }

    companion object {
        val Zero = ObjReal(0.0)
        val One = ObjReal(1.0)

        fun of(value: Double): ObjReal = when (value) {
            0.0 -> Zero
            1.0 -> One
            else -> ObjReal(value)
        }

        val type: ObjClass = object : ObjClass("Real") {
            override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
                of(decoder.unpackDouble())
        }.apply {
            isClosed = true
            // roundToInt: number rounded to the nearest integer
            addConstDoc(
                name = "roundToInt",
                value = ObjExternCallable.fromBridge {
                    (thisObj as ObjReal).value.roundToLong().toObj()
                },
                doc = "This real number rounded to the nearest integer.",
                type = type("lyng.Int"),
                moduleName = "lyng.stdlib"
            )
            addFnDoc(
                name = "toInt",
                doc = "Truncate this real number toward zero to an integer.",
                returns = type("lyng.Int"),
                moduleName = "lyng.stdlib"
            ) {
                ObjInt.of(thisAs<ObjReal>().value.toLong())
            }
            addFnDoc(
                name = "isInfinite",
                doc = "Return true if this real number is positive or negative infinity.",
                returns = type("lyng.Bool"),
                moduleName = "lyng.stdlib"
            ) {
                ObjBool(thisAs<ObjReal>().value.isInfinite())
            }
            addFnDoc(
                name = "isNaN",
                doc = "Return true if this real number is NaN (not a number).",
                returns = type("lyng.Bool"),
                moduleName = "lyng.stdlib"
            ) {
                ObjBool(thisAs<ObjReal>().value.isNaN())
            }
        }
    }
}
