package com.gh.gamecenter.ksp.compiler

import com.alibaba.fastjson.JSON
import com.alibaba.fastjson.serializer.SerializerFeature
import com.gh.gamecenter.ksp.compiler.entity.RouteDoc
import com.gh.gamecenter.ksp.compiler.entity.RouteMeta
import com.gh.gamecenter.ksp.compiler.entity.RouteMetaKsp
import com.gh.gamecenter.ksp.compiler.entity.TypeKind
import com.gh.gamecenter.ksp.compiler.entity.kspRawType
import com.gh.gamecenter.ksp.compiler.utils.Consts
import com.gh.gamecenter.ksp.compiler.utils.KSPLoggerWrapper
import com.gh.gamecenter.ksp.compiler.utils.findAnnotationWithType
import com.gh.gamecenter.ksp.compiler.utils.findModuleName
import com.gh.gamecenter.ksp.compiler.utils.typeExchange
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFile
import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview
import com.therouter.router.Autowired
import com.therouter.router.Route
import java.util.*
import kotlin.text.get

@KotlinPoetKspPreview
class RouteSymbolProcessorProvider : SymbolProcessorProvider {

    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        return RouteSymbolProcessor(
            KSPLoggerWrapper(environment.logger), environment.codeGenerator, environment.options
        )
    }

    class RouteSymbolProcessor(
        private val logger: KSPLoggerWrapper,
        private val codeGenerator: CodeGenerator,
        options: Map<String, String>
    ) : SymbolProcessor {
        companion object {
            private val ROUTE_CLASS_NAME = Route::class.qualifiedName!!
        }

        private val moduleName = options.findModuleName(logger)
        private val generateDoc = true

        override fun process(resolver: Resolver): List<KSAnnotated> {
            val symbol = resolver.getSymbolsWithAnnotation(ROUTE_CLASS_NAME)

            val elements = symbol.filterIsInstance<KSClassDeclaration>().toList()

            if (elements.isNotEmpty()) {
                logger.warn(">>> RouteSymbolProcessor init. <<<")
                try {
                    parseRoute(elements)
                } catch (e: Exception) {
                    logger.exception(e)
                }
            }

            return emptyList()
        }

        private fun parseRoute(elements: List<KSClassDeclaration>) {
            logger.warn(">>> Found routes, size is " + elements.size + " <<<")

            val groupsMap = HashMap<String, TreeSet<RouteMeta>>()
            val docSource = mutableMapOf<String, List<RouteDoc>>()

            for (element in elements) {
                categories(extractRouteMeta(element), groupsMap)
            }

            generateGroupFiles(groupsMap, docSource)
            generateRootFile(groupsMap, docSource)
        }

        private fun categories(
            routeMeta: RouteMeta, groupsMap: HashMap<String, TreeSet<RouteMeta>>
        ) {
            if (routeVerify(routeMeta)) {
                logger.warn(">>> Start categories, group = " + routeMeta.group + ", path = " + routeMeta.path + routeMeta.paramsType + " <<<")
                val routeMetas = groupsMap[routeMeta.group]
                if (routeMetas.isNullOrEmpty()) {
                    val routeMetaSet = sortedSetOf<RouteMeta>({ r1, r2 ->
                        try {
                            r1!!.path.compareTo(r2!!.path)
                        } catch (npe: NullPointerException) {
                            logger.error(npe.message.toString())
                            0
                        }
                    })
                    routeMetaSet.add(routeMeta)
                    groupsMap[routeMeta.group] = routeMetaSet
                } else {
                    routeMetas.add(routeMeta)
                }
            } else {
                logger.warn(">>> Route meta verify error, group is " + routeMeta.group + " <<<")
            }
        }

        private fun generateGroupFiles(
            groupsMap: HashMap<String, TreeSet<RouteMeta>>,
            docSource: MutableMap<String, List<RouteDoc>>
        ) {
            for (entry in groupsMap) {
                if (entry.key.isEmpty() || entry.value.isEmpty()) {
                    continue
                }

                val groupName = entry.key
                val groupRouteMetas = entry.value
                val dependencies = mutableSetOf<KSFile>()
                val routeDocList = mutableListOf<RouteDoc>()

                for (routeMeta in groupRouteMetas) {
                    val routeDoc = extractDocInfo(routeMeta)
                    val element = routeMeta.kspRawType
                    element.containingFile?.let {
                        dependencies.add(it)
                    }

                    extractRouteMetaParamInfoDoc(routeMeta)?.let {
                        routeDoc.params = it
                    }

                    routeDoc.className = element.qualifiedName!!.asString()
                    routeDocList.add(routeDoc)
                }
                docSource[groupName] = routeDocList
            }
        }

        private fun generateRootFile(
            groupsMap: HashMap<String, TreeSet<RouteMeta>>,
            docSource: MutableMap<String, List<RouteDoc>>
        ) {
            if (groupsMap.isEmpty()) {
                return
            }

            val dependencies = mutableSetOf<KSFile>()
            for (entry in groupsMap) {
                val groupName = entry.key

                if (groupName.isEmpty() || entry.value.isEmpty()) {
                    continue
                }

                logger.warn(">>> Route meta generateRootFile, groupName is " + groupName + " <<<")

                for (item in entry.value) {
                    item.kspRawType.containingFile?.let {
                        dependencies.add(it)
                    }
                }
            }

            val rootClassName = Consts.NAME_OF_ROOT + Consts.SEPARATOR + moduleName

            logger.warn(">>> Generated root, name is $rootClassName <<<")
            generateDocFile(docSource, dependencies)
        }

        @Suppress("SpellCheckingInspection")
        private fun generateDocFile(
            docSource: MutableMap<String, List<RouteDoc>>, source: MutableSet<KSFile>
        ) {
            if (generateDoc) {
                val doc = JSON.toJSONString(docSource, SerializerFeature.PrettyFormat)
                val dependencies = Dependencies(false, *source.toTypedArray())
                val file = codeGenerator.createNewFile(
                    dependencies,
                    Consts.PACKAGE_OF_GENERATE_DOCS,
                    "router-map-of-$moduleName",
                    "json"
                )
                file.use {
                    it.write(doc.toByteArray())
                }
                logger.warn(">>> Generated doc, name is arouter-map-of-$moduleName <<<")
            }
        }

        private fun routeVerify(meta: RouteMeta): Boolean {
            val path = meta.path
            // The path must be start with '/' and not empty!
            if (path.isNullOrEmpty() || !path.startsWith("/")) {
                return false
            }
            return if (meta.group.isNullOrEmpty()) { // Use default group(the first word in path)
                try {
                    val defaultGroup = path.substring(1, path.indexOf("/", 1))
                    if (defaultGroup.isEmpty()) {
                        return false
                    }
                    meta.group = defaultGroup
                    true
                } catch (e: Exception) {
                    logger.error("Failed to extract default group! " + e.message)
                    false
                }
            } else true
        }

        private fun extractRouteMeta(element: KSClassDeclaration): RouteMeta {
            val qualifiedName = element.qualifiedName?.asString()
                ?: error("local variable can not be annotated with @Route")
            val route = element.findAnnotationWithType<Route>()
                ?: error("$qualifiedName must annotated with @Route")

            val paramsType = HashMap<String, Int>()
            val injectConfig = HashMap<String, Autowired>()
            injectParamCollector(element, paramsType, injectConfig)
            return RouteMetaKsp.build(route, element, paramsType).also {
                it.injectConfig = injectConfig
            }
        }

        private fun extractDocInfo(routeMeta: RouteMeta): RouteDoc {
            val routeDoc = RouteDoc()
            routeDoc.group = routeMeta.group
            routeDoc.path = routeMeta.path
            routeDoc.description = routeMeta.name
            routeDoc.mark = routeMeta.extra
            return routeDoc
        }

        private fun extractRouteMetaParamInfoDoc(routeMeta: RouteMeta): MutableList<RouteDoc.Param>? {
            val paramsType = routeMeta.paramsType
            val injectConfigs = routeMeta.injectConfig
            if (!paramsType.isNullOrEmpty()) {
                val paramList = mutableListOf<RouteDoc.Param>()
                for (types in paramsType) {
                    val param = RouteDoc.Param()
                    val injectConfig = injectConfigs[types.key]
                    param.key = types.key
                    param.type = TypeKind.entries[types.value].name.lowercase()
                    param.description = injectConfig?.description
                    param.isRequired = injectConfig?.required == true
                    paramList.add(param)
                }
                return paramList
            }
            return null
        }

        private fun injectParamCollector(
            element: KSClassDeclaration,
            paramsType: HashMap<String, Int>,
            injectConfig: HashMap<String, Autowired>
        ) {
            for (field in element.getAllProperties()) {
                val annotation = field.findAnnotationWithType<Autowired>()
                if (annotation != null) {
                    val injectName = annotation.name.ifEmpty { field.simpleName.asString() }
                    paramsType[injectName] = field.typeExchange()
                    injectConfig[injectName] = annotation
                }
            }
        }
    }

}

