/*
 * Copyright 2019 the original author or authors.
 *
 * 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.
 */

import data.Trie
import elmish.elementById
import elmish.mountComponentAt
import elmish.tree.Tree
import elmish.tree.TreeView
import kotlin.js.JSON.stringify


fun main() {
    mountComponentAt(
        elementById("report"),
        ConfigurationCacheReportPage,
        reportPageModelFromJsModel(configurationCacheProblems())
    )
}


/**
 * External model defined in `configuration-cache-report-data.js`, a file generated by `ConfigurationCacheReport`.
 */
private
external val configurationCacheProblems: () -> JsModel


private
external interface JsModel {
    val cacheAction: String
    val requestedTasks: String
    val documentationLink: String
    val totalProblemCount: Int
    val diagnostics: Array<JsDiagnostic>
}


/** A diagnostic is either an [input] or a [problem] description (not both). */
private
external interface JsDiagnostic {
    val input: Array<JsMessageFragment>?
    val problem: Array<JsMessageFragment>?
    val trace: Array<JsTrace>
    val documentationLink: String?
    val error: String?
}


private
external interface JsTrace {
    val kind: String
}


private
external interface JsTraceTask : JsTrace {
    val path: String
    val type: String
}


private
external interface JsTraceBean : JsTrace {
    val type: String
}


private
external interface JsTraceField : JsTrace {
    val name: String
    val declaringType: String
}


private
external interface JsTraceProperty : JsTrace {
    val name: String
    val task: String
}


private
external interface JSBuildLogic : JsTrace {
    val location: String
}


private
external interface JSBuildLogicClass : JsTrace {
    val type: String
}


private
external interface JsMessageFragment {
    val text: String?
    val name: String?
}


private
data class ImportedProblem(
    val problem: JsDiagnostic,
    val message: PrettyText,
    val trace: List<ProblemNode>
)


private
fun reportPageModelFromJsModel(jsModel: JsModel): ConfigurationCacheReportPage.Model {
    val diagnostics = importDiagnostics(jsModel.diagnostics)
    return ConfigurationCacheReportPage.Model(
        cacheAction = jsModel.cacheAction,
        requestedTasks = jsModel.requestedTasks,
        documentationLink = jsModel.documentationLink,
        totalProblems = jsModel.totalProblemCount,
        reportedProblems = diagnostics.problems.size,
        messageTree = treeModelFor(
            ProblemNode.Label("Problems grouped by message"),
            problemNodesByMessage(diagnostics.problems)
        ),
        locationTree = treeModelFor(
            ProblemNode.Label("Problems grouped by location"),
            problemNodesByLocation(diagnostics.problems)
        ),
        inputTree = treeModelFor(
            ProblemNode.Label("Inputs"),
            problemNodesByMessage(diagnostics.inputs)
        )
    )
}


private
class ImportedDiagnostics(
    val problems: List<ImportedProblem>,
    val inputs: List<ImportedProblem>
)


private
fun importDiagnostics(jsDiagnostics: Array<JsDiagnostic>): ImportedDiagnostics {
    val importedProblems = mutableListOf<ImportedProblem>()
    val importedInputs = mutableListOf<ImportedProblem>()
    for (diagnostic in jsDiagnostics) {
        diagnostic.input?.let {
            importedInputs.add(toImportedProblem(it, diagnostic))
        } ?: diagnostic.problem!!.let {
            importedProblems.add(toImportedProblem(it, diagnostic))
        }
    }
    return ImportedDiagnostics(importedProblems, importedInputs)
}


private
fun toImportedProblem(label: Array<JsMessageFragment>, jsProblem: JsDiagnostic) = ImportedProblem(
    jsProblem,
    label.let(::toPrettyText),
    jsProblem.trace.map(::toProblemNode)
)


private
fun problemNodesByMessage(problems: List<ImportedProblem>): Sequence<MutableList<ProblemNode>> =
    problems.asSequence().map { problem ->
        mutableListOf<ProblemNode>().apply {
            add(problemNodeFor(problem))
            addAll(problem.trace)
            addExceptionNode(problem)
        }
    }


private
fun problemNodesByLocation(problems: List<ImportedProblem>): Sequence<List<ProblemNode>> =
    problems.asSequence().map { problem ->
        mutableListOf<ProblemNode>().apply {
            addAll(problem.trace.asReversed())
            add(problemNodeFor(problem))
            addExceptionNode(problem)
        }
    }


private
fun MutableList<ProblemNode>.addExceptionNode(problem: ImportedProblem) {
    exceptionNodeFor(problem.problem)?.let {
        add(it)
    }
}


private
fun problemNodeFor(problem: ImportedProblem) = errorOrWarningNodeFor(
    problem.problem,
    messageNodeFor(problem),
    docLinkFor(problem.problem)
)


private
fun toPrettyText(message: Array<JsMessageFragment>) = PrettyText(
    message.map {
        it.text?.let(PrettyText.Fragment::Text)
            ?: it.name?.let(PrettyText.Fragment::Reference)
            ?: PrettyText.Fragment.Text("Unrecognised message fragment: ${stringify(it)}")
    }
)


private
fun toProblemNode(trace: JsTrace): ProblemNode = when (trace.kind) {
    "Task" -> trace.unsafeCast<JsTraceTask>().run {
        ProblemNode.Task(path, type)
    }
    "Bean" -> trace.unsafeCast<JsTraceBean>().run {
        ProblemNode.Bean(type)
    }
    "Field" -> trace.unsafeCast<JsTraceField>().run {
        ProblemNode.Property("field", name, declaringType)
    }
    "InputProperty" -> trace.unsafeCast<JsTraceProperty>().run {
        ProblemNode.Property("input property", name, task)
    }
    "OutputProperty" -> trace.unsafeCast<JsTraceProperty>().run {
        ProblemNode.Property("output property", name, task)
    }
    "BuildLogic" -> trace.unsafeCast<JSBuildLogic>().run {
        ProblemNode.BuildLogic(location)
    }
    "BuildLogicClass" -> trace.unsafeCast<JSBuildLogicClass>().run {
        ProblemNode.BuildLogicClass(type)
    }
    else -> ProblemNode.Label("Gradle runtime")
}


private
fun errorOrWarningNodeFor(problem: JsDiagnostic, label: ProblemNode, docLink: ProblemNode?): ProblemNode =
    problem.error?.let {
        ProblemNode.Error(label, docLink)
    } ?: ProblemNode.Warning(label, docLink)


private
fun messageNodeFor(importedProblem: ImportedProblem) =
    ProblemNode.Message(importedProblem.message)


private
fun exceptionNodeFor(it: JsDiagnostic): ProblemNode? =
    it.error?.let(ProblemNode::Exception)


private
fun docLinkFor(it: JsDiagnostic): ProblemNode? =
    it.documentationLink?.let { ProblemNode.Link(it, " ?") }


private
fun <T> treeModelFor(
    label: T,
    sequence: Sequence<List<T>>
): TreeView.Model<T> = TreeView.Model(
    treeFromTrie(
        label,
        Trie.from(sequence),
        Tree.ViewState.Collapsed
    )
)


private
fun <T> treeFromTrie(label: T, trie: Trie<T>, state: Tree.ViewState): Tree<T> {
    val subTreeState = if (trie.size == 1) Tree.ViewState.Expanded else Tree.ViewState.Collapsed
    return Tree(
        label,
        subTreesFromTrie(trie, subTreeState),
        // nodes with no children such as Exception nodes are considered `Collapsed` by default
        if (trie.size == 0) Tree.ViewState.Collapsed else state
    )
}


private
fun <T> subTreesFromTrie(trie: Trie<T>, state: Tree.ViewState): List<Tree<T>> =
    trie.entries.sortedBy { (label, _) -> label.toString() }.map { (label, subTrie) ->
        treeFromTrie(
            label,
            subTrie,
            state
        )
    }.toList()
