/**
 *  Licensed under GPL. For more information, see
 *    http://jaxodraw.sourceforge.net/license.html
 *  or the LICENSE file in the jaxodraw distribution.
 */
package net.sf.jaxodraw.io;

import java.awt.Component;

import java.beans.ExceptionListener;
import java.beans.XMLDecoder;
import java.beans.XMLEncoder;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import java.net.MalformedURLException;
import java.net.URL;

import javax.swing.JOptionPane;

import net.sf.jaxodraw.graph.JaxoGraph;
import net.sf.jaxodraw.graph.JaxoSaveGraph;
import net.sf.jaxodraw.gui.JaxoDialogs;
import net.sf.jaxodraw.gui.panel.JaxoChooseFile;
import net.sf.jaxodraw.util.JaxoPrefs;
import net.sf.jaxodraw.util.JaxoInfo;
import net.sf.jaxodraw.util.JaxoLanguage;
import net.sf.jaxodraw.util.JaxoLog;


/** The class responsible for opening/saving JaxoGraphs.
 * Also contains utility methods for file name management etc.
 * @since 2.0
 */
public final class JaxoIO {
    private final Component parent;

    /**
     * Constructor.
     * @param parentc The parent component.
     */
    public JaxoIO(final Component parentc) {
        this.parent = parentc;
    }

    /**
     * Convert 'f' to URL, and MalformedURLExceptions to IOExceptions.
     * @param f The file to convert.
     * @return The URL of the file.
     * @throws java.io.IOException If the file cannot be converted.
     */
    public static URL toURL(final File f) throws IOException {
        try {
            return f.toURI().toURL();
        } catch (MalformedURLException e) {
            throw new IOException("File " + f + " cannot be converted to URL", e);
        }
    }

    /**
     * Returns the absolute path of the given file name.
     * @param fileName The file name.
     * @return The absolute path.
     */
    public static String absoluteName(final String fileName) {
        return new File(fileName).getAbsolutePath();
    }

    /**
     * Returns the current directory as a string.
     * @return The current directory.
     */
    public static String currentDirectoryString() {
        return JaxoPrefs.getStringPref(JaxoPrefs.PREF_USER_DIR).concat(File.separator);
    }

    /**
     * Returns the absolute path of he directory that contains the fgiven file.
     * @param fileName The file name.
     * @return The absolute path of the directory.
     */
    public static String directoryString(final String fileName) {
        return new File(fileName).getParent().concat(File.separator);
    }

    /**
     * Returns the short name if the given file.
     * @param fileName The file name.
     * @return The short file name.
     */
    public static String shortName(final String fileName) {
        return new File(fileName).getName();
    }

    /**
     * Extension: part after the last '.' that does not
     * occur at the beginning, or "" if there is none.
     * Always: <code>f.equals(baseName(f) + "." + extension(f))</code>
     * @param fileName The file name.
     * @return The file extension.
     */
    public static String extension(final String fileName) {
        final int lp = fileName.lastIndexOf('.');

        // .xyz -> ""
        // xyz -> ""
        return (lp < 1) ? "" : fileName.substring(lp + 1);
    }

    /**
     * Basename: part before the last '.' that does not
     * occur at the beginning, or the fileName itself if
     * there is none.
     * @param fileName The file name.
     * @return The basename.
     */
    public static String baseName(final String fileName) {
        final int lp = fileName.lastIndexOf('.');

        // Do not convert ".xyz" -> ""
        //    but convert "xyz." -> ""
        return (lp < 1) ? fileName : fileName.substring(0, lp);
    }

    /**
     * fileName with "." + extension added unless it is already at the end
     *  or the fileName is empty.
     * @param name The file name.
     * @param extension The extension to add.
     * @return The full file name.
     */
    public static String withExtension(final String name, final String extension) {
        final StringBuffer fileName = new StringBuffer(name);
        if ((name.length() > 0) && !name.endsWith("." + extension)) {
            fileName.append('.').append(extension);
        }

        return fileName.toString();
    }

    /** Saves the specified JaxoGraph: if the current JaxoGraph has no
     * save file associated with it, asks for a save file name.
     * Othrewise saves the graph to the default file.
     * @param title title of graph (e.g. in tab)
     * @param fileName suggested file name (if the graph does not have one)
     * @param graph The graph to be saved.
     */
    public void save(final JaxoGraph graph, final String title, final String fileName) {
        if (graph.getSaveFileName().length() == 0) {
            final String saveFileName = getSaveFileName(title, fileName);

            if (saveFileName.length() != 0) {
                doSave(graph, title, saveFileName);
                graph.setSaved(true);
            }
        } else {
            doSave(graph, title, graph.getSaveFileName());
            graph.setSaved(true);
        }
    }

    /** Saves the specified JaxoGraph: asks for a save file.
     * @param title title of graph (e.g. in tab)
     * @param fileName suggested file name (if the graph does not have one)
     * @param graph The graph to be saved.
     */
    public void saveAs(final JaxoGraph graph, final String title, final String fileName) {
        final String saveFileName = getSaveFileName(title, fileName);

        if (saveFileName.length() != 0) {
            doSave(graph, title, saveFileName);
            graph.setSaved(true);
        }
    }

    /** Saves the specified JaxoGraph into the file fileName.
     * @param title The name of the current tab.
     * @param graph The graph to be saved.
     * @param fileName The save file name.
     */
    private void doSave(final JaxoGraph graph, final String title, final String fileName) {
        final String trimName = withExtension(fileName.trim(), JaxoInfo.EXTENSION);
        final File f = new File(trimName);

        graph.setSaveFileName(trimName);
        graph.getSaveGraph().setJaxoDrawVersion(JaxoInfo.VERSION_NUMBER);

        final Listener l = new Listener();

        OutputStream out = null;
        XMLEncoder encoder = null;

        try {
            try {
                out = new BufferedOutputStream(new FileOutputStream(trimName));
            } catch (FileNotFoundException e) {
                JaxoLog.debug(e);
                showSaveError(f, title, "JaxoIO.write%0OpenFailed%1");
                return;
            }

            encoder = new XMLEncoder(out);

            encoder.setExceptionListener(l);
            encoder.writeObject(graph.getSaveGraph());
        } finally {
            if (encoder == null) {
                if (out != null) {
                    try {
                        out.close();
                    } catch (IOException e) {
                        l.exceptionThrown(e);
                    }
                }
            } else {
                encoder.close();
            }
        }

        if (l.hasFailed()) {
            if (l.ioFailure() == null) {
                showSaveError(f, title, "JaxoIO.save%0SavingFailed%1");
            } else {
                showSaveError(f, title, "JaxoIO.write%0WriteFailed%1");
            }
        }
    }

    /** Show a dialog if 'f' exists, asking the user whether 'f' should be overwritten
     * and return the result.
     * Otherwise, return true.
     * @param parent parent component for the dialog.
     * @param f The file to test.
     * @return True if the file should be overwritten.
     */
    public static boolean shouldOverwrite(final Component parent, final File f) {
        return shouldOverwrite(parent, f, null);
    }

    /** Show a dialog if 'f' exists, asking the user whether 'f' should be overwritten
     * and return the result.
     * Otherwise, return true.
     * @param parent parent component for the dialog.
     * @param f The file to test.
     * @param dTitle of the question dialog, may be 'null', then a default title
     * will be used.
     * @return True if the file should be overwritten.
     */
    public static boolean shouldOverwrite(final Component parent, final File f,
        final String dTitle) {
        if (!f.exists()) {
            return true;
        }

        String title = dTitle;
        if (title == null) {
            title = JaxoLanguage.message("JaxoIO.overwrite.defaultTitle%0", f);
        }

        final String s1 = JaxoLanguage.translate("JaxoIO.overwrite.Overwrite");
        final String s2 = JaxoLanguage.translate("JaxoIO.overwrite.Cancel");
        final Object[] options = {s1, s2};

        return JOptionPane.showOptionDialog(parent,
            JaxoLanguage.message("JaxoIO.overwrite.message%0", f),
            JaxoDialogs.translatedWindowTitle(title),
            JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
            options, s1) == 0;
    }

    /**
     * Brings up a file chooser dialog to choose a file for saving.
     * @param title The name of the current tab
     * @param fileName The file name.
     * @return the chosen save file.
     */
    private String getSaveFileName(final String title, final String fileName) {
        final JaxoChooseFile c = new JaxoChooseFile(parent);
        c.setApproveText(JaxoLanguage.translate("JaxoIO.saveApproveText"));
        if (title.length() == 0) {
            c.setDialogTitle(JaxoLanguage.translate("JaxoIO.saveDialogTitle"));
        } else {
            c.setDialogTitle(JaxoLanguage.message(
                    "JaxoIO.saveNamed%0DialogTitle", title));
        }

        String saveFileName =
            c.chooseFile(new String[]{JaxoInfo.EXTENSION},
                JaxoLanguage.translate("JaxoIO.JaxoDrawFileDescription"),
                fileName);

        saveFileName = withExtension(saveFileName.trim(), JaxoInfo.EXTENSION);

        if ((saveFileName.length() > 0)
                && !shouldOverwrite(parent, new File(saveFileName))) {
            saveFileName = "";
        }

        return saveFileName;
    }

    private void showVersionMismatch(final String message, final String titleKey) {
        JOptionPane.showMessageDialog(parent, message,
            JaxoDialogs.translatedWindowTitle(titleKey),
            JOptionPane.ERROR_MESSAGE);
    }

    private void showError(final File f, final String key, final String title) {
        JOptionPane.showMessageDialog(parent, JaxoLanguage.message(key, f),
            JaxoDialogs.translatedWindowTitle(title), JOptionPane.ERROR_MESSAGE);
    }

    private void showOpenError(final File f, final String key) {
        showError(f, key,
            JaxoLanguage.message("JaxoIO.open%0ErrorTitle", new Object[]{f}));
    }

    private void showSaveError(final File f, final String title, final String key) {
        showError(f, key,
            JaxoLanguage.message("JaxoIO.save%0ErrorTitle", new Object[]{f, title}));
    }

    /** Opens a new JaxoGraph: asks for an open file.
     * @return The new JaxoGraph or null if the file could not be opened.
     */
    public JaxoGraph open() {
        final String openFileName = getOpenFileName();

        if (openFileName.length() != 0) {
            return open(openFileName);
        }

        return null;
    }

    /** Opens a new JaxoGraph from the specified file fileName.
     * @param name An absolute path to the file to be opened.
     * @return The new JaxoGraph or null if the file could not be opened.
     */
    public JaxoGraph open(final String name) {
        return open(new File(name));
    }

    /** Opens a new JaxoGraph from the specified file.
     * @param f A file to be opened.
     * @return The new JaxoGraph or null if the file could not be opened.
     */
    public JaxoGraph open(final File f) {
        return open(f, true);
    }

    /**
     * Opens a new JaxoGraph from the specified file.
     *
     * @param f A file to be opened.
     * @param guiWarnings set to false to suppress any graphical warning dialogs.
     * @return The new JaxoGraph or null if the file could not be opened.
     */
    public JaxoGraph open(final File f, final boolean guiWarnings) {
        final String fileName = f.getAbsolutePath();

        JaxoSaveGraph newSaveGraph = null;

        InputStream in = null;

        try {
            in = new BufferedInputStream(new FileInputStream(fileName));
        } catch (FileNotFoundException e) {
            JaxoLog.debug(e);

            if (guiWarnings) {
                if (f.exists()) {
                    showOpenError(f, "JaxoIO.read%0OpenFailed");
                } else {
                    showOpenError(f, "JaxoIO.read%0NotFound");
                }
            }
            return null;
        }

        final Listener l = new Listener();

        final XMLDecoder decoder = new XMLDecoder(in, null, l);

        try {
            newSaveGraph = (JaxoSaveGraph) decoder.readObject();
        } finally {
            decoder.close();
        }

        if (l.hasFailed()) {
            if (guiWarnings) {
                if (l.ioFailure() == null) {
                    showOpenError(f, "JaxoIO.open%0ParsingFailed");
                } else {
                    showOpenError(f, "JaxoIO.read%0ReadFailed");
                }
            }

            return null;
        }

        final JaxoGraph openedGraph = new JaxoGraph();

        if (newSaveGraph != null) {
            openedGraph.setSaveGraph(newSaveGraph);

            final String fileVersion = newSaveGraph.getJaxoDrawVersion();
            if (JaxoInfo.compareVersion(fileVersion) != 0 && guiWarnings) {
                showVersionMismatch(
                    JaxoLanguage.message("JaxoIO.versionMismatch%0%1%2",
                        new Object[]{f, fileVersion, JaxoInfo.VERSION_NUMBER}),
                    JaxoLanguage.message("JaxoIO.open%0versionMismatchTitle",
                        new Object[]{f}));
            }
        }

        openedGraph.setSaved(true);
        openedGraph.setSaveFileName(fileName);

        return openedGraph;
    }

    /** Brings up a file chooser dialog to choose a file for opening.
     * Checks whether there is a non-empty current graph, it will be destroyed.
     * @return The chosen open file name.
     */
    private String getOpenFileName() {
        String openFileName = "";

        final JaxoChooseFile c = new JaxoChooseFile(parent);
        c.setApproveText(JaxoLanguage.translate("JaxoIO.openApproveText"));
        c.setDialogTitle(JaxoLanguage.translate("JaxoIO.openDialogTitle"));
        openFileName =
            c.chooseFile(new String[]{JaxoInfo.EXTENSION},
                JaxoLanguage.translate("JaxoIO.JaxoDrawFileDescription"), "");

        return openFileName;
    }

    private static class Listener implements ExceptionListener {
        private IOException ioFailure;
        private Exception lastException;

        public void exceptionThrown(final Exception e) {
            JaxoLog.debug(e);
            if (e instanceof IOException) {
                ioFailure = (IOException) e;
            }
            lastException = e;
        }

        public final Exception ioFailure() {
            return ioFailure;
        }

        public final Exception lastException() {
            return lastException;
        }

        public boolean hasFailed() {
            return lastException != null;
        }
    }
}
