// SPDX-License-Identifier: BSD-3-Clause
package org.islandoftex.arara.ruleset

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.module.kotlin.readValue
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import java.io.File
import java.util.regex.Pattern
import org.islandoftex.arara.Arara
import org.islandoftex.arara.configuration.AraraSpec
import org.islandoftex.arara.filehandling.FileHandlingUtils
import org.islandoftex.arara.localization.LanguageController
import org.islandoftex.arara.localization.Messages
import org.islandoftex.arara.model.AraraException
import org.islandoftex.arara.utils.DisplayUtils
import org.slf4j.LoggerFactory

/**
 * Implements directive utilitary methods.
 *
 * @author Island of TeX
 * @version 5.0
 * @since 4.0
 */
@UseExperimental(kotlinx.serialization.ImplicitReflectionSerializer::class)
object DirectiveUtils {
    // the application messages obtained from the
    // language controller
    private val messages = LanguageController

    // get the logger context from a factory
    private val logger = LoggerFactory.getLogger(DirectiveUtils::class.java)

    /**
     * This function filters the lines of a file to identify the potential
     * directives.
     *
     * @param lines The lines of the file.
     * @return A map containing the line number and the line's content.
     */
    private fun getPotentialDirectiveLines(lines: List<String>):
            Map<Int, String> {
        val header = Arara.config[AraraSpec.Execution.onlyHeader]
        val validLineRegex = Arara.config[AraraSpec.Execution.filePattern]
        val validLinePattern = validLineRegex.toPattern()
        val validLineStartPattern = (validLineRegex + Arara.config[AraraSpec
                .Application.namePattern]).toPattern()
        val map = mutableMapOf<Int, String>()
        for ((i, text) in lines.withIndex()) {
            val validLineMatcher = validLineStartPattern.matcher(text)
            if (validLineMatcher.find()) {
                val line = text.substring(validLineMatcher.end())
                map[i + 1] = line

                logger.info(messages.getMessage(
                        Messages.LOG_INFO_POTENTIAL_PATTERN_FOUND,
                        i + 1, line.trim()))
            } else if (header && !checkLinePattern(validLinePattern, text)) {
                // if we should only look within the file's header and reached
                // a point where the line pattern does not match anymore, we
                // assume we have left the header and break
                break
            }
        }
        return map
    }

    /**
     * Extracts a list of directives from a list of strings.
     *
     * @param lines List of strings.
     * @return A list of directives.
     * @throws AraraException Something wrong happened, to be caught in the
     * higher levels.
     */
    @Throws(AraraException::class)
    @Suppress("MagicNumber")
    fun extractDirectives(lines: List<String>): List<Directive> {
        val pairs = getPotentialDirectiveLines(lines)
                .takeIf { it.isNotEmpty() }
                ?: throw AraraException(messages.getMessage(
                        Messages.ERROR_VALIDATE_NO_DIRECTIVES_FOUND))

        val assemblers = mutableListOf<DirectiveAssembler>()
        var assembler = DirectiveAssembler()
        val linebreakPattern = Arara.config[AraraSpec.Directive
                .linebreakPattern].toPattern()
        for ((lineno, content) in pairs) {
            val linebreakMatcher = linebreakPattern.matcher(content)
            if (linebreakMatcher.find()) {
                if (!assembler.isAppendAllowed) {
                    throw AraraException(
                            messages.getMessage(
                                    Messages.ERROR_VALIDATE_ORPHAN_LINEBREAK,
                                    lineno
                            )
                    )
                } else {
                    assembler.addLineNumber(lineno)
                    assembler.appendLine(linebreakMatcher.group(1))
                }
            } else {
                if (assembler.isAppendAllowed) {
                    assemblers.add(assembler)
                }
                assembler = DirectiveAssembler()
                assembler.addLineNumber(lineno)
                assembler.appendLine(content)
            }
        }
        if (assembler.isAppendAllowed) {
            assemblers.add(assembler)
        }

        return assemblers.map { generateDirective(it) }
    }

    /**
     * Generates a directive from a directive assembler.
     *
     * @param assembler The directive assembler.
     * @return The corresponding directive.
     * @throws AraraException Something wrong happened, to be caught in the
     * higher levels.
     */
    @Throws(AraraException::class)
    @Suppress("MagicNumber")
    fun generateDirective(assembler: DirectiveAssembler): Directive {
        val matcher = Arara.config[AraraSpec.Directive.directivePattern]
                .toPattern().matcher(assembler.getText())
        if (matcher.find()) {
            val directive = Directive(
                    identifier = matcher.group(1)!!,
                    parameters = getParameters(matcher.group(3),
                            assembler.getLineNumbers()),
                    conditional = Conditional(
                            type = getType(matcher.group(5)),
                            condition = matcher.group(6) ?: ""
                    ),
                    lineNumbers = assembler.getLineNumbers()
            )

            logger.info(messages.getMessage(
                    Messages.LOG_INFO_POTENTIAL_DIRECTIVE_FOUND, directive))

            return directive
        } else {
            throw AraraException(
                    messages.getMessage(
                            Messages.ERROR_VALIDATE_INVALID_DIRECTIVE_FORMAT,
                            "(" + assembler.getLineNumbers()
                                    .joinToString(", ") + ")"
                    )
            )
        }
    }

    /**
     * Gets the conditional type based on the input string.
     *
     * @param text The input string.
     * @return The conditional type.
     */
    private fun getType(text: String?): Conditional.ConditionalType {
        return when (text) {
            null -> Conditional.ConditionalType.NONE
            "if" -> Conditional.ConditionalType.IF
            "while" -> Conditional.ConditionalType.WHILE
            "until" -> Conditional.ConditionalType.UNTIL
            else -> Conditional.ConditionalType.UNLESS
        }
    }

    /**
     * Gets the parameters from the input string.
     *
     * @param text The input string.
     * @param numbers The list of line numbers.
     * @return A map containing the directive parameters.
     * @throws AraraException Something wrong happened, to be caught in the
     * higher levels.
     */
    @Throws(AraraException::class)
    private fun getParameters(
        text: String?,
        numbers: List<Int>
    ): Map<String, Any> {
        if (text == null)
            return mapOf()

        /* Before using kotlinx.serialization, there has been a dedicated
         * directive resolver which instructed SnakeYAML to do the following:
         *
         * addImplicitResolver(Tag.MERGE, MERGE, "<")
         * addImplicitResolver(Tag.NULL, NULL, "~nN\u0000")
         * addImplicitResolver(Tag.NULL, EMPTY, null)
         *
         * This has been removed.
         */
        return ObjectMapper(YAMLFactory()).registerKotlinModule().runCatching {
            readValue<Map<String, Any>>(text)
        }.getOrElse {
            throw AraraException(messages.getMessage(
                    Messages.ERROR_VALIDATE_YAML_EXCEPTION,
                    "(" + numbers.joinToString(", ") + ")"),
                    it)
        }
    }

    /**
     * Replicate a directive for given files.
     *
     * @param holder The list of files.
     * @param parameters The parameters for the directive.
     * @param directive The directive to clone.
     * @return List of cloned directives.
     * @throws AraraException If there is an error validating the [holder]
     *   object.
     */
    @Throws(AraraException::class)
    private fun replicateDirective(
        holder: Any,
        parameters: Map<String, Any>,
        directive: Directive
    ): List<Directive> {
        return if (holder is List<*>) {
            // we received a file list, so we map that list to files
            holder.filterIsInstance<Any>()
                    .asSequence()
                    .map { File(it.toString()) }
                    .map(FileHandlingUtils::getCanonicalFile)
                    // and because we want directives, we replicate our
                    // directive to be applied to that file
                    .map { reference ->
                        directive.copy(parameters = parameters
                                .plus("reference" to reference))
                    }
                    .toList()
                    // we take the result if and only if we have at least one
                    // file and we did not filter out any invalid argument
                    .takeIf { it.isNotEmpty() && holder.size == it.size }
            // TODO: check exception according to condition
                    ?: throw AraraException(
                            messages.getMessage(
                                    Messages.ERROR_VALIDATE_EMPTY_FILES_LIST,
                                    "(" + directive.lineNumbers
                                            .joinToString(", ") + ")"
                            )
                    )
        } else {
            throw AraraException(
                    messages.getMessage(
                            Messages.ERROR_VALIDATE_FILES_IS_NOT_A_LIST,
                            "(" + directive.lineNumbers.joinToString(", ") + ")"
                    )
            )
        }
    }

    /**
     * Validates the list of directives, returning a new list.
     *
     * @param directives The list of directives.
     * @return A new list of directives.
     * @throws AraraException Something wrong happened, to be caught in the
     * higher levels.
     */
    @Throws(AraraException::class)
    fun process(directives: List<Directive>): List<Directive> {
        val result = mutableListOf<Directive>()
        directives.forEach { directive ->
            val parameters = directive.parameters

            if (parameters.containsKey("reference"))
                throw AraraException(messages.getMessage(
                        Messages.ERROR_VALIDATE_REFERENCE_IS_RESERVED,
                        "(" + directive.lineNumbers.joinToString(", ") + ")"))

            if (parameters.containsKey("files")) {
                result.addAll(replicateDirective(parameters.getValue("files"),
                        parameters.minus("files"), directive))
            } else {
                result.add(directive.copy(parameters = parameters
                        .plus("reference" to
                                Arara.config[AraraSpec.Execution.reference])))
            }
        }

        logger.info(messages.getMessage(
                Messages.LOG_INFO_VALIDATED_DIRECTIVES))
        logger.info(DisplayUtils.displayOutputSeparator(
                messages.getMessage(Messages.LOG_INFO_DIRECTIVES_BLOCK)))
        result.forEach { logger.info(it.toString()) }
        logger.info(DisplayUtils.displaySeparator())

        return result
    }

    /**
     * Checks if the provided line contains the corresponding pattern, based on
     * the file type, or an empty line.
     *
     * @param pattern Pattern to be matched, based on the file type.
     * @param line Provided line.
     * @return Logical value indicating if the provided line contains the
     * corresponding pattern, based on the file type, or an empty line.
     */
    private fun checkLinePattern(pattern: Pattern, line: String): Boolean {
        return line.isBlank() || pattern.matcher(line).find()
    }
}
