/*
 * 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.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import net.sergeych.lyng.*
import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.requireScope
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
import net.sergeych.sprintf.sprintf

@Serializable
@SerialName("string")
data class ObjString(val value: String) : Obj() {

//    fun normalize(context: Context, index: Int, allowsEndInclusive: Boolean = false): Int {
//        val i = if (index < 0) value.length + index else index
//        if (allowsEndInclusive && i == value.length) return i
//        if (i !in value.indices) context.raiseError("index $index out of bounds for length ${value.length} of \"$value\"")
//        return i
//    }

    override suspend fun compareTo(scope: Scope, other: Obj): Int {
        if (other !is ObjString) return -2
        return this.value.compareTo(other.value)
    }

    override fun toString(): String = value

    override suspend fun inspect(scope: Scope): String {
        return "\"$value\""
    }

    override val objClass: ObjClass
        get() = type

    override suspend fun plus(scope: Scope, other: Obj): Obj {
        return ObjString(value + other.toString(scope).value)
    }

    override suspend fun getAt(scope: Scope, index: Obj): Obj {
        when (index) {
            is ObjInt -> return ObjChar(value[index.toInt()])
            is ObjRange -> {
                val start = if (index.start == null || index.start.isNull) 0 else index.start.toInt()
                val end = if (index.end == null || index.end.isNull) value.length else {
                    val e = index.end.toInt()
                    if (index.isEndInclusive) e + 1 else e
                }
                return ObjString(value.substring(start, end))
            }

            is ObjRegex -> {
                return index.find(this)
            }

            else -> scope.raiseIllegalArgument("String index must be Int, Regex or Range")
        }
    }

    override suspend fun mul(scope: Scope, other: Obj): Obj {
        var times = other.toInt()
        if( times < 0 ) scope.raiseIllegalArgument("negative string repetitions")
        return ObjString( buildString {
            while( times-- > 0 ) append(value)
        })
    }

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

    override suspend fun callOn(scope: Scope): Obj {
        return ObjString(this.value.sprintf(*scope.args
            .toKotlinList(scope)
            .map { if (it == null) "null" else it }
            .toTypedArray()))
    }

    override suspend fun contains(scope: Scope, other: Obj): Boolean {
        return if (other is ObjString)
            value.contains(other.value)
        else if (other is ObjChar)
            value.contains(other.value)
        else scope.raiseIllegalArgument("String.contains can't take $other")
    }

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

        other as ObjString

        return value == other.value
    }

    override suspend fun operatorMatch(scope: Scope, other: Obj): Obj {
        val re = other.cast<ObjRegex>(scope)
        return re.operatorMatch(scope, this)
    }

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

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

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

    private fun replaceLiteralChar(oldValue: Char, newValue: String, firstOnly: Boolean): ObjString {
        if (!firstOnly) return ObjString(value.replace(oldValue.toString(), newValue))
        val index = value.indexOf(oldValue)
        if (index < 0) return this
        return ObjString(value.substring(0, index) + newValue + value.substring(index + 1))
    }

    private fun replaceLiteralString(oldValue: String, newValue: String, firstOnly: Boolean): ObjString =
        ObjString(
            if (firstOnly) value.replaceFirst(oldValue, newValue)
            else value.replace(oldValue, newValue)
        )

    private suspend fun replaceRegex(
        scope: ScopeFacade,
        pattern: Regex,
        firstOnly: Boolean,
        replacementProvider: suspend (ObjRegexMatch) -> String
    ): ObjString {
        val firstMatch = pattern.find(value) ?: return this
        if (firstOnly) {
            val replacement = replacementProvider(ObjRegexMatch(firstMatch))
            val start = firstMatch.range.first
            val endExclusive = start + firstMatch.value.length
            return ObjString(value.substring(0, start) + replacement + value.substring(endExclusive))
        }

        val result = StringBuilder(value.length)
        var lastIndex = 0
        for (match in pattern.findAll(value)) {
            val start = match.range.first
            val endExclusive = start + match.value.length
            result.append(value, lastIndex, start)
            result.append(replacementProvider(ObjRegexMatch(match)))
            lastIndex = endExclusive
        }
        result.append(value, lastIndex, value.length)
        return ObjString(result.toString())
    }

    companion object {
        val type = object : ObjClass("String", ObjCollection) {
            override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
                ObjString(decoder.unpackBinaryData().decodeToString())
        }.apply {
            isClosed = true
            addFnDoc(
                name = "iterator",
                doc = "Iterator over characters of this string.",
                returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Char"))),
                moduleName = "lyng.stdlib"
            ) { ObjKotlinIterator(thisAs<ObjString>().value.iterator()) }
            addFnDoc(
                name = "toInt",
                doc = "Parse this string as an integer or throw if it is not a valid integer.",
                returns = type("lyng.Int"),
                moduleName = "lyng.stdlib"
            ) {
                ObjInt.of(
                    thisAs<ObjString>().value.toLongOrNull()
                        ?: raiseIllegalArgument("can't convert to int: $thisObj")
                )
            }
            addFnDoc(
                name = "startsWith",
                doc = "Whether this string starts with the given prefix.",
                params = listOf(ParamDoc("prefix", type("lyng.String"))),
                returns = type("lyng.Bool"),
                moduleName = "lyng.stdlib"
            ) {
                ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value))
            }
            addFnDoc(
                name = "endsWith",
                doc = "Whether this string ends with the given suffix.",
                params = listOf(ParamDoc("suffix", type("lyng.String"))),
                returns = type("lyng.Bool"),
                moduleName = "lyng.stdlib"
            ) {
                ObjBool(thisAs<ObjString>().value.endsWith(requiredArg<ObjString>(0).value))
            }
            addPropertyDoc(
                name = "length",
                doc = "Number of UTF-16 code units in this string.",
                type = type("lyng.Int"),
                moduleName = "lyng.stdlib",
                getter = { ObjInt.of((this.thisObj as ObjString).value.length.toLong()) }
            )
            addFnDoc(
                name = "takeLast",
                doc = "Return a string with the last N characters.",
                params = listOf(ParamDoc("n", type("lyng.Int"))),
                returns = type("lyng.String"),
                moduleName = "lyng.stdlib"
            ) {
                thisAs<ObjString>().value.takeLast(
                    requiredArg<ObjInt>(0).toInt()
                ).let(::ObjString)
            }
            addFnDoc(
                name = "take",
                doc = "Return a string with the first N characters.",
                params = listOf(ParamDoc("n", type("lyng.Int"))),
                returns = type("lyng.String"),
                moduleName = "lyng.stdlib"
            ) {
                thisAs<ObjString>().value.take(
                    requiredArg<ObjInt>(0).toInt()
                ).let(::ObjString)
            }
            addFnDoc(
                name = "drop",
                doc = "Drop the first N characters and return the remainder.",
                params = listOf(ParamDoc("n", type("lyng.Int"))),
                returns = type("lyng.String"),
                moduleName = "lyng.stdlib"
            ) {
                thisAs<ObjString>().value.drop(
                    requiredArg<ObjInt>(0).toInt()
                ).let(::ObjString)
            }
            addFnDoc(
                name = "dropLast",
                doc = "Drop the last N characters and return the remainder.",
                params = listOf(ParamDoc("n", type("lyng.Int"))),
                returns = type("lyng.String"),
                moduleName = "lyng.stdlib"
            ) {
                thisAs<ObjString>().value.dropLast(
                    requiredArg<ObjInt>(0).toInt()
                ).let(::ObjString)
            }
            addFnDoc(
                name = "lower",
                doc = "Lowercase version of this string (default locale).",
                returns = type("lyng.String"),
                moduleName = "lyng.stdlib"
            ) {
                thisAs<ObjString>().value.lowercase().let(::ObjString)
            }
            addFnDoc(
                name = "lowercase",
                doc = "Lowercase version of this string (default locale).",
                returns = type("lyng.String"),
                moduleName = "lyng.stdlib"
            ) {
                thisAs<ObjString>().value.lowercase().let(::ObjString)
            }
            addFnDoc(
                name = "upper",
                doc = "Uppercase version of this string (default locale).",
                returns = type("lyng.String"),
                moduleName = "lyng.stdlib"
            ) {
                thisAs<ObjString>().value.uppercase().let(::ObjString)
            }
            addFnDoc(
                name = "uppercase",
                doc = "Uppercase version of this string (default locale).",
                returns = type("lyng.String"),
                moduleName = "lyng.stdlib"
            ) {
                thisAs<ObjString>().value.uppercase().let(::ObjString)
            }
            addPropertyDoc(
                name = "characters",
                doc = "List of characters of this string.",
                type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Char"))),
                moduleName = "lyng.stdlib",
                getter = {
                    ObjList(
                        (this.thisObj as ObjString).value.map { ObjChar(it) }.toMutableList()
                    )
                }
            )
            addFnDoc(
                name = "last",
                doc = "The last character of this string or throw if the string is empty.",
                returns = type("lyng.Char"),
                moduleName = "lyng.stdlib"
            ) {
                ObjChar(thisAs<ObjString>().value.lastOrNull() ?: raiseNoSuchElement("empty string"))
            }
            addFnDoc(
                name = "encodeUtf8",
                doc = "Encode this string as UTF-8 bytes.",
                returns = type("lyng.Buffer"),
                moduleName = "lyng.stdlib"
            ) { ObjBuffer(thisAs<ObjString>().value.encodeToByteArray().asUByteArray()) }
            addPropertyDoc(
                name = "size",
                doc = "Alias for length: the number of characters (code units) in this string.",
                type = type("lyng.Int"),
                moduleName = "lyng.stdlib",
                getter = { ObjInt.of((this.thisObj as ObjString).value.length.toLong()) }
            )
            addFnDoc(
                name = "toReal",
                doc = "Parse this string as a real number (floating point).",
                returns = type("lyng.Real"),
                moduleName = "lyng.stdlib"
            ) {
                ObjReal.of(thisAs<ObjString>().value.toDouble())
            }
            addFnDoc(
                name = "trim",
                doc = "Return a copy of this string with leading and trailing whitespace removed.",
                returns = type("lyng.String"),
                moduleName = "lyng.stdlib"
            ) {
                thisAs<ObjString>().value.trim().let(::ObjString)
            }
            addFnDoc("isBlank", "Whether this string is empty or contains only whitespace characters.",
                returns = type("lyng.Bool"), moduleName = "lyng.stdlib") {
                ObjBool(thisAs<ObjString>().value.isBlank())
            }
            addFnDoc("isEmpty", "Whether this string is empty.",
                returns = type("lyng.Bool"), moduleName = "lyng.stdlib") {
                ObjBool(thisAs<ObjString>().value.isEmpty())
            }
            addFnDoc("isNotEmpty", "Whether this string is not empty.",
                returns = type("lyng.Bool"), moduleName = "lyng.stdlib") {
                ObjBool(thisAs<ObjString>().value.isNotEmpty())
            }
            addFnDoc(
                name = "matches",
                doc = "Whether this string matches the given regular expression or pattern string.",
                params = listOf(ParamDoc("pattern")),
                returns = type("lyng.Bool"),
                moduleName = "lyng.stdlib"
            ) {
                val s = requireOnlyArg<Obj>()
                val self = thisAs<ObjString>().value
                ObjBool(
                    when (s) {
                        is ObjRegex -> self.matches(s.regex)
                        is ObjString -> {
                            if (s.value == ".*") true
                            else {
                                val re = if (PerfFlags.REGEX_CACHE) RegexCache.get(s.value) else s.value.toRegex()
                                self.matches(re)
                            }
                        }

                        else ->
                            raiseIllegalArgument("can't match ${s.objClass.className}: required Regex or String")
                    }
                )
            }
            addFnDoc(
                name = "replace",
                doc = "Return a copy of this string with all literal or regex matches replaced. String arguments are treated literally; use Regex for regular expressions.",
                params = listOf(ParamDoc("old"), ParamDoc("new")),
                returns = type("lyng.String"),
                moduleName = "lyng.stdlib"
            ) {
                val source = thisAs<ObjString>()
                val oldValue = requiredArg<Obj>(0)
                val newValue = requiredArg<Obj>(1)
                when (oldValue) {
                    is ObjChar -> {
                        val replacement = when (newValue) {
                            is ObjChar -> newValue.value.toString()
                            is ObjString -> newValue.value
                            else -> raiseIllegalArgument("String.replace(Char, ...) requires Char or String replacement")
                        }
                        source.replaceLiteralChar(oldValue.value, replacement, firstOnly = false)
                    }

                    is ObjString -> {
                        val replacement = (newValue as? ObjString)?.value
                            ?: raiseIllegalArgument("String.replace(String, ...) requires String replacement")
                        source.replaceLiteralString(oldValue.value, replacement, firstOnly = false)
                    }

                    is ObjRegex -> when {
                        newValue is ObjString ->
                            source.replaceRegex(this, oldValue.regex, firstOnly = false) { newValue.value }

                        newValue.isInstanceOf("Callable") ->
                            source.replaceRegex(this, oldValue.regex, firstOnly = false) { match ->
                                val transformed = call(newValue, Arguments(match), ObjVoid)
                                (transformed as? ObjString)?.value
                                    ?: raiseIllegalArgument("String.replace(Regex, transform) callback must return String")
                            }

                        else ->
                            raiseIllegalArgument("String.replace(Regex, ...) requires String replacement or a callable")
                    }

                    else ->
                        raiseIllegalArgument("String.replace requires Char, String, or Regex as the first argument")
                }
            }
            addFnDoc(
                name = "replaceFirst",
                doc = "Return a copy of this string with the first literal or regex match replaced. String arguments are treated literally; use Regex for regular expressions.",
                params = listOf(ParamDoc("old"), ParamDoc("new")),
                returns = type("lyng.String"),
                moduleName = "lyng.stdlib"
            ) {
                val source = thisAs<ObjString>()
                val oldValue = requiredArg<Obj>(0)
                val newValue = requiredArg<Obj>(1)
                when (oldValue) {
                    is ObjChar -> {
                        val replacement = when (newValue) {
                            is ObjChar -> newValue.value.toString()
                            is ObjString -> newValue.value
                            else -> raiseIllegalArgument("String.replaceFirst(Char, ...) requires Char or String replacement")
                        }
                        source.replaceLiteralChar(oldValue.value, replacement, firstOnly = true)
                    }

                    is ObjString -> {
                        val replacement = (newValue as? ObjString)?.value
                            ?: raiseIllegalArgument("String.replaceFirst(String, ...) requires String replacement")
                        source.replaceLiteralString(oldValue.value, replacement, firstOnly = true)
                    }

                    is ObjRegex -> when {
                        newValue is ObjString ->
                            source.replaceRegex(this, oldValue.regex, firstOnly = true) { newValue.value }

                        newValue.isInstanceOf("Callable") ->
                            source.replaceRegex(this, oldValue.regex, firstOnly = true) { match ->
                                val transformed = call(newValue, Arguments(match), ObjVoid)
                                (transformed as? ObjString)?.value
                                    ?: raiseIllegalArgument("String.replaceFirst(Regex, transform) callback must return String")
                            }

                        else ->
                            raiseIllegalArgument("String.replaceFirst(Regex, ...) requires String replacement or a callable")
                    }

                    else ->
                        raiseIllegalArgument("String.replaceFirst requires Char, String, or Regex as the first argument")
                }
            }
            BuiltinDocRegistry.module("lyng.stdlib") {
                classDoc("String", doc = "") {
                    method(
                        name = "replace",
                        doc = "Replace all occurrences of the given character with another character.",
                        params = listOf(
                            ParamDoc("old", type("lyng.Char")),
                            ParamDoc("new", type("lyng.Char"))
                        ),
                        returns = type("lyng.String")
                    )
                    method(
                        name = "replace",
                        doc = "Replace all occurrences of the given character with another character or string.",
                        params = listOf(
                            ParamDoc("old", type("lyng.Char")),
                            ParamDoc("new", type("lyng.String"))
                        ),
                        returns = type("lyng.String")
                    )
                    method(
                        name = "replace",
                        doc = "Replace all literal occurrences of the given string with another string.",
                        params = listOf(
                            ParamDoc("old", type("lyng.String")),
                            ParamDoc("new", type("lyng.String"))
                        ),
                        returns = type("lyng.String")
                    )
                    method(
                        name = "replace",
                        doc = "Replace all regular-expression matches with the given replacement string.",
                        params = listOf(
                            ParamDoc("pattern", type("lyng.Regex")),
                            ParamDoc("new", type("lyng.String"))
                        ),
                        returns = type("lyng.String")
                    )
                    method(
                        name = "replace",
                        doc = "Replace all regular-expression matches using the callback result for each RegexMatch.",
                        params = listOf(
                            ParamDoc("pattern", type("lyng.Regex")),
                            ParamDoc("transform", funType(listOf(type("lyng.RegexMatch")), type("lyng.String")))
                        ),
                        returns = type("lyng.String")
                    )
                    method(
                        name = "replaceFirst",
                        doc = "Replace the first occurrence of the given character with another character.",
                        params = listOf(
                            ParamDoc("old", type("lyng.Char")),
                            ParamDoc("new", type("lyng.Char"))
                        ),
                        returns = type("lyng.String")
                    )
                    method(
                        name = "replaceFirst",
                        doc = "Replace the first occurrence of the given character with another character or string.",
                        params = listOf(
                            ParamDoc("old", type("lyng.Char")),
                            ParamDoc("new", type("lyng.String"))
                        ),
                        returns = type("lyng.String")
                    )
                    method(
                        name = "replaceFirst",
                        doc = "Replace the first literal occurrence of the given string with another string.",
                        params = listOf(
                            ParamDoc("old", type("lyng.String")),
                            ParamDoc("new", type("lyng.String"))
                        ),
                        returns = type("lyng.String")
                    )
                    method(
                        name = "replaceFirst",
                        doc = "Replace the first regular-expression match with the given replacement string.",
                        params = listOf(
                            ParamDoc("pattern", type("lyng.Regex")),
                            ParamDoc("new", type("lyng.String"))
                        ),
                        returns = type("lyng.String")
                    )
                    method(
                        name = "replaceFirst",
                        doc = "Replace the first regular-expression match using the callback result for the RegexMatch.",
                        params = listOf(
                            ParamDoc("pattern", type("lyng.Regex")),
                            ParamDoc("transform", funType(listOf(type("lyng.RegexMatch")), type("lyng.String")))
                        ),
                        returns = type("lyng.String")
                    )
                }
            }
            createField(
                name = "re",
                initialValue = ObjProperty(
                    name = "re",
                    getter = ObjExternCallable.fromBridge {
                        val pattern = (thisObj as ObjString).value
                        val re = if (PerfFlags.REGEX_CACHE) RegexCache.get(pattern) else pattern.toRegex()
                        ObjRegex(re)
                    },
                    setter = null
                ),
                type = ObjRecord.Type.Property
            )
            createField(
                name = "operatorMatch",
                initialValue = ObjExternCallable.fromBridge {
                    val other = args.firstAndOnly(Pos.builtIn)
                    val scope = requireScope()
                    val targetScope = scope.parent ?: scope
                    (thisObj as ObjString).operatorMatch(targetScope, other)
                },
                type = ObjRecord.Type.Fun
            )
        }
    }
}
