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

import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.io.OutputStream
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import org.islandoftex.arara.Arara
import org.islandoftex.arara.configuration.AraraSpec
import org.islandoftex.arara.localization.LanguageController
import org.islandoftex.arara.localization.Messages
import org.islandoftex.arara.model.AraraException
import org.islandoftex.arara.ruleset.Command
import org.islandoftex.arara.ruleset.Conditional
import org.slf4j.LoggerFactory
import org.zeroturnaround.exec.InvalidExitValueException
import org.zeroturnaround.exec.ProcessExecutor
import org.zeroturnaround.exec.listener.ShutdownHookProcessDestroyer

/**
 * Implements interpreter utilitary methods.
 *
 * @author Island of TeX
 * @version 5.0
 * @since 4.0
 */
object InterpreterUtils {
    // the application messages obtained from the
    // language controller
    private val messages = LanguageController

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

    /**
     * Checks if the current conditional has a prior evaluation.
     *
     * @param conditional The current conditional object.
     * @return A boolean value indicating if the current conditional has a prior
     * evaluation.
     */
    fun runPriorEvaluation(conditional: Conditional): Boolean {
        return if (Arara.config[AraraSpec.Execution.dryrun]) {
            false
        } else {
            when (conditional.type) {
                Conditional.ConditionalType.IF,
                Conditional.ConditionalType.WHILE,
                Conditional.ConditionalType.UNLESS -> true
                else -> false
            }
        }
    }

    @ExperimentalTime
    private fun getProcessExecutorForCommand(
        command: Command,
        buffer: OutputStream
    ):
            ProcessExecutor {
        val timeOutValue = Arara.config[AraraSpec.Execution.timeoutValue]
        var executor = ProcessExecutor().command((command).elements)
                .directory(command.workingDirectory.absoluteFile)
                .addDestroyer(ShutdownHookProcessDestroyer())
        if (Arara.config[AraraSpec.Execution.timeout]) {
            if (timeOutValue == Duration.ZERO) {
                throw AraraException(messages.getMessage(Messages
                        .ERROR_RUN_TIMEOUT_INVALID_RANGE))
            }
            executor = executor.timeout(timeOutValue.toLongNanoseconds(),
                    TimeUnit.NANOSECONDS)
        }
        val tee = if (Arara.config[AraraSpec.Execution.verbose]) {
            executor = executor.redirectInput(System.`in`)
            TeeOutputStream(System.out, buffer)
        } else {
            TeeOutputStream(buffer)
        }
        executor = executor.redirectOutput(tee).redirectError(tee)
        return executor
    }

    /**
     * Runs the command in the underlying operating system.
     *
     * @param command An object representing the command.
     * @return An integer value representing the exit code.
     * @throws AraraException Something wrong happened, to be caught in the
     * higher levels.
     */
    @ExperimentalTime
    @Throws(AraraException::class)
    fun run(command: Command): Int {
        val buffer = ByteArrayOutputStream()
        val executor = getProcessExecutorForCommand(command, buffer)
        return executor.runCatching {
            val exit = execute().exitValue
            logger.info(DisplayUtils.displayOutputSeparator(
                    messages.getMessage(Messages.LOG_INFO_BEGIN_BUFFER)))
            logger.info(buffer.toString())
            logger.info(DisplayUtils.displayOutputSeparator(
                    messages.getMessage(Messages.LOG_INFO_END_BUFFER)))
            exit
        }.getOrElse {
            throw AraraException(messages.getMessage(
                    when (it) {
                        is IOException -> Messages.ERROR_RUN_IO_EXCEPTION
                        is InterruptedException ->
                            Messages.ERROR_RUN_INTERRUPTED_EXCEPTION
                        is InvalidExitValueException ->
                            Messages.ERROR_RUN_INVALID_EXIT_VALUE_EXCEPTION
                        is TimeoutException ->
                            Messages.ERROR_RUN_TIMEOUT_EXCEPTION
                        else -> Messages.ERROR_RUN_GENERIC_EXCEPTION
                    }), it)
        }
    }

    /**
     * Builds the rule path based on the rule name and returns the corresponding
     * file location.
     *
     * @param name The rule name.
     * @return The rule file.
     * @throws AraraException Something wrong happened, to be caught in the
     * higher levels.
     */
    @Throws(AraraException::class)
    fun buildRulePath(name: String): File? {
        Arara.config[AraraSpec.Execution.rulePaths].forEach { path ->
            val location = File(construct(path, name))
            if (location.exists())
                return location
        }
        return null
    }

    /**
     * Constructs the path given the current path and the rule name.
     *
     * @param path The current path.
     * @param name The rule name.
     * @return The constructed path.
     * @throws AraraException Something wrong happened, to be caught in the
     * higher levels.
     */
    @Throws(AraraException::class)
    fun construct(path: String, name: String): String {
        val fileName = "$name.yaml"
        val location = File(path)
        return if (location.isAbsolute) {
            location.resolve(fileName).toString()
        } else {
            Arara.config[AraraSpec.Execution.workingDirectory]
                    // first resolve the path (rule path) against the working
                    // directory, then the rule name we want to resolve
                    .resolve(path).resolve(fileName).toAbsolutePath().toString()
        }
    }
}
