package de.renew.draw.ui.impl.menus;

import java.awt.Component;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;

import CH.ifa.draw.DrawPlugin;
import CH.ifa.draw.IOHelper;
import CH.ifa.draw.io.CombinationFileFilter;
import CH.ifa.draw.io.DrawingFileHelper;
import CH.ifa.draw.io.NoFileFilter;
import CH.ifa.draw.util.CommandMenu;
import de.renew.draw.storables.api.StorableApi;
import de.renew.draw.storables.impl.drawings.DrawingExportFormatHolder;
import de.renew.draw.storables.ontology.Drawing;
import de.renew.draw.storables.ontology.exporting.AbstractDrawingExportFormatMulti;
import de.renew.draw.ui.api.ApplicationApi;
import de.renew.draw.ui.api.EditorApi;
import de.renew.draw.ui.api.MenuApi;
import de.renew.draw.ui.ontology.AbstractCommand;
import de.renew.draw.ui.ontology.StatusDisplayer;
import de.renew.ioontology.ExtensionFileFilter;
import de.renew.ioontology.FileFilter;
import de.renew.ioontology.MultiExtensionFileFilter;
import de.renew.ioontology.exporting.ExportFormat;
import de.renew.ioontology.exporting.ExportFormatListener;
import de.renew.util.StringUtil;

/**
 * Maintains the export menus and offers convenience methods to export to a specific format.
 */
public class ExportMenuHolder {
    /**
     * Creates log4j Logger for this class to represent logging information.
     */
    private static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(ExportMenuHolder.class);

    // Static export mode appendix
    private static final String APPENDIX_11 = " current drawing ...";
    private static final String APPENDIX_N1 = " all drawings (N to 1) ...";
    private static final String APPENDIX_NN = " all drawings (N to N)";

    private final CommandMenu _exportMenu;

    // The ExportMenu
    private final CommandMenu _exportMenu11;
    private final CommandMenu _exportMenuNN;
    private final CommandMenu _exportMenuN1;

    /**
     * Constructs and initializes the ExportMenuHolder.
     */
    public ExportMenuHolder() {
        _exportMenu = new CommandMenu("Export");
        _exportMenu.putClientProperty(MenuApi.ID_PROPERTY, "de.renew.draw.ui.menu.export");

        _exportMenu11 = new CommandMenu("Export current drawing");
        _exportMenuNN = new CommandMenu("Export all drawings (single file each)");
        _exportMenuN1 = new CommandMenu("Export all drawings (merged file)");
        buildExportAll();
        _exportMenu.add(_exportMenu11);
        _exportMenu.add(_exportMenuNN);
        _exportMenu.add(_exportMenuN1);

        DrawingExportFormatHolder.getInstance()
            .addExportFormatListener(createExportFormatListener());
    }


    /**
     * Returns the export menu for the export menu holder so that it can be registered in a menu.
     *
     * @return the export menu
     */
    public JMenu getExportMenu() {
        return _exportMenu;
    }

    private List<ExportFormat<Drawing>> getExportFormats() {
        return DrawingExportFormatHolder.getInstance().getExportFormats();
    }

    /**
     * Returns the file filters for all known export formats that the given drawing can be exported to.
     * @return Array of all FileFilters
     */
    private FileFilter[] fileFilterExport(Drawing drawing) {
        List<FileFilter[]> fileFilters = new ArrayList<>();
        List<ExportFormat<Drawing>> formats = getExportFormats();
        for (ExportFormat<Drawing> format : formats) {
            if (format.canExportObject(drawing)) {
                fileFilters.add(buildFileFilter(format));
            }
        }

        List<FileFilter> allFileFilters = new ArrayList<>();
        allFileFilters.add(new NoFileFilter());
        for (FileFilter[] element : fileFilters) {
            for (int pos = 0; pos < element.length; pos++) {
                FileFilter current = element[pos];

                // work around
                boolean exists = false;
                for (int pos2 = 0; pos2 <= pos; pos2++) {
                    if (current.equals(allFileFilters.get(pos2))) {
                        exists = true;
                        break;
                    }
                }
                if (!exists) {
                    allFileFilters.add(current);
                }
            }
        }

        return allFileFilters.toArray(new FileFilter[0]);
    }

    private static FileFilter[] buildFileFilter(ExportFormat<?> exportFormat) {
        FileFilter filter = exportFormat.fileFilter();
        List<FileFilter> list = new ArrayList<>();
        if (filter instanceof CombinationFileFilter comFilter) {
            list.addAll(comFilter.getFileFilters());
        } else if (filter instanceof MultiExtensionFileFilter multiFilter) {
            list.addAll(multiFilter.getFileFilters());
        } else {
            list.add(filter);
        }

        return list.toArray(new FileFilter[0]);
    }

    /**
     * Constructs the menu item exportAll by using a Command.
     */
    private void buildExportAll() {
        AbstractCommand command = new AbstractCommand("Export current drawing (any type)...") {
            @Override
            public void execute() {
                executeExportAll();

            }

            @Override
            public boolean isExecutable() {
                return super.isExecutable() && DrawPlugin.getGui() != null
                    && !(StorableApi.isDrawingNullDrawing(EditorApi.getCurrentDrawing()));
            }
        };
        _exportMenu.add(command);
    }

    private void executeExportAll() {
        Drawing currentDrawing = EditorApi.getCurrentDrawing();
        if (StorableApi.isDrawingNullDrawing(currentDrawing)) {
            ApplicationApi.showStatus("no drawing");
            return;
        }
        File path =
            getIOHelper().getSaveFile(null, fileFilterExport(currentDrawing), currentDrawing);
        if (path != null) {
            List<ExportFormat<?>[]> list = new ArrayList<>();
            for (int pos = 0; pos < getExportFormats().size(); pos++) {
                ExportFormat<?>[] formats = getExportFormats().get(pos).canExport(path);
                if (formats.length > 0) {
                    list.add(formats);
                }
            }
            Iterator<ExportFormat<?>[]> formatsIter = list.iterator();
            List<ExportFormat<?>> allFormats = new ArrayList<>();
            while (formatsIter.hasNext()) {
                ExportFormat<?>[] formatArray = formatsIter.next();
                for (ExportFormat<?> exportFormat : formatArray) {
                    if (exportFormat.canExportObject(currentDrawing)) {
                        if (new NoFileFilter().equals(exportFormat.fileFilter())) {
                            if ("".equals(StringUtil.getExtension(path.getPath()))) {
                                allFormats.add(exportFormat);
                            }
                        } else {
                            allFormats.add(exportFormat);
                        }
                    }
                }
            }
            ExportFormat<?> format = null;
            if (allFormats.size() == 1) {
                format = allFormats.get(0);
            } else if (allFormats.size() > 1) {
                Object choice = JOptionPane.showInputDialog(
                    null, "Choose", "ExportFormats", JOptionPane.OK_CANCEL_OPTION, null,
                    allFormats.toArray(), allFormats.get(0));
                if (choice != null) {
                    format = (ExportFormat<?>) choice;
                }
            }
            if (format != null) {
                saveDrawing(currentDrawing, (ExportFormat<Drawing>) format, path);
            } else {
                ApplicationApi.showStatus("no ExportFormat");
            }
        }
    }

    /**
     * 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
     * @param displ the StatusDisplayer the export message will be shown in
     */
    static void saveDrawings(
        List<Drawing> drawings, ExportFormat<Drawing> format, File path, StatusDisplayer displ)
    {
        try {
            Drawing[] array = drawings.toArray(new Drawing[0]);
            format.export(array, path);
            displ.showStatus("Exported " + path.getPath() + ".");
        } catch (Exception e) {
            LOGGER.error(e.getMessage());
            displ.showStatus(e.toString());
            LOGGER.debug(ExportMenuHolder.class.getSimpleName() + ": ", e);
        }
    }

    /**
     * Save an enumeration of drawings with the help of format.
     * @param drawings the drawings to be saved
     * @param format the export format to be used to save the drawings
     */
    private void saveDrawings(Enumeration<Drawing> drawings, ExportFormat<Drawing> format) {
        try {
            List<Drawing> drawingList = new ArrayList<>();
            while (drawings.hasMoreElements()) {
                Drawing drawing = drawings.nextElement();

                if (drawing.getFilename() == null) {
                    // save drawing
                    DrawPlugin.getGui().saveDrawingAs(drawing);
                } else {
                    // add Drawing to list
                    drawingList.add(drawing);
                }
            }

            // list to array
            Drawing[] drawingArray = drawingList.toArray(new Drawing[0]);

            // generate paths
            File[] paths = new File[drawingArray.length];
            for (int pos = 0; pos < drawingArray.length; pos++) {
                String name = drawingArray[pos].getName();
                File path = drawingArray[pos].getFilename().getCanonicalFile();
                String pathString = path.getParent() + File.separator + name;
                paths[pos] = DrawingFileHelper
                    .checkAndAddExtension(new File(pathString), getExtensionFileFilter(format));
            }

            if (drawingArray.length > 0) {
                format.exportAll(drawingArray, paths);
                ApplicationApi.showStatus("Exported.");
            }
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
            ApplicationApi.showStatus(e.toString());
        }
    }

    private ExtensionFileFilter getExtensionFileFilter(ExportFormat<?> exportFormat) {
        if (exportFormat == null || exportFormat.fileFilter() == null) {
            return null;
        }
        if (exportFormat.fileFilter() instanceof MultiExtensionFileFilter mff) {
            return new ExtensionFileFilter() {
                @Override
                public String getExtension() {
                    return mff.getPreferredFileFilter().getExtension();
                }

                @Override
                public boolean accept(File file) {
                    return mff.accept(file);
                }

                @Override
                public String getDescription() {
                    return mff.getDescription();
                }

                @Override
                public void setDescription(String description) {
                    mff.setDescription(description);
                }
            };
        } else if (exportFormat.fileFilter() instanceof ExtensionFileFilter eff) {
            return eff;
        } else {
            return new ExtensionFileFilter() {
                @Override
                public String getExtension() {
                    return "";
                }

                @Override
                public boolean accept(File file) {
                    return exportFormat.fileFilter().accept(file);
                }

                @Override
                public String getDescription() {
                    return exportFormat.fileFilter().getDescription();
                }

                @Override
                public void setDescription(String description) {
                    exportFormat.fileFilter().setDescription(description);
                }
            };
        }
    }


    /**
     * 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
     * @param sd the StatusDisplayer the export message will be shown in
     */
    static void saveDrawing(
        Drawing drawing, ExportFormat<Drawing> format, File path, StatusDisplayer sd)
    {
        try {
            File pathResult = format.export(drawing, path);
            sd.showStatus("Exported " + pathResult.getPath() + ".");
        } catch (Exception e) {
            sd.showStatus(e.getMessage());
            LOGGER.error(e.getMessage(), e);
        }
    }


    /**
     * Save an array of drawings with the help of format.
     * @param drawing The drawing to be saved
     * @param format The format
     * @param path The file to be saved to
     */
    private void saveDrawing(Drawing drawing, ExportFormat<Drawing> format, File path) {
        try {
            File pathResult = format.export(drawing, path);
            ApplicationApi.showStatus("Exported " + pathResult.getPath() + ".");
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
            ApplicationApi.showStatus(e.toString());
        }
    }

    /**
    * Constructs a menu item for this exportFormat in exportMenu.
    * @param exportFormat the ExportFormat for the new menu item
    * @param parent11 The parent menu from 1 to 1
    * @param parentNN The parent menu from n to n
    * @param parentN1 The parent menu from n to 1
    */
    private void buildExportFormat(
        ExportFormat<Drawing> exportFormat, CommandMenu parent11, CommandMenu parentNN,
        CommandMenu parentN1)
    {
        if (exportFormat instanceof AbstractDrawingExportFormatMulti drawingExportFormatMulti) {
            List<ExportFormat<Drawing>> formats = drawingExportFormatMulti.getExportFormats();
            String name = drawingExportFormatMulti.formatName();
            CommandMenu menu11 = new CommandMenu(name);
            CommandMenu menuNN = new CommandMenu(name);
            CommandMenu menuN1 = new CommandMenu(name);
            for (ExportFormat<Drawing> f : formats) {
                buildExportFormat(f, menu11, menuNN, menuN1);
            }
            parent11.add(menu11);
            parent11.addSeparator();
            parentNN.add(menuNN);
            parentNN.addSeparator();
            parentN1.add(menuN1);
            parentN1.addSeparator();
        } else {
            generateCommands(exportFormat, parent11, parentNN, parentN1);
        }
    }

    /**
     * Generates the export commands for the given format and adds them to the given menus.
     * @param format Export format
     * @param menu11 The menu from 1 to 1
     * @param menuNN The menu from n to n
     * @param menuN1 The menu from n to 1
     */
    private void generateCommands(
        ExportFormat<Drawing> format, CommandMenu menu11, CommandMenu menuNN, CommandMenu menuN1)
    {
        // 1 to 1
        {
            AbstractCommand command11 = createDrawingExportFormatCommand(
                format, APPENDIX_11, this::executeExport1to1, this::isExecutable1to1);
            if (format.getShortCut() == -1) {
                menu11.add(command11);
            } else if (format.getModifier() == -1) {
                menu11.add(command11, format.getShortCut());
            } else {
                menu11.add(command11, format.getShortCut(), format.getModifier());
            }
        }
        // n to n
        {
            AbstractCommand commandNN = createDrawingExportFormatCommand(
                format, APPENDIX_NN, this::executeNtoN, this::isExecutableNtoN);
            menuNN.add(commandNN);
        }

        // n to 1
        if (format.canExportNto1()) {
            AbstractCommand commandN1 = createDrawingExportFormatCommand(
                format, APPENDIX_N1, this::executeNto1, this::isExecutableNto1);
            menuN1.add(commandN1);
        }
    }

    private void executeExport1to1(ExportFormat<Drawing> format) {
        Drawing drawing = EditorApi.getCurrentDrawing();
        if (drawing != null && !(StorableApi.isDrawingNullDrawing(drawing))) {
            if (format.forceGivenName()) {
                if (drawing.getFilename() != null) {
                    try {
                        String fileNameText =
                            drawing.getFilename().getCanonicalPath() + drawing.getName();
                        fileNameText =
                            StringUtil.getPath(fileNameText) + File.separator + drawing.getName();
                        File file = new File(fileNameText);
                        saveDrawing(drawing, format, file);
                    } catch (IOException e) {
                        LOGGER.error("Could not create export file: ");
                        LOGGER.debug("Could not create export file: ", e);
                    }
                }
            } else {
                File path = getIOHelper()
                    .getSaveFile(null, new FileFilter[] { format.fileFilter() }, drawing);
                if (path != null) {
                    ApplicationApi.showStatus("Exporting " + path + " ...");
                    saveDrawing(drawing, format, path);
                }
            }
        } else {
            ApplicationApi.showStatus("no drawing");
        }
        EditorApi.toolDone();
    }

    private boolean isExecutable1to1(ExportFormat<Drawing> format) {
        return DrawPlugin.getGui() != null
            && !(StorableApi.isDrawingNullDrawing(EditorApi.getCurrentDrawing()))
            && format.canExportObject(EditorApi.getCurrentDrawing());
    }

    private void executeNto1(ExportFormat<Drawing> format) {
        Enumeration<Drawing> drawings = EditorApi.getCurrentDrawings();
        if (drawings != null) {
            if (drawings.hasMoreElements()) {
                File path = getIOHelper().getSaveFile(
                    null, new FileFilter[] { format.fileFilter() }, drawings.nextElement());
                if (path != null) {
                    ApplicationApi.showStatus("Exporting " + path + " ...");
                    saveDrawings(
                        Collections.list(EditorApi.getCurrentDrawings()), format, path,
                        DrawPlugin.getGui());
                }
            } else {
                ApplicationApi.showStatus("no drawing");
            }
        }
        EditorApi.toolDone();
        ApplicationApi.showStatus("export");
    }

    private boolean isExecutableNto1(ExportFormat<Drawing> format) {
        if (DrawPlugin.getGui() == null
            || StorableApi.isDrawingNullDrawing(EditorApi.getCurrentDrawing())) {
            return false;
        }

        Enumeration<Drawing> drawings = EditorApi.getCurrentDrawings();
        while (drawings.hasMoreElements()) {
            Drawing drawing = drawings.nextElement();
            if (!format.canExportObject(drawing)) {
                return false;
            }
        }

        return true;
    }

    private void executeNtoN(ExportFormat<Drawing> format) {
        ApplicationApi.showStatus("Exporting...");
        Enumeration<Drawing> drawings = EditorApi.getCurrentDrawings();
        if (drawings != null) {
            if (drawings.hasMoreElements()) {
                saveDrawings(drawings, format);
            } else {
                ApplicationApi.showStatus("no drawing");
            }
        }
        EditorApi.toolDone();
        ApplicationApi.showStatus("export");
    }

    private boolean isExecutableNtoN(ExportFormat<Drawing> format) {
        if (DrawPlugin.getGui() == null
            || StorableApi.isDrawingNullDrawing(EditorApi.getCurrentDrawing())) {
            return false;
        }

        Enumeration<Drawing> drawings = EditorApi.getCurrentDrawings();
        while (drawings.hasMoreElements()) {
            Drawing drawing = drawings.nextElement();
            if (!format.canExportObject(drawing)) {
                return false;
            }
        }
        return true;
    }

    void addExportFormat(ExportFormat<Drawing> exportFormat) {
        LOGGER.debug(getClass() + ": adding export format " + exportFormat);
        buildExportFormat(exportFormat, _exportMenu11, _exportMenuNN, _exportMenuN1);
    }


    void removeExportFormat(ExportFormat<Drawing> exportFormat) {
        LOGGER.debug(getClass() + ": removing export format " + exportFormat);

        Component[] ele = _exportMenu.getMenuComponents();
        for (Component component : ele) {
            if (component instanceof JMenuItem item) {
                if (item.getText().equals(exportFormat.formatName())) {
                    _exportMenu.remove(item);
                }
            }
        }
        removeExportFormatFromMenu(exportFormat, _exportMenu11, APPENDIX_11);
        removeExportFormatFromMenu(exportFormat, _exportMenuN1, APPENDIX_N1);
        removeExportFormatFromMenu(exportFormat, _exportMenuNN, APPENDIX_NN);

    }

    private void removeExportFormatFromMenu(
        ExportFormat<?> format, CommandMenu menu, String appendix)
    {

        Component[] ele = menu.getMenuComponents();
        for (Component component : ele) {
            if (component instanceof JMenuItem item) {
                if (item.getText().equals(format.formatName() + appendix)) {
                    menu.remove(item);
                }
            }
        }

    }

    private static IOHelper getIOHelper() {
        return Objects.requireNonNull(DrawPlugin.getCurrent()).getIOHelper();
    }

    /**
     * Creates a new drawing export format command for the given parameters.
     *
     * @param format the export format to be used
     * @param nameAppend the name to append to the format name to build the command name
     * @param executor the functionality to be executed when the command is executed
     * @param verifier the functionality to be executed to verify whether the command is executable
     * @return the created command
     */
    private AbstractCommand createDrawingExportFormatCommand(
        ExportFormat<Drawing> format, String nameAppend, Consumer<ExportFormat<Drawing>> executor,
        Predicate<ExportFormat<Drawing>> verifier)
    {
        return new AbstractCommand(format.formatName() + nameAppend) {

            @Override
            public void execute() {
                executor.accept(format);
            }

            @Override
            public boolean isExecutable() {
                return verifier.test(format);
            }
        };
    }

    private ExportFormatListener<Drawing> createExportFormatListener() {
        return new ExportFormatListener<>() {
            @Override
            public void exportFormatAdded(ExportFormat<Drawing> addedFormat) {
                addExportFormat(addedFormat);
            }

            @Override
            public void exportFormatRemoved(ExportFormat<Drawing> removedFormat) {
                removeExportFormat(removedFormat);
            }
        };
    }
}