package generators.math

import templates.COPYRIGHT_NOTICE
import templates.autoGeneratedWarning
import java.io.File
import java.io.FileWriter
import kotlin.math.*
import kotlin.system.exitProcess

fun main(args: Array<String>) {
    val baseDir: File
    when (args.size) {
        1 -> {
            baseDir = File(args[0])
        }
        else -> {
            println("Parameters: <kotlin-base-dir> - generates sources for std math tests")
            exitProcess(1)
        }
    }

    generateUtils(baseDir.resolve("mathTestUtils.kt"))

    val trigonometryFunctions = listOf(
        MathTestsGenerator.ModelFunction1(::sin),
        MathTestsGenerator.ModelFunction1(::cos),
        MathTestsGenerator.ModelFunction1(::tan, exact = false),
        MathTestsGenerator.ModelFunction1(::asin),
        MathTestsGenerator.ModelFunction1(::atan),
        MathTestsGenerator.ModelFunction2(::atan2),
        MathTestsGenerator.ModelFunction1(::sinh),
        MathTestsGenerator.ModelFunction1(::cosh),
        MathTestsGenerator.ModelFunction1(::tanh),
        MathTestsGenerator.ModelFunction1(::asinh, exact = false),
        MathTestsGenerator.ModelFunction1(::acosh, exact = false),
        MathTestsGenerator.ModelFunction1(::atanh, exact = false),
    )
    MathTestsGenerator(
        outputFile = baseDir.resolve("trigonometry.kt"),
        testPoints = listOf(0.0, 1.0, PI),
        functions = trigonometryFunctions
    ).generate()

    //workaround about method with receiver
    fun powWrapper(x: Double, y: Double): Double = x.pow(y)

    val powerFunctions = listOf(
        MathTestsGenerator.ModelFunction2(::hypot),
        MathTestsGenerator.ModelFunction1(::sqrt),
        MathTestsGenerator.ModelFunction1(::exp, exact = false),
        MathTestsGenerator.ModelFunction1(::expm1),
        MathTestsGenerator.ModelFunction2(::log, exact = false),
        MathTestsGenerator.ModelFunction1(::ln, exact = false, customTestPoint = E),
        MathTestsGenerator.ModelFunction1(::log10, exact = false, customTestPoint = 10.0),
        MathTestsGenerator.ModelFunction1(::log2, exact = false, customTestPoint = 2.0),
        MathTestsGenerator.ModelFunction1(::ln1p, customTestPoint = E),
        MathTestsGenerator.ModelFunction2(::powWrapper, exact = false),
    )
    MathTestsGenerator(
        outputFile = baseDir.resolve("powers.kt"),
        testPoints = listOf(0.0, 1.0),
        functions = powerFunctions
    ).generate()


    val roundingFunctions = listOf(
        MathTestsGenerator.ModelFunction1(::ceil),
        MathTestsGenerator.ModelFunction1(::floor),
        MathTestsGenerator.ModelFunction1(::truncate),
        MathTestsGenerator.ModelFunction1(::round),
    )
    MathTestsGenerator(
        outputFile = baseDir.resolve("roundings.kt"),
        testPoints = listOf(0.4, 0.5, 0.6, 1.0),
        functions = roundingFunctions
    ).generate()

}

internal fun FileWriter.writeHeader(outputFile: File) {
    println("Generating file: $outputFile")
    appendLine(COPYRIGHT_NOTICE)
    appendLine("package test.math")
    appendLine()
    appendLine(autoGeneratedWarning("mathTestGeneratorMain.kt"))
    appendLine()
    appendLine("import kotlin.math.*")
    appendLine("import kotlin.test.*")
    appendLine()
}

private fun generateUtils(outputFile: File) {
    FileWriter(outputFile).use { writer ->
        writer.writeHeader(outputFile)
        writer.appendLine()
        writer.write("""
            
//workaround about method with receiver
internal fun powWrapper(x: Double, y: Double): Double = x.pow(y)

private fun getMantissa(d: Double): Double = Double.fromBits(((d.toBits().toULong() and 0x800fffffffffffffUL) or 0x3ff0000000000000UL).toLong())
private fun getExp(d: Double): Int = (((d.toBits() shr 52) and 0x7FF) - 1023).toInt()

private fun compare(arg1: Double, arg2: Double?, result1: Double, result2: Double, exact: Boolean = false) {
    val difference: Any? = when {
        result1.isNaN() -> if (result2.isNaN()) null else result1
        result1.isInfinite() -> if (result2.isInfinite() && result1.sign == result2.sign) null else result1
        result2.isNaN() -> result2
        result2.isInfinite() -> result2
        else -> {
            if (exact) {
                if (result1.toBits() == result2.toBits()) null else abs(result1 - result2)
            } else {
                val (toCompare1, toCompare2) = when (getExp(result1) - getExp(result2)) {
                    0 -> getMantissa(result1) to getMantissa(result2)
                    1 -> getMantissa(result1) to getMantissa(result2) / 2.0
                    -1 -> getMantissa(result1) / 2.0 to getMantissa(result2)
                    else -> result1 to result2
                }
                abs(toCompare1 - toCompare2).takeIf { it > 1e-14 }
            }
        }
    }
    assertNull(difference, "ARG1 = " + arg1 + (if (arg2 == null) "" else " and ARG2 = " + arg2))
}

internal fun checkAnswers(function: Function1<Double, Double>, arguments: Array<ULong>, answers: Array<ULong>, exact: Boolean) {
    arguments.forEachIndexed { i, x ->
        val argument1 = Double.fromBits(x.toLong())
        val answer = Double.fromBits(answers[i].toLong())
        compare(argument1, null, answer, function(argument1), exact = exact)
    }
}

internal fun checkAnswers(function: Function2<Double, Double, Double>, arguments: Array<ULong>, answers: Array<ULong>, exact: Boolean) {
    arguments
        .flatMap { lhsElem -> arguments.map { rhsElem -> lhsElem to rhsElem } }
        .forEachIndexed { i, x ->
            val argument1 = Double.fromBits(x.first.toLong())
            val argument2 = Double.fromBits(x.second.toLong())
            val answer = Double.fromBits(answers[i].toLong())
            compare(argument1, argument2, answer, function(argument1, argument2), exact = exact)
        }
}
        """.trimIndent())
    }
}