package com.technia.tif.enovia.job.executors.file.external;

import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.apache.commons.lang3.StringUtils.trimToNull;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.technia.common.xml.XMLException;
import com.technia.common.xml.XMLReader;
import com.technia.tif.core.TIFRuntimeException;
import com.technia.tif.core.annotation.CopyToDocumentation;
import com.technia.tif.enovia.EnoviaModule;
import com.technia.tif.enovia.job.executors.file.StatusLog;
import com.technia.tvc.commons.io.FileUtils;
import com.technia.tvc.commons.lang.text.StrSubstitutor;

/**
 * Calls an external program that converts a file to other format.
 *
 * @since 2018.3.0
 */
public class ExternalFileConverter extends ExtCreatePDF {

    private static final String ARG_ELEMENT = "Arg";

    private static final String EXTENSION_ELEMENT = "Extension";

    private static final String EXTENSIONS_ELEMENT = "Extensions";

    private static final String COMMAND_ELEMENT = "Command";

    private static final String OUTPUT_FILE_ELEMENT = "OutputFile";

    private static final String TIMEOUT_ELEMENT = "Timeout";

    private static final String TIMEOUT_ARG = "timeout";

    private static final String OUTPUT_FILE_ARG = "outputFile";

    private static final String COMMANDS_ARG = "commands";

    private static final String EXTENSIONS_ARG = "extensions";

    private static final String PROPERTIES_FORMAT = "externalFileConverter.%s";

    private static final Logger logger = LoggerFactory.getLogger(ExternalFileConverter.class);

    private final List<String> extensions = new ArrayList<>();

    private final List<String> commands = new ArrayList<>();

    private String outputFile;

    private long timeout = -1L;

    public ExternalFileConverter() {
    }

    private void setTimeout(String value) {
        if (NumberUtils.isCreatable(value)) {
            this.timeout = NumberUtils.createLong(value);
        }
    }

    @Override
    public void initialize(XMLReader reader) throws XMLException {
        super.initialize(reader);
        /*
         * Init from global module.properties in case some property are not set
         */
        if (extensions.isEmpty()) {
            splitAdd(readGlobal(EXTENSIONS_ARG), extensions);
        }
        if (commands.isEmpty()) {
            splitAdd(readGlobal(COMMANDS_ARG), commands);
            if (commands.isEmpty()) {
                throw new XMLException("No commands have been specified");
            }
        }
        if (outputFile == null) {
            outputFile = readGlobal(OUTPUT_FILE_ARG);
            if (outputFile == null) {
                throw new XMLException("No output file have been specified");
            }
        }
        if (timeout == -1L) {
            setTimeout(readGlobal(TIMEOUT_ARG));
        }
    }

    /**
     * Parse the configuration
     */
    @Override
    protected boolean handleElement(XMLReader reader, String elementName) throws XMLException {
        switch (elementName) {
            case OUTPUT_FILE_ELEMENT:
                this.outputFile = trimToNull(reader.getElementText());
                return true;
            case COMMAND_ELEMENT:
                this.commands.add(trimToEmpty(reader.getElementText()));
                return true;
            case EXTENSION_ELEMENT:
                this.extensions.add(trimToEmpty(reader.getElementText()));
                return true;
            case EXTENSIONS_ELEMENT:
                splitAdd(trimToEmpty(reader.getElementText()), this.extensions);
                return true;
            case TIMEOUT_ELEMENT:
                setTimeout(reader.getElementText());
                return true;
            case ARG_ELEMENT:
                // Support specifying as <Arg> to be backward compatible.
                String name = reader.getAttributeValue("name"), value = reader.getAttributeValue("value");
                if (name != null) {
                    handleArg(name, value);
                }
                return true;
            default:
                return super.handleElement(reader, elementName);
        }
    }

    private void handleArg(String name, String value) {
        Consumer<String> logDeprecated = (replacedWith) -> {
            logger.info("The <Arg name=\"{}\"> element is deprecated. Prefer using <{}> instead", name, replacedWith);
        };
        switch (name) {
            case EXTENSIONS_ARG:
                logDeprecated.accept(EXTENSION_ELEMENT);
                splitAdd(value, this.extensions);
                break;
            case COMMANDS_ARG:
                logDeprecated.accept(EXTENSION_ELEMENT);
                splitAdd(value, this.commands);
                break;
            case OUTPUT_FILE_ARG:
                logDeprecated.accept(OUTPUT_FILE_ELEMENT);
                this.outputFile = value;
                break;
            case TIMEOUT_ARG:
                logDeprecated.accept(TIMEOUT_ELEMENT);
                setTimeout(value);
                break;
            default:
                logger.warn("Unknown argument detected: '{}' value set to '{}'", name, value);
                break;
        }
    }

    @Override
    protected void process(File input, String pdfFileName, StatusLog log) throws IOException {
        ensureAllowedFile(input, extensions);
        StrSubstitutor substitutor = getSettingsSubstitutor(input);
        List<String> commands = this.commands.stream().map(substitutor::replace).collect(Collectors.toList());
        String outputFile = substitutor.replace(this.outputFile);
        File workDir = input.getParentFile();
        executeProcess(workDir, commands, timeout, log);
        File destFile = new File(input.getParentFile(), pdfFileName);
        maybeRenameFile(new File(workDir, outputFile), destFile);
    }

    /**
     * Ensures the input file is supported by checking the configured list of
     * supported file extensions.
     *
     * @param file Input File
     * @param extensions List of supported file extensions
     */
    protected void ensureAllowedFile(File file, List<String> extensions) {
        final String ext = FilenameUtils.getExtension(file.getName());
        if (extensions.isEmpty() || extensions.stream().filter(a -> ext.equalsIgnoreCase(a)).findAny().isPresent()) {
            return;
        }
        throw new TIFRuntimeException("File is not supported: " + file.getName());
    }

    /**
     * Executes the converter process.
     *
     * @param workingDir Working directory
     * @param commands Commands for process builder
     * @param timeout Process timeout in seconds
     * @param log
     * @throws IOException
     */
    protected void executeProcess(File workingDir,
                                  List<String> commands,
                                  long timeout,
                                  StatusLog log) throws IOException {
        log.log("About to start process commands in working dir %s", workingDir.getAbsolutePath());
        commands.forEach(log::log);

        ProcessBuilder builder = new ProcessBuilder();
        Process p = builder.directory(workingDir).command(commands).start();
        try {
            log.log("Waiting for process to complete. (timeout=%d)", timeout);
            if (!p.waitFor(timeout, TimeUnit.MILLISECONDS)) {
                p.destroyForcibly();
                throw new TIFRuntimeException("Process failed to complete before timeout");
            }
            int exitValue = p.exitValue();
            log.log("Process exit code: %d", exitValue);
            if (exitValue != 0) {
                throw new TIFRuntimeException("Process ended with exit value " + exitValue);
            }
        } catch (InterruptedException e) {
            throw new TIFRuntimeException(e);
        }
    }

    /**
     * Renames the final output file according to expected target file so that
     * it could be handled further by TIF.
     *
     * @param workingDir Process working directory
     * @param srcFile Source file
     * @param destFile Dest file
     * @throws IOException
     */
    protected void maybeRenameFile(File srcFile, File destFile) throws IOException {
        if (!srcFile.exists()) {
            throw new IOException(String.format("Cannot find srcFile file %s", srcFile.getAbsolutePath()));
        }
        if (!srcFile.getName().equals(destFile.getName())) {
            logger.info("Renaming output file {} to {}", srcFile.getName(), destFile.getName());
            FileUtils.moveFile(srcFile, destFile);
        }
    }

    /**
     * Constructs a string substitutor that can be used to resolve setting
     * values including macros.
     *
     * @param file Input file.
     * @return String substitutor
     */
    protected StrSubstitutor getSettingsSubstitutor(File file) {
        Map<String, String> map = new HashMap<>();
        String fileName = file.getName();
        map.put("file", fileName);
        map.put("fileprefix", FilenameUtils.removeExtension(fileName));
        map.put("filesuffix", FilenameUtils.getExtension(fileName));
        map.put("iso", Boolean.toString(isISO190051Compliant()));
        map.put("scaletofit", Boolean.toString(isScaleToFit()));
        map.put("marginbottom", Integer.toString(getBottomMargin()));
        map.put("marginleft", Integer.toString(getLeftMargin()));
        map.put("marginright", Integer.toString(getRightMargin()));
        map.put("margintop", Integer.toString(getTopMargin()));
        return new StrSubstitutor(map);
    }

    private static void splitAdd(String value, List<String> into) {
        Arrays.asList(StringUtils.split(value, ";\n"))
            .stream()
            .map(StringUtils::trimToNull)
            .filter(Objects::nonNull)
            .forEach(into::add);
    }

    private static String readGlobal(String propertyName) {
        return EnoviaModule.getInstance().getSettings().getValue(String.format(PROPERTIES_FORMAT, propertyName));
    }
}