/*  Copyright (C) 2003-2011 JabRef contributors.
    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package net.sf.jabref.export;

import com.jgoodies.forms.builder.DefaultFormBuilder;
import com.jgoodies.forms.layout.FormLayout;
import net.sf.jabref.*;
import net.sf.jabref.gui.FileDialogs;
import net.sf.jabref.collab.ChangeScanner;

import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Vector;

/**
 * Action for the "Save" and "Save as" operations called from BasePanel. This class is also used for
 * save operations when closing a database or quitting the applications.
 *
 * The operations run synchronously, but offload the save operation from the event thread using Spin.
 * Callers can query whether the operation was cancelled, or whether it was successful.
 */
public class SaveDatabaseAction extends AbstractWorker {
    private BasePanel panel;
    private JabRefFrame frame;
    private boolean success = false, cancelled = false, fileLockedError = false;

    public SaveDatabaseAction(BasePanel panel) {

        this.panel = panel;
        this.frame = panel.frame();
    }


    public void init() throws Throwable {
        success = false;
        cancelled = false;
        fileLockedError = false;
        if (panel.getFile() == null)
            saveAs();
        else {

            // Check for external modifications:
            if (panel.isUpdatedExternally() || Globals.fileUpdateMonitor.hasBeenModified(panel.getFileMonitorHandle())) {

                String[] opts = new String[]{Globals.lang("Review changes"), Globals.lang("Save"),
                        Globals.lang("Cancel")};
                int answer = JOptionPane.showOptionDialog(panel.frame(), Globals.lang("File has been updated externally. "
                        + "What do you want to do?"), Globals.lang("File updated externally"),
                        JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
                        null, opts, opts[0]);
                //  int choice = JOptionPane.showConfirmDialog(frame, Globals.lang("File has been updated externally. "
                // +"Are you sure you want to save?"), Globals.lang("File updated externally"),
                // JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);

                if (answer == JOptionPane.CANCEL_OPTION) {
                    cancelled = true;
                    return;
                }
                else if (answer == JOptionPane.YES_OPTION) {
                    //try {

                    cancelled = true;

                    (new Thread(new Runnable() {
                        public void run() {

                            if (!Util.waitForFileLock(panel.getFile(), 10)) {
                                // TODO: GUI handling of the situation when the externally modified file keeps being locked.
                                System.err.println("File locked, this will be trouble.");
                            }

                            ChangeScanner scanner = new ChangeScanner(panel.frame(), panel);
                            scanner.changeScan(panel.getFile());
                            try {
                                scanner.join();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            if (scanner.changesFound()) {
                                scanner.displayResult(new ChangeScanner.DisplayResultCallback() {
                                    public void scanResultsResolved(boolean resolved) {
                                        if (!resolved) {
                                            cancelled = true;
                                        } else {
                                            panel.setUpdatedExternally(false);
                                            SwingUtilities.invokeLater(new Runnable() {
                                                public void run() {
                                                    panel.getSidePaneManager().hide("fileUpdate");
                                                }
                                            });
                                        }
                                    }
                                });
                            }
                        }
                    })).start();

                    return;
                }
                else { // User indicated to store anyway.
                    // See if the database has the protected flag set:
                    Vector<String> pd = panel.metaData().getData(Globals.PROTECTED_FLAG_META);
                    boolean databaseProtectionFlag = (pd != null) && Boolean.parseBoolean(pd.get(0));
                    if (databaseProtectionFlag) {
                        JOptionPane.showMessageDialog(frame, Globals.lang("Database is protected. Cannot save until external changes have been reviewed."),
                                Globals.lang("Protected database"), JOptionPane.ERROR_MESSAGE);
                        cancelled = true;
                    }
                    else {
                        panel.setUpdatedExternally(false);
                        panel.getSidePaneManager().hide("fileUpdate");
                    }
                }
            }

            panel.frame().output(Globals.lang("Saving database") + "...");
            panel.setSaving(true);
        }
    }

    public void update() {
        if (success) {
            // Reset title of tab
            frame.setTabTitle(panel, panel.getFile().getName(),
                    panel.getFile().getAbsolutePath());
            frame.output(Globals.lang("Saved database") + " '"
                    + panel.getFile().getPath() + "'.");
            frame.setWindowTitle();
        } else if (!cancelled) {
            if (fileLockedError) {
                // TODO: user should have the option to override the lock file.
                frame.output(Globals.lang("Could not save, file locked by another JabRef instance."));
            } else
                frame.output(Globals.lang("Save failed"));
        }
    }

    public void run() {
        if (cancelled || (panel.getFile() == null)) {
            return;
        }

        try {

            // Make sure the current edit is stored:
            panel.storeCurrentEdit();

            // If the option is set, autogenerate keys for all entries that are
            // lacking keys, before saving:
            panel.autoGenerateKeysBeforeSaving();

            if (!Util.waitForFileLock(panel.getFile(), 10)) {
                success = false;
                fileLockedError = true;
            }
            else {
                // Now save the database:
                success = saveDatabase(panel.getFile(), false, panel.getEncoding());

                //Util.pr("Testing resolve string... BasePanel line 237");
                //Util.pr("Resolve aq: "+database.resolveString("aq"));
                //Util.pr("Resolve text: "+database.resolveForStrings("A text which refers to the string #aq# and #billball#, hurra."));

                try {
                    Globals.fileUpdateMonitor.updateTimeStamp(panel.getFileMonitorHandle());
                } catch (IllegalArgumentException ex) {
                    // This means the file has not yet been registered, which is the case
                    // when doing a "Save as". Maybe we should change the monitor so no
                    // exception is cast.
                }
            }
            panel.setSaving(false);
            if (success) {
                panel.undoManager.markUnchanged();

                if (!AutoSaveManager.deleteAutoSaveFile(panel)) {
                    //System.out.println("Deletion of autosave file failed");
                }/* else
                    System.out.println("Deleted autosave file (if it existed)");*/
                // (Only) after a successful save the following
                // statement marks that the base is unchanged
                // since last save:
                panel.setNonUndoableChange(false);
                panel.setBaseChanged(false);
                panel.setUpdatedExternally(false);
            }
        } catch (SaveException ex2) {
            if (ex2 == SaveException.FILE_LOCKED) {
                success =false;
                fileLockedError = true;
                return;
            }
            ex2.printStackTrace();
        }
    }

    private boolean saveDatabase(File file, boolean selectedOnly, String encoding) throws SaveException {
        SaveSession session;
        frame.block();
        try {
            if (!selectedOnly)
                session = FileActions.saveDatabase(panel.database(), panel.metaData(), file,
                        Globals.prefs, false, false, encoding, false);
            else
                session = FileActions.savePartOfDatabase(panel.database(), panel.metaData(), file,
                        Globals.prefs, panel.getSelectedEntries(), encoding);

        } catch (UnsupportedCharsetException ex2) {
            JOptionPane.showMessageDialog(frame, Globals.lang("Could not save file. "
                    + "Character encoding '%0' is not supported.", encoding),
                    Globals.lang("Save database"), JOptionPane.ERROR_MESSAGE);
            throw new SaveException("rt");
        } catch (SaveException ex) {
            if (ex == SaveException.FILE_LOCKED) {
                throw ex;
            }
            if (ex.specificEntry()) {
                // Error occured during processing of
                // be. Highlight it:
                int row = panel.mainTable.findEntry(ex.getEntry()),
                        topShow = Math.max(0, row - 3);
                panel.mainTable.setRowSelectionInterval(row, row);
                panel.mainTable.scrollTo(topShow);
                panel.showEntry(ex.getEntry());
            } else ex.printStackTrace();

            JOptionPane.showMessageDialog
                    (frame, Globals.lang("Could not save file")
                            + ".\n" + ex.getMessage(),
                            Globals.lang("Save database"),
                            JOptionPane.ERROR_MESSAGE);
            throw new SaveException("rt");

        } finally {
            frame.unblock();
        }

        boolean commit = true;
        if (!session.getWriter().couldEncodeAll()) {
            DefaultFormBuilder builder = new DefaultFormBuilder(new FormLayout("left:pref, 4dlu, fill:pref", ""));
            JTextArea ta = new JTextArea(session.getWriter().getProblemCharacters());
            ta.setEditable(false);
            builder.append(Globals.lang("The chosen encoding '%0' could not encode the following characters: ",
                    session.getEncoding()));
            builder.append(ta);
            builder.append(Globals.lang("What do you want to do?"));
            String tryDiff = Globals.lang("Try different encoding");
            int answer = JOptionPane.showOptionDialog(frame, builder.getPanel(), Globals.lang("Save database"),
                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null,
                    new String[]{Globals.lang("Save"), tryDiff, Globals.lang("Cancel")}, tryDiff);

            if (answer == JOptionPane.NO_OPTION) {
                // The user wants to use another encoding.
                Object choice = JOptionPane.showInputDialog(frame, Globals.lang("Select encoding"), Globals.lang("Save database"),
                        JOptionPane.QUESTION_MESSAGE, null, Globals.ENCODINGS, encoding);
                if (choice != null) {
                    String newEncoding = (String) choice;
                    return saveDatabase(file, selectedOnly, newEncoding);
                } else
                    commit = false;
            } else if (answer == JOptionPane.CANCEL_OPTION)
                commit = false;


        }

        try {
            if (commit) {
                session.commit();
                panel.setEncoding(encoding); // Make sure to remember which encoding we used.
            } else
                session.cancel();
        } catch (SaveException e) {
            int ans = JOptionPane.showConfirmDialog(null, Globals.lang("Save failed during backup creation")+". "
                +Globals.lang("Save without backup?"), Globals.lang("Unable to create backup"),
                    JOptionPane.YES_NO_OPTION);
            if (ans == JOptionPane.YES_OPTION) {
                session.setUseBackup(false);
                session.commit();
                panel.setEncoding(encoding);
            }
            else commit = false;
        } catch (IOException e) {
            e.printStackTrace();
        }

        return commit;
    }

    /**
     * Run the "Save" operation. This method offloads the actual save operation to a background thread, but
     * still runs synchronously using Spin (the method returns only after completing the operation).
     */
    public void runCommand() throws Throwable {
        // This part uses Spin's features:
        Worker wrk = getWorker();
        // The Worker returned by getWorker() has been wrapped
        // by Spin.off(), which makes its methods be run in
        // a different thread from the EDT.
        CallBack clb = getCallBack();

        init(); // This method runs in this same thread, the EDT.
        // Useful for initial GUI actions, like printing a message.

        // The CallBack returned by getCallBack() has been wrapped
        // by Spin.over(), which makes its methods be run on
        // the EDT.
        wrk.run(); // Runs the potentially time-consuming action
        // without freezing the GUI. The magic is that THIS line
        // of execution will not continue until run() is finished.
        clb.update(); // Runs the update() method on the EDT.

    }

    public void save() throws Throwable {
        runCommand();
    }

    /**
     * Run the "Save as" operation. This method offloads the actual save operation to a background thread, but
     * still runs synchronously using Spin (the method returns only after completing the operation).
     */
    public void saveAs() throws Throwable {
        String chosenFile = null;
        File f = null;
        while (f == null) {
            chosenFile = FileDialogs.getNewFile(frame, new File(Globals.prefs.get("workingDirectory")), ".bib",
                    JFileChooser.SAVE_DIALOG, false, null);
            if (chosenFile == null) {
                cancelled = true;
                return; // cancelled
            }
            f = new File(chosenFile);
            // Check if the file already exists:
            if (f.exists() && (JOptionPane.showConfirmDialog
                    (frame, "'" + f.getName() + "' " + Globals.lang("exists. Overwrite file?"),
                            Globals.lang("Save database"), JOptionPane.OK_CANCEL_OPTION)
                    != JOptionPane.OK_OPTION)) {
                f = null;
            }
        }

        if (chosenFile != null) {
            File oldFile = panel.metaData().getFile();
            panel.metaData().setFile(f);
            Globals.prefs.put("workingDirectory", f.getParent());
            runCommand();
            // If the operation failed, revert the file field and return:
            if (!success) {
                panel.metaData().setFile(oldFile);
                return;
            }
            // Register so we get notifications about outside changes to the file.
            try {
                panel.setFileMonitorHandle(Globals.fileUpdateMonitor.addUpdateListener(panel, panel.getFile()));
            } catch (IOException ex) {
                ex.printStackTrace();
            }
            frame.getFileHistory().newFile(panel.metaData().getFile().getPath());
        }

    }

    /**
     * Query whether the last operation was successful.
     *
     * @returns true if the last Save/SaveAs operation completed successfully, false otherwise.
     */
    public boolean isSuccess() {
        return success;
    }

    /**
     * Query whether the last operation was cancelled.
     *
     * @returns true if the last Save/SaveAs operation was cancelled from the file dialog or from another 
     * query dialog, false otherwise.
     */
    public boolean isCancelled() {
        return cancelled;
    }
}
