/*
 * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */

package kotlinx.coroutines.tools

import org.objectweb.asm.*
import org.objectweb.asm.tree.*
import java.io.*
import java.util.jar.*

fun JarFile.classEntries() = entries().asSequence().filter {
    !it.isDirectory && it.name.endsWith(".class") && !it.name.startsWith("META-INF/")
}

fun getBinaryAPI(jar: JarFile, visibilityMap: Map<String, ClassVisibility>): List<ClassBinarySignature> =
    getBinaryAPI(jar.classEntries().map { entry -> jar.getInputStream(entry) }, visibilityMap)

fun getBinaryAPI(
    classStreams: Sequence<InputStream>,
    visibilityMap: Map<String, ClassVisibility>
): List<ClassBinarySignature> =
    classStreams.map {
        it.use { stream ->
            val classNode = ClassNode()
            ClassReader(stream).accept(classNode, ClassReader.SKIP_CODE)
            classNode
        }
    }.map {
        with(it) {
            val classVisibility = visibilityMap[name]
            val classAccess = AccessFlags(effectiveAccess and Opcodes.ACC_STATIC.inv())
            val supertypes = listOf(superName) - "java/lang/Object" + interfaces.sorted()

            val memberSignatures = (
                    fields.map {
                        with(it) {
                            FieldBinarySignature(
                                name,
                                desc,
                                isPublishedApi(),
                                AccessFlags(access)
                            )
                        }
                    } +
                            methods.map {
                                with(it) {
                                    MethodBinarySignature(
                                        name,
                                        desc,
                                        isPublishedApi(),
                                        AccessFlags(access)
                                    )
                                }
                            }
                    ).filter {
                it.isEffectivelyPublic(classAccess, classVisibility)
            }

            ClassBinarySignature(
                name,
                superName,
                outerClassName,
                supertypes,
                memberSignatures,
                classAccess,
                isEffectivelyPublic(classVisibility),
                isFileOrMultipartFacade() || isDefaultImpls()
            )
        }
    }.asIterable().sortedBy { it.name }


fun List<ClassBinarySignature>.filterOutNonPublic(nonPublicPackages: List<String> = emptyList()): List<ClassBinarySignature> {
    val nonPublicPaths = nonPublicPackages.map { it.replace('.', '/') + '/' }
    val classByName = associateBy { it.name }

    fun ClassBinarySignature.isInNonPublicPackage() =
        nonPublicPaths.any { name.startsWith(it) }

    fun ClassBinarySignature.isPublicAndAccessible(): Boolean =
        isEffectivelyPublic &&
                (outerName == null || classByName[outerName]?.let { outerClass ->
                    !(this.access.isProtected && outerClass.access.isFinal)
                            && outerClass.isPublicAndAccessible()
                } ?: true)

    fun supertypes(superName: String) = generateSequence({ classByName[superName] }, { classByName[it.superName] })

    fun ClassBinarySignature.flattenNonPublicBases(): ClassBinarySignature {

        val nonPublicSupertypes = supertypes(superName).takeWhile { !it.isPublicAndAccessible() }.toList()
        if (nonPublicSupertypes.isEmpty())
            return this

        val inheritedStaticSignatures =
            nonPublicSupertypes.flatMap { it.memberSignatures.filter { it.access.isStatic } }

        // not covered the case when there is public superclass after chain of private superclasses
        return this.copy(
            memberSignatures = memberSignatures + inheritedStaticSignatures,
            supertypes = supertypes - superName
        )
    }

    return filter { !it.isInNonPublicPackage() && it.isPublicAndAccessible() }
        .map { it.flattenNonPublicBases() }
        .filterNot { it.isNotUsedWhenEmpty && it.memberSignatures.isEmpty() }
}

fun List<ClassBinarySignature>.dump() = dump(to = System.out)

fun <T : Appendable> List<ClassBinarySignature>.dump(to: T): T = to.apply {
    this@dump.forEach {
        append(it.signature).appendln(" {")
        it.memberSignatures.sortedWith(MEMBER_SORT_ORDER).forEach { append("\t").appendln(it.signature) }
        appendln("}\n")
    }
}

