/*
 * ImageToolbox is an image editor for android
 * Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *
 * 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.
 *
 * You should have received a copy of the Apache License
 * along with this program.  If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
 */

@file:Suppress("unused")

package com.t8rin.imagetoolbox.core.ksp.processor

import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.Variance
import com.t8rin.imagetoolbox.core.ksp.annotations.UiFilterInject

internal class UiFilterInjectProcessor : SymbolProcessorProvider {
    override fun create(
        environment: SymbolProcessorEnvironment
    ): SymbolProcessor = UiFilterInjectProcessorImpl(environment)
}

private class UiFilterInjectProcessorImpl(
    environment: SymbolProcessorEnvironment
) : SymbolProcessor {

    private val logger = environment.logger
    private val codegen = environment.codeGenerator

    private var generated = false

    override fun process(resolver: Resolver): List<KSAnnotated> {
        if (generated) return emptyList()

        val annotationFqn = "$PACKAGE.core.ksp.annotations.UiFilterInject"

        val annotated = resolver.getSymbolsWithAnnotation(annotationFqn)
            .filterIsInstance<KSClassDeclaration>()
            .sortedBy { it.simpleName.asString() }
            .toList()

        if (annotated.isEmpty()) return emptyList()

        val entries = annotated.mapNotNull { declaration ->
            declaration.toUiFilterEntry(annotationFqn, logger)
        }

        if (entries.isEmpty()) return emptyList()

        val duplicates = entries.groupBy { it.filterInterface }
            .filterValues { it.size > 1 }
        if (duplicates.isNotEmpty()) {
            duplicates.forEach { (filterInterface, declarations) ->
                logger.error(
                    "Multiple UiFilters mapped to $filterInterface: " +
                            declarations.joinToString { it.uiClassName }
                )
            }
            return emptyList()
        }

        val file = codegen.createNewFile(
            dependencies = Dependencies(
                aggregating = false,
                *annotated.mapNotNull { it.containingFile }.toTypedArray()
            ),
            packageName = GENERATED_PACKAGE,
            fileName = GENERATED_FILE
        )

        file.bufferedWriter().use { writer ->
            writer.apply {
                appendLine("// Generated by KSP — do not edit")
                appendLine("package $GENERATED_PACKAGE")
                appendLine()
                appendLine("import $PACKAGE.core.filters.domain.model.Filter")
                appendLine("import $PACKAGE.core.filters.presentation.model.*")
                appendLine()

                appendCopyFunction(entries)
                appendLine()
                appendNewInstanceFunction(entries)
                appendLine()
                appendToUiMapping(entries)
                appendLine()
                appendGroupFactories(entries)
            }
        }

        generated = true
        return emptyList()
    }

    private fun Appendable.appendCopyFunction(entries: List<UiFilterEntry>) {
        appendLine("@Suppress(\"UNCHECKED_CAST\")")
        appendLine("internal fun copyUiFilterInstance(")
        appendLine("    filter: UiFilter<*>,")
        appendLine("    newValue: Any")
        appendLine("): UiFilter<*> = when (filter) {")

        entries.forEach { entry ->
            val fallback = entry.defaultConstructorCall(receiverName = "filter")
            val line = if (entry.valueType != null) {
                "    is ${entry.uiClassName} -> runCatching { ${entry.uiClassName}(newValue as ${entry.valueType}) }.getOrElse { $fallback }"
            } else {
                "    is ${entry.uiClassName} -> $fallback"
            }
            appendLine(line)
        }

        appendLine("}.also { copied ->")
        appendLine("    copied.isVisible = filter.isVisible")
        appendLine("}")
    }

    private fun Appendable.appendNewInstanceFunction(entries: List<UiFilterEntry>) {
        appendLine("internal fun newUiFilterInstance(filter: UiFilter<*>): UiFilter<*> = when (filter) {")

        entries.forEach { entry ->
            appendLine("    is ${entry.uiClassName} -> ${entry.defaultConstructorCall(receiverName = "filter")}")
        }

        appendLine("}.also { instance ->")
        appendLine("    if (filter.value is Unit) {")
        appendLine("        instance.isVisible = filter.isVisible")
        appendLine("    }")
        appendLine("}")
    }

    private fun Appendable.appendToUiMapping(entries: List<UiFilterEntry>) {
        appendLine("internal fun mapFilterToUiFilter(")
        appendLine("    filter: Filter<*>,")
        appendLine("    preserveVisibility: Boolean = true")
        appendLine("): UiFilter<*> = when (filter) {")

        entries.forEach { entry ->
            val constructorCall = if (entry.valueType != null) {
                "${entry.uiClassName}(filter.value)"
            } else {
                "${entry.uiClassName}()"
            }
            appendLine("    is ${entry.filterInterface} -> $constructorCall")
        }

        appendLine($$"    else -> error(\"No UiFilter mapping for ${filter::class.qualifiedName}\")")
        appendLine("}.also { uiFilter ->")
        appendLine("    if (preserveVisibility) {")
        appendLine("        uiFilter.isVisible = filter.isVisible")
        appendLine("    }")
        appendLine("}")
    }

    private fun Appendable.appendGroupFactories(entries: List<UiFilterEntry>) {
        val grouped = entries.groupBy { it.group }

        GROUP_ORDER.forEach { group ->
            val functionName = groupFunctionName(group)
            val groupEntries = grouped[group].orEmpty().sortedBy { it.uiClassName }

            appendLine("internal fun $functionName(): List<UiFilter<*>> = listOf(")

            if (groupEntries.isEmpty()) {
                appendLine("    // No filters for $group")
            } else {
                groupEntries.forEachIndexed { index, entry ->
                    val constructorCall = entry.defaultConstructorCall(receiverName = "it")
                        .replace("it.", "")
                    val suffix = if (index == groupEntries.lastIndex) "" else ","
                    appendLine("    $constructorCall$suffix")
                }
            }

            appendLine(")")
            appendLine()
        }
    }

    private fun KSClassDeclaration.toUiFilterEntry(
        annotationFqn: String,
        logger: KSPLogger
    ): UiFilterEntry? {
        val className = simpleName.asString()

        val filterInterface =
            superTypes.mapNotNull { it.resolve().declaration as? KSClassDeclaration }
                .firstOrNull {
                it.qualifiedName?.asString()
                    ?.startsWith("$PACKAGE.core.filters.domain.model.Filter.") == true
            }
            ?.qualifiedName
            ?.asString()
            ?.removePrefix("$PACKAGE.core.filters.domain.model.")

        if (filterInterface == null) {
            logger.error("$className is annotated with @UiFilterInject but does not implement Filter.*")
            return null
        }

        val constructorParameters = primaryConstructor?.parameters.orEmpty()
        if (constructorParameters.size > 1) {
            logger.error("$className should declare zero or one constructor parameter")
            return null
        }

        val valueParameter = constructorParameters.firstOrNull()
        val valueType = valueParameter?.type?.resolve()?.renderType()
        val hasDefaultValue = valueParameter?.hasDefault ?: false

        val group = annotations.firstOrNull {
            it.annotationType.resolve().declaration.qualifiedName?.asString() == annotationFqn
        }?.arguments?.firstOrNull {
            it.name?.asString() == "group"
        }?.value as? String ?: UiFilterInject.Groups.UNSPECIFIED

        if (group !in GROUP_ORDER && group != UiFilterInject.Groups.UNSPECIFIED) {
            logger.error("Unknown UiFilter group '$group' on $className")
            return null
        }

        return UiFilterEntry(
            uiClassName = className,
            filterInterface = filterInterface,
            valueType = valueType,
            valueParamHasDefault = hasDefaultValue,
            group = group
        )
    }

    private fun KSType.renderType(): String {
        val declarationName = declaration.qualifiedName?.asString()
            ?: declaration.simpleName.asString()

        val argumentsRendered = arguments.takeIf { it.isNotEmpty() }
            ?.joinToString(", ") { argument ->
                val type = argument.type?.resolve()
                if (type == null || argument.variance == Variance.STAR) {
                    "*"
                } else {
                    val rendered = type.renderType()
                    when (argument.variance) {
                        Variance.COVARIANT -> "out $rendered"
                        Variance.CONTRAVARIANT -> "in $rendered"
                        else -> rendered
                    }
                }
            }
            ?.let { "<$it>" }
            ?: ""

        val nullable = if (isMarkedNullable) "?" else ""

        return declarationName + argumentsRendered + nullable
    }

    private fun UiFilterEntry.defaultConstructorCall(receiverName: String): String = when {
        valueType == null -> "$uiClassName()"
        valueParamHasDefault -> "$uiClassName()"
        else -> "$uiClassName($receiverName.value as $valueType)"
    }

    private fun groupFunctionName(group: String): String = when (group) {
        UiFilterInject.Groups.SIMPLE -> "simpleGroupFilters"
        UiFilterInject.Groups.COLOR -> "colorGroupFilters"
        UiFilterInject.Groups.LUT -> "lutGroupFilters"
        UiFilterInject.Groups.LIGHT -> "lightGroupFilters"
        UiFilterInject.Groups.EFFECTS -> "effectsGroupFilters"
        UiFilterInject.Groups.BLUR -> "blurGroupFilters"
        UiFilterInject.Groups.PIXELATION -> "pixelationGroupFilters"
        UiFilterInject.Groups.DISTORTION -> "distortionGroupFilters"
        UiFilterInject.Groups.DITHERING -> "ditheringGroupFilters"
        else -> "unspecifiedGroupFilters"
    }

    private data class UiFilterEntry(
        val uiClassName: String,
        val filterInterface: String,
        val valueType: String?,
        val valueParamHasDefault: Boolean,
        val group: String
    )

    private companion object {
        private const val PACKAGE = "com.t8rin.imagetoolbox"
        private const val GENERATED_PACKAGE =
            "com.t8rin.imagetoolbox.core.filters.presentation.model.generated"
        private const val GENERATED_FILE = "UiFilterMappings"

        private val GROUP_ORDER = listOf(
            UiFilterInject.Groups.SIMPLE,
            UiFilterInject.Groups.COLOR,
            UiFilterInject.Groups.LUT,
            UiFilterInject.Groups.LIGHT,
            UiFilterInject.Groups.EFFECTS,
            UiFilterInject.Groups.BLUR,
            UiFilterInject.Groups.PIXELATION,
            UiFilterInject.Groups.DISTORTION,
            UiFilterInject.Groups.DITHERING
        )
    }
}
