package de.renew.export.io.exportFormats;

import java.io.File;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;

import CH.ifa.draw.io.DrawingFileHelper;
import CH.ifa.draw.io.StatusDisplayer;
import de.renew.draw.storables.api.StorableApi;
import de.renew.draw.storables.ontology.Drawing;
import de.renew.ioontology.ExtensionFileFilter;
import de.renew.ioontology.FileFilter;
import de.renew.ioontology.exporting.ExportFormat;
import de.renew.ioontology.exporting.ExportFormatMultiAbstract;
import de.renew.plugin.command.CLCommand;


/**
 * Exports a drawing to an available export format.
 *
 * @author Lawrence Cabac
 *
 */
public class ExportClCommand implements CLCommand, StatusDisplayer {
    private static final String SYNOPSIS =
        "Exports a drawing. Usage: ex <type> [drawing]+ [options]";
    private static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(ExportClCommand.class);

    /* (non-Javadoc)
     * @see de.renew.plugin.command.CLCommand#execute(java.lang.String[], java.io.PrintStream)
     */
    @Override
    public void execute(String[] args, PrintStream response) {
        CommandLineParser parser = new DefaultParser();
        Options opts = new Options();
        opts.addOption(
            "a", "accumulate", false,
            "n-to-1 export (only available for some formats, e.g. ShadowNetSystem)");
        opts.addOption("o", "output", true, "output file");

        List<ExportFormat<Drawing>> formats = flattenedExportFormats();

        CommandLine line = null;
        try {
            line = parser.parse(opts, args);
        } catch (ParseException e1) {
            response.append("Could not parse command.\n");
            printHelp(response, formats, opts);
            return;
        }
        List<String> argList = line.getArgList();

        if (argList.size() < 2) {
            response.append("Not enough arguments.\n");
            printHelp(response, formats, opts);
            return;
        }

        String type = argList.remove(0);
        List<String> files = argList;

        boolean accumulate = line.hasOption("a");

        // multiple files and custom output file, but not a n-to-1 export
        if (files.size() > 1 && line.hasOption("o") && !accumulate) {
            response.append(
                "Cannot export multiple drawings with custom output file (except for n-to-1 export).\n");
            printHelp(response, formats, opts);
            return;
        }

        ExportFormat<Drawing> exportFormat = null;
        String extension = type;

        for (ExportFormat<Drawing> format : formats) {
            LOGGER.debug(format);
            if (format.formatName().equalsIgnoreCase(type)) {
                exportFormat = format;
                LOGGER.info(
                    ExportClCommand.class.getSimpleName() + ": format is " + format.formatName());
                // if it's an ExtensionFileFilter use the defined extension
                FileFilter fileFilter = format.fileFilter();
                if (fileFilter instanceof ExtensionFileFilter) {
                    extension = ((ExtensionFileFilter) fileFilter).getExtension();
                }
                break; // we found our format
            }
        }

        if (exportFormat == null) {
            response.append("Export format not found.\n");
            return;
        }

        if (accumulate) {
            if (!exportFormat.canExportNto1()) {
                response.append("Export format does not support n-to-1 export.\n");
                return;
            }


            // do a n-t-1 export, i.e. accumulate all nets in one system (e.g. sns) 
            List<Drawing> drawings = new ArrayList<Drawing>();
            for (String filename : files) {
                LOGGER.info(
                    ExportClCommand.class.getSimpleName() + ": add filename to n-to-1 export "
                        + filename);
                File file = new File(filename);
                if (file.exists()) {
                    drawings.add(DrawingFileHelper.loadDrawing(file, this));
                }
            }
            File exportFile;
            if (line.hasOption("o")) {
                exportFile = new File(line.getOptionValue("o"));
            } else {
                exportFile = new File(drawings.get(0).getName() + ".sns");
            }
            saveDrawings(drawings, exportFormat, exportFile);
            LOGGER.info(
                ExportClCommand.class.getSimpleName() + ": exported n-to-1  "
                    + exportFile.getAbsolutePath());

        } else {
            if (line.hasOption("o")) {
                exportSingleDrawing(
                    exportFormat, extension, files.get(0), line.getOptionValue("o"));
            } else {
                // export each file one by one
                for (String filename : files) {
                    exportSingleDrawing(exportFormat, extension, filename, null);
                }
            }
        }
    }

    /**
     * Flattens all multi ExportFormats that are in the drawing plugin and puts them into a list.
     *
     * @return List contains all ExportFormats of current drawing sorted by export type
     */
    public List<ExportFormat<Drawing>> flattenedExportFormats() {
        List<ExportFormat<Drawing>> formats = StorableApi.getExportFormats();
        Queue<ExportFormat<Drawing>> exportFormatsQueue = new LinkedList<>(formats);
        List<ExportFormat<Drawing>> flattenedExportFormats = new ArrayList<>();
        while (!exportFormatsQueue.isEmpty()) {
            ExportFormat<Drawing> format = exportFormatsQueue.poll();
            if (format instanceof ExportFormatMultiAbstract<Drawing> multiFormat) {
                exportFormatsQueue.addAll(multiFormat.getExportFormats());
            } else {
                flattenedExportFormats.add(format);
            }
        }
        flattenedExportFormats.sort(Comparator.comparing(ExportFormat::formatName));
        return flattenedExportFormats;
    }

    private void exportSingleDrawing(
        ExportFormat<Drawing> format, String extension, String filename, String outputFile)
    {
        LOGGER.info(ExportClCommand.class.getName() + ": filename is " + filename);
        File file = new File(filename);
        if (file.exists()) {
            Drawing drawing = DrawingFileHelper.loadDrawing(file, this);
            LOGGER.info(ExportClCommand.class.getName() + ": drawing is " + drawing.getName());
            File exportFile;
            if (outputFile == null) {
                exportFile = new File(
                    drawing.getFilename().getAbsoluteFile().getParentFile(),
                    drawing.getName() + "." + extension);
            } else {
                exportFile = new File(outputFile);
            }
            LOGGER.info(ExportClCommand.class.getName() + ": path is " + exportFile);
            saveDrawing(drawing, format, exportFile);
        }
    }

    /* (non-Javadoc)
     * @see de.renew.plugin.command.CLCommand#getDescription()
     */
    @Override
    public String getDescription() {
        return SYNOPSIS + " Type ex for a list of supported formats and options.";
    }

    /* (non-Javadoc)
     * @see CH.ifa.draw.io.StatusDisplayer#showStatus(java.lang.String)
     */
    @Override
    public void showStatus(String message) {
        LOGGER.info(ExportClCommand.class.getName() + " Status: " + message);
    }

    /**
     * @see de.renew.plugin.command.CLCommand#getArguments()
     */
    @Override
    public String getArguments() {
        List<ExportFormat<Drawing>> formats = flattenedExportFormats();
        String arguments = "";
        if (formats.size() > 0) {
            StringBuilder sb = new StringBuilder();
            sb.append("(");
            for (int i = 0; i < formats.size(); i++) {
                ExportFormat<Drawing> format = formats.get(i);
                sb.append(format.formatName());
                if (i < formats.size() - 1) {
                    sb.append("|");
                }
            }
            sb.append(")");
            sb.append(" ");
            arguments = sb.toString();
        }
        arguments += "[fileNames|-o|-a]*";
        return arguments;
    }

    private void printHelp(
        PrintStream response, List<ExportFormat<Drawing>> formats, Options opts)
    {
        HelpFormatter formatter = new HelpFormatter();
        String header = "Exports a drawing.";
        formatter.printHelp(
            new PrintWriter(response, true), HelpFormatter.DEFAULT_WIDTH, "ex <type> [drawing]+",
            header, opts, HelpFormatter.DEFAULT_LEFT_PAD, HelpFormatter.DEFAULT_DESC_PAD, null,
            true);
        //        response.append(SYNOPSIS + "\n");
        response.append("List of available formats:\n");
        for (ExportFormat<Drawing> exportFormat : formats) {
            response.append(ExportClCommand.class.getSimpleName() + ": " + exportFormat + "\n");
            if (exportFormat.canExportNto1()) {
                response.append(
                    ExportClCommand.class.getSimpleName() + ": " + exportFormat
                        + " -a file.rnw [morefiles.rnw] (n-to-1)\n");
            }
        }
    }

    /**
     * Save a drawing with the help of the given format.
     * @param drawing the drawing to be saved
     * @param format the format in which the drawing is to be saved
     * @param path the filename of the file the drawing is to be saved to
     */
    private void saveDrawing(Drawing drawing, ExportFormat<Drawing> format, File path) {
        try {
            File pathResult = format.export(drawing, path);
            this.showStatus("Exported " + pathResult.getPath() + ".");
        } catch (Exception e) {
            this.showStatus(e.getMessage());
            LOGGER.error(e.getMessage(), e);
        }
    }

    /**
     * Save an enumeration of drawings with the help of format (n to 1).
     * @param drawings the enumeration of drawings to be saved
     * @param format the format in which the enumeration is to be saved
     * @param path the filename of the file the enumeration is to be saved to
     */
    private void saveDrawings(List<Drawing> drawings, ExportFormat<Drawing> format, File path) {
        try {
            Drawing[] array = drawings.toArray(new Drawing[0]);
            format.export(array, path);
            this.showStatus("Exported " + path.getPath() + ".");
        } catch (Exception e) {
            LOGGER.error(e.getMessage());
            this.showStatus(e.toString());
            LOGGER.debug(this.getClass().getSimpleName() + ": ", e);
        }
    }
}