/* Copyright (C) 2005-2011 Fabio Riccardi */

package com.lightcrafts.app;

import static com.lightcrafts.app.Locale.LOCALE;
import com.lightcrafts.app.batch.BatchConfig;
import com.lightcrafts.app.batch.BatchConfigurator;
import com.lightcrafts.app.batch.BatchProcessor;
import com.lightcrafts.app.batch.SendDialog;
import com.lightcrafts.app.menu.ComboFrameMenuBar;
import com.lightcrafts.app.menu.WindowMenu;
import com.lightcrafts.app.other.OtherApplication;
import com.lightcrafts.image.*;
import com.lightcrafts.image.export.ImageExportOptions;
import com.lightcrafts.image.export.ImageFileExportOptions;
import com.lightcrafts.image.metadata.ImageMetadata;
import com.lightcrafts.image.types.*;
import com.lightcrafts.mediax.jai.PlanarImage;
import com.lightcrafts.model.Engine;
import com.lightcrafts.model.OperationType;
import com.lightcrafts.model.PrintSettings;
import com.lightcrafts.model.Scale;
import com.lightcrafts.platform.*;
import com.lightcrafts.prefs.PreferencesDialog;
import com.lightcrafts.prefs.ApplicationMode;
import com.lightcrafts.splash.AboutDialog;
import com.lightcrafts.splash.SplashWindow;
import com.lightcrafts.splash.StartupProgress;
import com.lightcrafts.templates.TemplateDatabase;
import com.lightcrafts.templates.TemplateKey;
import com.lightcrafts.ui.editor.*;
import com.lightcrafts.ui.editor.assoc.DocumentDatabase;
import com.lightcrafts.ui.editor.assoc.DocumentInterpreter;
import com.lightcrafts.ui.export.ExportLogic;
import com.lightcrafts.ui.export.ExportNameUtility;
import com.lightcrafts.ui.export.SaveOptions;
import com.lightcrafts.ui.print.PrintLayoutDialog;
import com.lightcrafts.ui.print.PrintLayoutModel;
import com.lightcrafts.ui.templates.TemplateList;
import com.lightcrafts.ui.help.HelpConstants;
import com.lightcrafts.utils.TerseLoggingHandler;
import com.lightcrafts.utils.UserCanceledException;
import com.lightcrafts.utils.file.FileUtil;
import com.lightcrafts.utils.thread.ProgressThread;
import com.lightcrafts.utils.xml.XMLException;
import com.lightcrafts.utils.xml.XmlDocument;
import com.lightcrafts.utils.xml.XmlNode;
import com.lightcrafts.license.LicenseChecker;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.awt.print.PageFormat;
import java.awt.print.PrinterAbortException;
import java.awt.print.PrinterException;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.*;
import java.util.*;
import java.util.List;
import java.util.logging.Handler;
import java.util.logging.Logger;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;

/** This class collects static methods and structures for top-level document
  * management.  These methods handle startup processing; window management;
  * look-and-feel configuration; document-level actions like open, save,
  * close, import, and export; and shutdown.
  */

public class Application {

    public static final String LznNamespace =
        "http://www.lightcrafts.com/lightzone/LightZoneTransform";

    public static final String LznPrefix = "lzn";

    private static Rectangle InitialFrameBounds; // First window bounds

    private static int InitialFrameState;    // First Frame "extended" state

    private static StartupProgress Startup = new StartupProgress();

    private static int RecentCount = 5;     // Max recent file count

    private static LinkedList<ComboFrame> Current =
        new LinkedList<ComboFrame>();

    private static LinkedList<File> RecentFiles = new LinkedList<File>();

    private static LinkedList<File> RecentFolders = new LinkedList<File>();

    private static File LastOpenPath;       // Most recent "Open" selection

    private static SaveOptions LastSaveOptions;     // Most recent "Save"

    private static ImageExportOptions LastExportOptions; // Most recent "Export"

    private static PrintLayoutModel LastPrintLayout;     // Most recent "Print"

    private static boolean IsQuitting;  // Detect shutdown in progress

    private static Platform Env = Platform.getPlatform();

    public static void setStartupProgress(StartupProgress startup) {
        Startup = startup;
    }

    public static void open(ComboFrame parent) {
        FileChooser chooser = Env.getFileChooser();
        File file = chooser.openFile(
            LOCALE.get("OpenFileDialogTitle"),
            LastOpenPath,
            parent,
            ImageFilenameFilter.INSTANCE
        );
        if (file != null) {
            ComboFrame frame = getFrameForFile(file);
            if (frame != parent) {
                open(file, parent, null);
            }
            else {
                parent.requestFocus();
            }
            LastOpenPath = file.getParentFile();
            savePrefs();
        }
    }

    public static void open(ComboFrame frame, File file) {
        ComboFrame priorFrame = getFrameForFile(file);
        if (priorFrame != null) {
            priorFrame.requestFocus();
            return;
        }
        // The IDE event handler calls this with null, because it
        // doesn't know anything about frames.
        if (frame == null) {
            // See if there's an active frame:
            frame = getActiveFrame();
            if (frame == null) {
                // Otherwise, open into a new window.
                frame = openEmpty();
            }
        }
        open(file, frame, null);
    }

    /**
     * Open an image file at the request of some other application.
     */
    public static void openFrom(File file, OtherApplication otherApp) {
        ComboFrame priorFrame = getFrameForFile(file);
        if (priorFrame != null) {
            priorFrame.requestFocus();
            return;
        }
        // The desktop event handlers don't know anything about frames.
        // See if there's an active frame:
        ComboFrame frame = getActiveFrame();
        if (frame == null) {
            // Otherwise, open into a new window.
            frame = openEmpty();
        }
        open(file, frame, otherApp);
    }

    public static void reOpen(ComboFrame frame) {
        Document doc = frame.getDocument();
        File file = doc.getFile();
        ImageMetadata meta = doc.getMetadata();
        OtherApplication otherApp = (OtherApplication) doc.getSource();
        if (file != null) {
            open(file, frame, otherApp);
        }
        else {
            open(meta.getFile(), frame, otherApp);
        }
    }

    static void open(
        final File file, final ComboFrame frame, final OtherApplication otherApp
    ) {
        Document doc = frame.getDocument();
        if ((doc != null) && doc.isDirty()) {
            int result = askToSaveChanges(frame);
            if (result == SAVE_YES) {
                boolean saved = save(frame, true);
                if (! saved) {
                    return;
                }
            }
            else if (result == SAVE_CANCEL) {
                return;
            }
            else if (result < 0) {
                // The dialog was disposed without any selection:
                return;
            }
        }
        // Kick off Document initialization, and display the Document in the
        // ComboFrame if everything works out.
        DocumentInitializer.createDocument(
            file, frame,
            new DocumentInitializerListener() {
                public void documentStarted() {
                }
                public void documentInitialized(Document doc) {
                    if (doc != null) {
                        frame.pause();
                        try {
                            // Ensure the source is set before frame init:
                            doc.setSource(otherApp);
                            ComboFrame docFrame = show(doc, frame);
                            // Ensure layout is complete before zoom-to-fit:
                            docFrame.validate();
                            setInitialSize(doc);
                        }
                        finally {
                            frame.resume();
                        }
                    }
                }
                public void documentCancelled() {
                    System.err.println("Document cancelled");
                }
                public void documentFailed(Throwable t) {
                    handleDocInitError(t, frame);
                }
            }
        );
    }

    private static void handleDocInitError(Throwable t, ComboFrame frame) {
        try {
            throw t;
        }
        catch (BadImageFileException e) {
            showError(LOCALE.get("InvalidImageFileError"), e, frame);
        }
        catch (Document.MissingImageFileException e) {
            showError(LOCALE.get("UnknownOriginalFileError"), e, frame);
        }
        catch (XMLException e) {
            showError(LOCALE.get("MalformedLznFileError"), e, frame);
        }
        catch (IOException e) {
            showError(LOCALE.get("IOFileError"), e, frame);
        }
        catch (OutOfMemoryError e) {
            showError(LOCALE.get("InsufficientMemoryFileError"), e, frame);
        }
        catch (UnknownImageTypeException e) {
            showError(LOCALE.get("ImageFormatFileError"), e, frame);
        }
        catch (UnsupportedColorProfileException e) {
            showError(LOCALE.get("UnsupportedCameraFileError"), e, frame);
        }
        catch (ColorProfileException e) {
            showError(LOCALE.get("UnsupportedColorProfileFileError"), e, frame);
        }
        catch (Throwable e) {
            showError(LOCALE.get("UnknownFileError"), e, frame);
        }
    }

    // This Document initialization method differs from the headless
    // initialization in createDocumentHeadless() by providing user
    // interaction during the initialization process:
    //
    //  1) A ProgressThread can give progress feedback and supports cancel.
    //
    //  2) A ComboFrame provides a parent for dialogs in case an original
    //     image file can't be found.
    //
    //  3) A preview RenderedImage gets passed to the Engine for fast painting
    //     before tiles are ready.

    static Document createDocument(
        File file,
        ComboFrame frame,
        ProgressThread cancel
    ) throws UserCanceledException,
             XMLException,
             UnknownImageTypeException,
             IOException,
             BadImageFileException,
             ColorProfileException,
             Document.MissingImageFileException
    {
        Document doc;

        // First try as a saved-document:
        DocumentReader.Interpretation interp = DocumentReader.read(file);

        // If it's somehow a saved document:
        if (interp != null) {

            XmlDocument xml = interp.xml;
            File imageFile = interp.imageFile;
            ImageInfo info = interp.info;

            if (imageFile == null) {
                // This is a symptom of a template file.
                if (file.getName().endsWith(".lzt")) {
                    int option = Env.getAlertDialog().showAlert(
                        frame,
                        LOCALE.get("TemplateQuestionMajor"),
                        LOCALE.get("TemplateQuestionMinor"),
                        AlertDialog.WARNING_ALERT,
                        LOCALE.get("TemplateImportOption"),
                        LOCALE.get("TemplateOpenOption"),
                        LOCALE.get("TemplateCancelOption")
                    );
                    if (option == 0) {
                        try {
                            InputStream in = new FileInputStream(file);
                            XmlDocument template = new XmlDocument(in);
                            TemplateKey key = TemplateKey.importKey(file);
                            TemplateDatabase.addTemplateDocument(
                                template, key, true
                            );
                            TemplateList.showDialog(frame);
                        }
                        catch (Throwable t) {
                            showError(
                                LOCALE.get(
                                    "TemplateImportError", file.getName()
                                ),
                                t, frame
                            );
                        }
                        return null;
                    }
                    else if (option == 2) {
                        return null;
                    }
                    // option == 1, let the initialization continue...
                }
                // LightweightDocument couldn't figure out the original image
                // path, so let Document throw its MissingImageFileException:
                doc = new Document(xml, null, info, cancel);
            }
            else {
                // Check for a missing image file:
                boolean hunted = false;
                if (! imageFile.exists()) {
                    // Isolate the image file name, admitting both unix and
                    // windows path syntax:
                    String imageFileName =
                        imageFile.getAbsolutePath().replaceAll(
                            ".*[/\\\\]", ""
                        );
                    // Try the DocumentDatabase:
                    File[] files =
                        DocumentDatabase.findImageFiles(imageFileName);

                    // Maybe check the Document file's directory directly,
                    // since DocumentDatabase is sometimes disabled:
                    if (files.length == 0) {
                        File docDir = file.getParentFile();
                        File altImageFile = new File(docDir, imageFileName);
                        if (altImageFile.isFile()) {
                            files = new File[1];
                            files[0] = altImageFile;
                        }
                    }
                    imageFile = DocumentImageSelector.chooseImageFile(
                        file, imageFile, files, LastOpenPath, frame
                    );
                    if (imageFile == null) {
                        // User cancelled.
                        return null;
                    }
                    hunted = true;
                }
                ImageInfo imageFileInfo = ImageInfo.getInstanceFor(imageFile);
                ImageMetadata meta = imageFileInfo.getMetadata();

                // Read the saved document:
                doc = new Document(xml, meta, info, cancel);
                if (hunted) {
                    doc.markDirty();
                }
            }
            DocumentDatabase.addDocumentFile(file);
        }
        else {
            // Maybe it's an image:
            ImageInfo info = ImageInfo.getInstanceFor(file);
            ImageMetadata meta = info.getMetadata();
            ImageType type = info.getImageType();

            // Maybe set up a template with default tools:
            XmlDocument xml = null;

            // First look for default settings in the user-defined templates:
            TemplateKey template = TemplateDatabase.getDefaultTemplate(meta);
            if (template != null) {
                try {
                    xml = TemplateDatabase.getTemplateDocument(template);
                }
                catch (TemplateDatabase.TemplateException e) {
                    // Let xml remain null, try the factory defaults.
                }
            }
            // Then look for factory default settings:
            if (xml == null) {
                xml = DocumentDatabase.getDefaultDocument(meta);
            }
            // Only apply default settings if the image is in a RAW format:
            boolean isRaw = (type instanceof RawImageType);

            if (isRaw && (xml != null)) {
                doc = new Document(xml, meta, null, cancel);
            }
            else {
                doc = new Document(meta, cancel);
            }
        }
        maybeAddRawAdjustments(doc);

        SaveOptions save = doc.getSaveOptions();

        // Make sure the save options point to the place the file was opened
        // from, in case it was moved since it was written:
        if (save != null) {
            save.setFile(file);
        }
        // Check for the legacy LZN saved document format, and mutate into the
        // current default format if the legacy format was used.
        ImageType type = ImageType.determineTypeByExtensionOf(file);
        boolean recentLzn = ((save != null) && save.isLzn());
        boolean oldLzn = (save == null) && type.equals(LZNImageType.INSTANCE);
        // (Basically, if save == null and it's not an original image.)
        if (recentLzn || oldLzn) {
            // A legacy file: clobber the save options with defaults.
            doc.setSaveOptions(null);
            save = getSaveOptions(doc);
            doc.setSaveOptions(save);
        }
        addToRecentFiles(file);

        return doc;
    }

    // This headless Document initialization differs from the behavior in
    // createDocument() in that it does not perform any user interaction
    // during the initialization process: no progress display, no cancel
    // capability, no dialogs for missing originals, and no Engine
    // preview image.

    public static Document createDocumentHeadless(File file)
        throws UserCanceledException,
               XMLException,
               UnknownImageTypeException,
               IOException,
               BadImageFileException,
               ColorProfileException,
               Document.MissingImageFileException
    {
        Document doc;

        // First try as a saved-document:
        DocumentReader.Interpretation interp = DocumentReader.read(file);

        // If it's somehow a saved document:
        if (interp != null) {
            if (interp.imageFile != null) {
                ImageInfo imageFileInfo =
                    ImageInfo.getInstanceFor(interp.imageFile);
                ImageMetadata meta = imageFileInfo.getMetadata();
                doc = new Document(interp.xml, meta, interp.info, null);
            }
            else {
                // Maybe an LZT.  Trigger the MissingImageFileException.
                doc = new Document(interp.xml);
            }
            DocumentDatabase.addDocumentFile(file);
        }
        else {
            // Maybe it's an image:
            ImageInfo info = ImageInfo.getInstanceFor(file);
            ImageMetadata meta = info.getMetadata();
            ImageType type = info.getImageType();

            // Maybe set up a template with default tools:
            XmlDocument xml = null;

            // First look for default settings in the user-defined templates:
            TemplateKey template = TemplateDatabase.getDefaultTemplate(meta);
            if (template != null) {
                try {
                    xml = TemplateDatabase.getTemplateDocument(template);
                }
                catch (TemplateDatabase.TemplateException e) {
                    // Let xml remain null, try the factory defaults.
                }
            }
            // Then look for factory default settings:
            if (xml == null) {
                xml = DocumentDatabase.getDefaultDocument(meta);
            }
            // Only apply default settings if the image is in a RAW format:
            boolean isRaw = (type instanceof RawImageType);

            if (isRaw && (xml != null)) {
                doc = new Document(xml, meta);
            }
            else {
                doc = new Document(meta);
            }
        }
        maybeAddRawAdjustments(doc);

        SaveOptions save = doc.getSaveOptions();

        // Make sure the save options point to the place the file was opened
        // from, in case it was moved since it was written:
        if (save != null) {
            save.setFile(file);
        }
        // Check for the legacy LZN saved document format, and mutate into the
        // current default format if the legacy format was used.
        ImageType type = ImageType.determineTypeByExtensionOf(file);
        boolean recentLzn = ((save != null) && save.isLzn());
        boolean oldLzn = (save == null) && type.equals(LZNImageType.INSTANCE);
        // (Basically, if save == null and it's not an original image.)
        if (recentLzn || oldLzn) {
            // A legacy file: clobber the save options with defaults.
            doc.setSaveOptions(null);
            save = getSaveOptions(doc);
            doc.setSaveOptions(save);
        }
        return doc;
    }

    // New Documents created from RAW files get a "RAW Adjustments" singleton
    // tool, if they don't have one already.  These tools can not be part of
    // the default RAW templates, because the tool's initial state depends on
    // the image itself.
    private static void maybeAddRawAdjustments(Document doc) {
        ImageMetadata meta = doc.getMetadata();
        ImageType type = meta.getImageType();
        if (type instanceof RawImageType) {
            if (! doc.hasRawAdjustments()) {
                Engine engine = doc.getEngine();
                OperationType rawType = engine.getRawAdjustmentsOperationType();
                Editor editor = doc.getEditor();
                editor.addControl(rawType, 0);
                doc.discardEdits();
                doc.markClean();
            }
        }
    }

    public static ComboFrame openEmpty() {
        ComboFrame frame = createNewComboFrame(null);
        addToCurrent(frame);
        frame.setVisible(true);
        return frame;
    }

    public static void openRecentFolder(ComboFrame frame, File folder) {
        // This can be called from the no-frame menu on the Mac.
        if (frame == null) {
            // See if there's an active frame:
            frame = getActiveFrame();
            if (frame == null) {
                // Otherwise, open into a new window.
                frame = openEmpty();
            }
        }
        frame.showRecentFolder(folder);
        addToRecentFolders(folder);
        savePrefs();
    }

    public static void notifyRecentFolder(File folder) {
        addToRecentFolders(folder);
        savePrefs();
    }

    public static boolean save(ComboFrame frame) {
        return save(frame, false);
    }

    private static boolean save( final ComboFrame frame,
                                 final boolean openPending ) {
        final Document doc = frame.getDocument();
        if ( doc == null ) {
            return false;
        }
        boolean saveDirectly = false;
        SaveOptions options = doc.getSaveOptions();
        if (options == null) {
            if (OtherApplicationShim.shouldSaveDirectly(doc)) {
                options = OtherApplicationShim.createExportOptions(doc);
                if (options == null) {
                    // Something went wrong in all the redundant I/O required
                    // by OtherApplication.
                    showError(LOCALE.get("DirectSaveError"), null, frame);
                    return false;
                }
                doc.setSaveOptions(options);
                saveDirectly = true;
            }
            else {
                options = getSaveOptions(doc);

                final FileChooser chooser =
                    Platform.getPlatform().getFileChooser();
                File file = options.getFile();
                file = chooser.saveFile(file, frame);

                if (file == null) {
                    return false;
                }
                options.setFile(file);

                // We've stopped exposing the multilayer TIFF option to users,
                // but the option can slip through via persisted options.
                if (options.isMultilayerTiff()) {
                    // Convert to a single-layer TIFF in this case.
                    final ImageExportOptions export =
                        SaveOptions.getExportOptions(options);
                    options = SaveOptions.createSidecarTiff(export);
                }
                LastSaveOptions = options;
                doc.setSaveOptions(options);
            }
        } else
            saveDirectly = options.shouldSaveDirectly();

        frame.showWait(LOCALE.get("SaveMessage"));

        final boolean isLzn = options.isLzn();
        final File saveFile = options.getFile();

        Throwable error = null;
//        Throwable error = BlockingExecutor.execute(
//            new BlockingExecutor.BlockingRunnable() {
//                public void run() throws IOException {
                    frame.pause();
                    try {
                        TemporaryEditorCommitState state = doc.saveStart();
                        DocumentWriter.save(doc, frame, saveDirectly, null);
                        doc.saveEnd(state);
                        if (!isLzn && OtherApplication.isIntegrationEnabled()) {
                            final OtherApplication app =
                                (OtherApplication)doc.getSource();
                            if (app != null) {
                                app.postSave(
                                    saveFile, saveDirectly, openPending
                                );
                            }
                        }
                    }
                    catch (Exception e) {
                        error = e;
                    }
                    finally {
                        frame.resume();
                    }
//                }
//            }
//        );
        frame.hideWait();

        if (error != null) {
            final File file = options.getFile();
            showError(
                LOCALE.get("SaveError", file.getPath()),
                error, frame
            );
            return false;
        }
        doc.markClean();

        // Don't let synthetic writeback options be persisted:
        if ( saveDirectly ) {
            doc.setSaveOptions(null);
        }
        final File file = options.getFile();
        DocumentDatabase.addDocumentFile(file);

        addToRecentFiles(file);
        savePrefs();

        return true;
    }

    public static SaveResult saveAs(ComboFrame frame) {
        Document doc = frame.getDocument();
        if (doc == null) {
            return SaveResult.DontSave;
        }
        SaveOptions options = getSaveOptions(doc);

        FileChooser chooser = Platform.getPlatform().getFileChooser();
        File file = options.getFile();
        file = chooser.saveFile(file, frame);

        if (file == null) {
            return SaveResult.Cancelled;
        }
        options.setFile(file);

        // We've stopped exposing the multilayer TIFF option to users,
        // but the option can slip through via persisted options.
        if (options.isMultilayerTiff()) {
            // Convert to a single-layer TIFF in this case.
            ImageExportOptions export =
                SaveOptions.getExportOptions(options);
            options = SaveOptions.createSidecarTiff(export);
        }
        LastSaveOptions = options;
        doc.setSaveOptions(options);
        boolean saved = save(frame);
        if (! saved) {
            return SaveResult.CouldntSave;
        }
        savePrefs();

        return SaveResult.Saved;
    }

    public static void saveAll() {
        for (ComboFrame frame : Current) {
            Document doc = frame.getDocument();
            if ((doc != null) && (doc.isDirty())) {
                save(frame);
            }
        }
    }

    public static boolean close(ComboFrame frame) {
        if (closeDocument(frame)) {
            savePrefs();
            removeFromCurrent(frame);
            frame.dispose();
            if (Platform.getType() != Platform.MacOSX) {
                maybeQuit();
            }
            return true;
        }
        return false;
    }

    // This handles ask-to-save and clearing the document on the given frame,
    // but it should only be called from ComboFrame so the auto-save logic
    // can be applied.
    public static boolean closeDocument(ComboFrame frame) {
        Document doc = frame.getDocument();
        if ((doc != null) && doc.isDirty()) {
            final int result = askToSaveChanges(frame);
            if (result == SAVE_YES) {
                boolean saved = save(frame);
                if (! saved) {
                    return false;
                }
            }
            else if (result == SAVE_CANCEL) {
                return false;
            }
            else if (result < 0) {
                // The dialog was disposed without the user selecting anything:
                return false;
            }
        }
        frame.setDocument(null);
        if (doc != null) {
            doc.dispose();
        }
        return true;
    }

    public static void closeDocumentForce(ComboFrame frame) {
        Document doc = frame.getDocument();
        frame.setDocument(null);
        if (doc != null) {
            doc.dispose();
        }
    }

    public static void quit() {
        // In case the 20 second wait before setting the startup flag has
        // not elapsed, set it here.
        StartupCrash.startupEnded();
        // Persist open documents in preferences.
        ArrayList<ComboFrame> frames = new ArrayList<ComboFrame>(Current);
        IsQuitting = true;
        for (ComboFrame frame : frames) {
            boolean closed = close(frame);
            if (!closed) {
                IsQuitting = false;
                return;
            }
            // The last one closing will trigger exit().
        }
        // Unless there are no active ComboFrames.
        savePrefs();
        System.exit(0);
    }

    public static boolean isQuitInProgress() {
        return IsQuitting;
    }

    public static void print(ComboFrame frame) {
        Document doc = frame.getDocument();
        if (doc != null) {
            print(frame, doc, null);
        }
    }

    /*
        On Mac OS X the CocoaPrinter triggers a return of the
        modal print dialog and would prematurely dispose the document,
        so we make a callback for the dialog dismissal.
        This is a hack, if anything breaks you know where to look...
     */

    private static class PrintDoneCallback {
        private final Document doc;

        PrintDoneCallback(Document doc) {
            this.doc = doc;
        }

        public void done() {
            doc.dispose();
            PrinterLayer printer = Env.getPrinterLayer();
            printer.dispose();
        }
    }

    public static void print(ComboFrame frame, File file) {
        try {
            Document doc = createDocument(file, frame, null);
            if (doc == null) {
                // For instance, couldn't locate the original image
                return;
            }
            print(frame, doc, new PrintDoneCallback(doc));
            // document disposal delegated to the done PrintDoneCallback
            // doc.dispose();
        }
        catch (Throwable t) {
            handleDocInitError(t, frame);
        }
    }

    public static void print(final ComboFrame frame, final Document doc, final PrintDoneCallback callback) {
        // Capture a preview image:
        Engine engine = doc.getEngine();
        RenderedImage image = engine.getRendering(new Dimension(400, 300));

        if (image instanceof PlanarImage) {
            image = ((PlanarImage) image).getAsBufferedImage();
        }
        // Restore the previous layout:
        PrintLayoutModel layout = doc.getPrintLayout();

        if (layout == null) {
            Dimension size = engine.getNaturalSize();
            // First try the most recent settings:
            if (LastPrintLayout != null) {
                layout = LastPrintLayout;
                layout.updateImageSize(size.width, size.height);
            }
            else {
                // Last resort is default settings:
                layout = new PrintLayoutModel(size.width, size.height);
            }
        }
        // Show the layout dialog:
        final PrintLayoutDialog dialog = new PrintLayoutDialog(
            (BufferedImage) image, layout, frame, LOCALE.get("PrintDialogTitle")
        );
        // Hook up behaviors for the dialog buttons:
        dialog.addCancelAction(
            new ActionListener() {
                public void actionPerformed(ActionEvent event) {
                    dialog.dispose();
                    if (callback != null)
                        callback.done();
                }
            }
        );
        dialog.addDoneAction(
            new ActionListener() {
                public void actionPerformed(ActionEvent event) {
                    PrintLayoutModel layout = dialog.getPrintLayout();
                    doc.setPrintLayout(layout);
                    dialog.dispose();
                    if (callback != null)
                        callback.done();
                    LastPrintLayout = layout;
                    savePrefs();
                }
            }
        );
        dialog.addPrintAction(
            new ActionListener() {
                public void actionPerformed(ActionEvent event) {
                    PrintLayoutModel layout = dialog.getPrintLayout();
                    doc.setPrintLayout(layout);
                    printHeadless(frame, doc);
                    LastPrintLayout = layout;
                    savePrefs();
                }
            }
        );
        dialog.addPageSetupAction(
            new ActionListener() {
                public void actionPerformed(ActionEvent event) {
                    PrintLayoutModel layout = dialog.getPrintLayout();
                    doc.setPrintLayout(layout);
                    pageSetup(doc);
                    layout = doc.getPrintLayout();
                    PageFormat format = layout.getPageFormat();
                    dialog.setPageFormat(format);
                }
            }
        );
        PrinterLayer printer = Env.getPrinterLayer();
        printer.initialize();

        dialog.pack();
        dialog.setLocationRelativeTo(frame);
        dialog.setVisible(true);

        // dispose happens in PrintDoneCallback
        // printer.dispose();
    }

    public static boolean export(ComboFrame frame) {
        Document doc = frame.getDocument();
        if (doc == null) {
            return false;
        }
        return export(frame, doc);
    }

    public static boolean export(ComboFrame frame, File file) {
        try {
            Document doc = createDocument(file, frame, null);
            boolean result = export(frame, doc);
            doc.dispose();
            return result;
        }
        catch (Throwable t) {
            handleDocInitError(t, frame);
            return false;
        }
    }

    public static boolean export(ComboFrame frame, Document doc) {
        ImageExportOptions oldOptions = doc.getExportOptions();
        ImageMetadata meta = doc.getMetadata();
        final Engine engine = doc.getEngine();
        Dimension size = engine.getNaturalSize();

        ImageExportOptions newOptions;
        if (oldOptions != null) {
            // This Document has been exported before.
            newOptions = ExportLogic.getDefaultExportOptions(
                oldOptions, size
            );
        }
        else if (LastExportOptions != null) {
            // This Document has never been exported, but export has been used.
            File file = doc.getFile();
            if (file != null) {
                // This Document has been saved:
                newOptions = ExportLogic.getDefaultExportOptions(
                    LastExportOptions, meta, size, file.getName()
                );
            }
            else {
                // This Document not has been saved:
                newOptions = ExportLogic.getDefaultExportOptions(
                    LastExportOptions, meta, size
                );
            }
        }
        else {
            // Export has never been used.
            newOptions = ExportLogic.getDefaultExportOptions(
                meta, size
            );
        }
        // Show the export dialog using these defaults:
        FileChooser chooser = Platform.getPlatform().getFileChooser();
        ImageExportOptions options = chooser.exportFile(newOptions, frame);

        // User cancelled:
        if (options == null) {
            return false;
        }
        // Do the write:
        boolean success;
        frame.pause();
        try {
            success = DocumentWriter.exportWithDialog(
                engine, options, LOCALE.get("ExportMessage"), frame
            );
        }
        catch (Throwable e) {   // IOException, unchecked Exceptions
            showError(
                LOCALE.get("ExportError", options.getExportFile().toString()),
                e, frame
            );
            success = false;
        }
        finally {
            frame.resume();
        }
        if (! success) {
            // User canceled.
            File file = options.getExportFile();
            if (file != null) {
                file.delete();
            }
            return false;
        }
        doc.setExportOptions(options);

        LastExportOptions = options;
        addToRecentFiles(options.getExportFile());
        savePrefs();

        return true;
    }

    public static TemplateKey saveTemplate(ComboFrame frame, String namespace) {
        Document doc = frame.getDocument();
        if (doc == null) {
            return null;
        }
        // Loop until a unique name is selected or the dialog is cancelled:
        boolean done = false;
        TemplateKey key;
        do {
            XmlDocument xml = new XmlDocument("Template");  // See Document()
            XmlNode root = xml.getRoot();
            doc.saveTemplate(root);

            SaveTemplateDialog dialog = new SaveTemplateDialog();
            ImageMetadata meta = doc.getMetadata();
            key = dialog.showDialog(meta, xml, namespace, frame);
            if (key == null) {
                // Dialog was disposed, or the user cancelled.
                return null;
            }
            // First check if a template with this name already exists:
            XmlDocument conflict = null;
            try {
                conflict = TemplateDatabase.getTemplateDocument(key);
            }
            catch (TemplateDatabase.TemplateException e) {
                // Interpret as no preexisting template with this name.
            }
            if (conflict != null) {
                int replace = Env.getAlertDialog().showAlert(
                    frame,
                    LOCALE.get("TemplateClobberQuestionMajor", key.toString()),
                    LOCALE.get("TemplateClobberQuestionMinor"),
                    AlertDialog.WARNING_ALERT,
                    LOCALE.get("TemplateClobberReplaceOption"),
                    LOCALE.get("TemplateClobberCancelOption")
                );
                if (replace != 0) {
                    // Skip the procedure below and redo the dialog:
                    continue;
                }
            }
            // Everything is OK, so make the changes:
            try {
                xml = dialog.getModifiedTemplate();
                TemplateDatabase.addTemplateDocument(xml, key, true);
                if (dialog.isDefaultSelected()) {
                    TemplateDatabase.setDefaultTemplate(meta, key);
                }
            }
            catch (TemplateDatabase.TemplateException e) {
                showError(LOCALE.get("TemplateWriteError"), e, frame);
            }
            done = true;
        } while (! done);

        return key;
    }

    /**
     * Append the named template from TemplateDocuments in the current editor.
     */
    public static void applyTemplate(ComboFrame frame, TemplateKey key) {
        Document doc = frame.getDocument();
        if (doc == null) {
            return;
        }
        try {
            XmlDocument template = TemplateDatabase.getTemplateDocument(key);
            if (template != null) {
                XmlNode root = template.getRoot();
                doc.applyTemplate(root);
            }
            else {
                showError(
                    LOCALE.get("TemplateNameError", key.toString()), null, frame
                );
            }
        }
        catch (TemplateDatabase.TemplateException e) {
            showError(
                LOCALE.get("TemplateReadError", key.toString()), e, frame
            );
        }
        catch (XMLException e) {
            showError(
                "Template \"" + key.toString() + "\" is malformed", e, frame
            );
        }
        catch (Throwable t) {
            showError(
                LOCALE.get("TemplateGeneralError", key.toString()), t, frame
            );
        }
    }

    /**
     * Apply the named template from TemplateDocuments to the File[].
     */
    public static void applyTemplate(
        ComboFrame frame, File[] files, TemplateKey key
    ) {
        XmlDocument template;
        try {
            template = TemplateDatabase.getTemplateDocument(key);
        }
        catch (TemplateDatabase.TemplateException e) {
            showError(
                LOCALE.get("TemplateReadError", key.toString()), e, frame
            );
            return;
        }
        if (template == null) {
            showError(
                LOCALE.get("TemplateNameError", key.toString()), null, frame
            );
            return;
        }
        BatchConfig conf = new BatchConfig();
        conf.name = "";
        conf.export = (ImageFileExportOptions)
            SaveOptions.getExportOptions(SaveOptions.getDefaultSaveOptions());

        try {
            BatchProcessor.process(frame, files, template, conf);
        }
        catch (RuntimeException e) {
            showError(
                "An error occurred during batch processing.", e, frame
            );
        }
    }

    /**
     * Interpret the file as a template and apply to the File[].
     */
    public static void applyTemplate(
        ComboFrame frame, File[] files, File file
    ) {
        DocumentReader.Interpretation interp = DocumentReader.read(file);
        if (interp == null) {
            showError(
                LOCALE.get("TemplateInterpError", file.getName()), null, frame
            );
            return;
        }
        BatchConfig conf = new BatchConfig();
        conf.name = "";
        conf.export = (ImageFileExportOptions)
            SaveOptions.getExportOptions(SaveOptions.getDefaultSaveOptions());

        try {
            XmlDocument template = interp.xml;
            BatchProcessor.process(frame, files, template, conf);
        }
        catch (RuntimeException e) {
            showError(
                "An error occurred during batch processing.", e, frame
            );
        }
    }

    /**
     * Apply a null template to the File[], which is our way of performing
     * batch export.
     */
    public static void export(ComboFrame frame, File[] files) {
        BatchConfig conf = BatchConfigurator.showDialog(
            files, frame, true
        );
        if (conf != null) {
            BatchProcessor.process(frame, files, null, conf);
        }
    }

    /**
     * Perform the "send" operation, a form of batch export with narrowly
     * constrained parameters.
     */
    public static void send(ComboFrame frame, File[] files) {
        File folder = frame.getRecentFolder();
        String from = folder.getName();
        BatchConfig conf = SendDialog.showDialog(frame, from, files.length);
        if (conf != null) {
            BatchProcessor.process(frame, files, null, conf);

            if (conf.directory != null && files.length > 0) {
                Platform.getPlatform().showFileInFolder(conf.directory.getAbsolutePath());
            }
        }
    }

    public static List<File> getRecentFiles() {
        return new ArrayList<File>(RecentFiles);
    }

    public static void clearRecentFiles() {
        RecentFiles.clear();
        savePrefs();
    }

    public static List<File> getRecentFolders() {
        return new ArrayList<File>(RecentFolders);
    }

    public static void clearRecentFolders() {
        RecentFolders.clear();
        savePrefs();
    }

    public static List<ComboFrame> getCurrentFrames() {
        List<ComboFrame> frames = new ArrayList<ComboFrame>(Current);
        return frames;
    }

    public static void setLookAndFeel(LookAndFeel laf) {
        // We presume it's always OK to do nothing if this fails:
        try {
            UIManager.setLookAndFeel(laf);
            for (ComboFrame frame : Current) {
                SwingUtilities.updateComponentTreeUI(frame);
                frame.pack();
            }
        }
        catch (UnsupportedLookAndFeelException e) {
            showError("Error setting look and feel", e, null);
        }
    }

    public static void setLookAndFeel(String className) {
        // We presume it's always OK to do nothing if this fails:
        try {
            UIManager.setLookAndFeel(className);
            for (ComboFrame frame : Current) {
                SwingUtilities.updateComponentTreeUI(frame);
                frame.pack();
            }
        }
        catch (ClassNotFoundException e) {
            showError("Error setting look and feel", e, null);
        }
        catch (InstantiationException e) {
            showError("Error setting look and feel", e, null);
        }
        catch (IllegalAccessException e) {
            showError("Error setting look and feel", e, null);
        }
        catch (UnsupportedLookAndFeelException e) {
            showError("Error setting look and feel", e, null);
        }
    }

    public static Preferences getPreferences() {
        return Preferences.userNodeForPackage(Application.class);
    }

    public static void showError(String message, Throwable e, Frame frame) {
        if (e != null) {
            e.printStackTrace();
        }
        if (System.getProperty("dieOnError") != null) {
            System.exit(-1);
        }
        // The splash can conceal other dialogs:
        SplashWindow.disposeSplash();

        String detail = null;
        if (e != null) {
            detail = e.getMessage();
            if (detail == null) {
                detail = e.getClass().toString();
            }
        }
        Env.getAlertDialog().showAlert(
            frame, message, detail, AlertDialog.ERROR_ALERT,
            LOCALE.get("ErrorDialogOk")
        );
    }

    public static void showAbout() {
        ComboFrame frame = getActiveFrame();
        AboutDialog about = new AboutDialog(frame);
        about.centerOnScreen();
        about.setVisible(true);
        if (LicenseChecker.getLicenseKey() == null) {
            LicenseChecker.license();
        }
    }

    public static void showPreferences() {
        boolean wasBasic = ApplicationMode.isBasicMode();
        PreferencesDialog.showDialog(getActiveFrame());
        boolean isBasic = ApplicationMode.isBasicMode();
        if (isBasic != wasBasic) {
            appModeChanged();
        }
    }

    // Reinitialize windows, for switching between Basic and Full.
    public static void appModeChanged() {
        ComboFrame newFrame = openEmpty();
        List<ComboFrame> frames = new LinkedList<ComboFrame>(Current);
        for (ComboFrame frame : frames) {
            if (newFrame != frame) {
                close(frame);
            }
        }
    }

    static ComboFrame getFrameForFile(File file) {
        for (ComboFrame frame : Current) {
            Document doc = frame.getDocument();
            if (doc != null) {
                File docFile = doc.getFile();
                if (docFile == null) {
                    docFile = doc.getMetadata().getFile();
                }
                if (file.equals(docFile)) {
                    long time = file.lastModified();
                    long docTime = docFile.lastModified();
                    if (time == docTime) {
                        return frame;
                    }
                }
            }
        }
        return null;
    }

    static void copyFiles(ComboFrame frame, List<File> files, File folder) {
        for (File source : files) {
            File target = new File(folder, source.getName());
            try {
                if (target.isFile()) {
                    throw new IOException(
                        LOCALE.get(
                            "MoveExistsMessage",
                            target.getName(),
                            folder.getName()
                        )
                    );
                }
                FileUtil.copyFile(source, target);
            }
            catch (IOException e) {
                showError(
                    LOCALE.get(
                        "MoveCopyFailedMessage",
                        source.getName(),
                        folder.getName()
                    ),
                    e, frame
                );
                return;
            }
        }
    }

    static void moveFiles(ComboFrame frame, List<File> files, File folder) {
        for (File source : files) {
            File target = new File(folder, source.getName());
            try {
                if (target.isFile()) {
                    throw new IOException(
                        LOCALE.get(
                            "MoveExistsMessage",
                            target.getName(),
                            folder.getName()
                        )
                    );
                }
                boolean renamed = source.renameTo(target);
                if (! renamed) {
                    FileUtil.copyFile(source, target);
                }
            }
            catch (IOException e) {
                showError(
                    LOCALE.get(
                        "MoveCopyFailedMessage",
                        source.getName(),
                        folder.getName()
                    ),
                    e, frame
                );
                return;
            }
        }
        for (File source : files) {
            if (source.isFile()) {
                boolean deleted = source.delete();
                if (! deleted) {
                    File oldFolder = source.getParentFile();
                    showError(
                        LOCALE.get(
                            "MoveDeleteFailedMessage",
                            source.getName(),
                            folder.getName(),
                            oldFolder.getName()
                        ),
                        null, frame
                    );
                }
            }
        }
    }

    // Make the ever-present invisible window on the Mac, with a menu.
    private static void openMacPlaceholderFrame() {
        JMenuBar menus = new ComboFrameMenuBar();
        JFrame frame = new JFrame();
        frame.setJMenuBar(menus);
        frame.setBounds(-1000000, -1000000, 0, 0);
        frame.setUndecorated(true);
        frame.setVisible(true);
    }

    private static final int SAVE_YES    = 0;
    private static final int SAVE_CANCEL = 1;

    private static int askToSaveChanges(ComboFrame frame) {
        return Env.getAlertDialog().showAlert(
            frame,
            LOCALE.get("SaveChangesQuestionMajor"),
            LOCALE.get("SaveChangesQuestionMinor"),
            AlertDialog.WARNING_ALERT,
            2,
            LOCALE.get("SaveChangesSaveOption"),
            LOCALE.get("SaveChangesCancelOption"),
            LOCALE.get("SaveChangesDontSaveOption")
        );
    }

    private static boolean askConfirmQuit(ComboFrame frame) {
        String ConfirmQuitKey = "ConfirmQuit";
        Preferences prefs = getPreferences();
        boolean ask = prefs.getBoolean(ConfirmQuitKey, true);
        int option = 0;
        if (ask) {
            String alwaysPrompt = LOCALE.get("ConfirmQuitAlwaysPrompt");
            JCheckBox alwaysCheck = new JCheckBox(alwaysPrompt);

            Box message = Box.createVerticalBox();
            message.add(new JLabel(LOCALE.get("ConfirmQuitQuestion")));
            message.add(alwaysCheck);

            option = JOptionPane.showOptionDialog(
                frame,
                message,
                "Quit LightZone",
                JOptionPane.OK_CANCEL_OPTION,
                JOptionPane.QUESTION_MESSAGE,
                null,
                new Object[] {
                    LOCALE.get("ConfirmQuitConfirmOption"),
                    LOCALE.get("ConfirmQuitCancelOption")
                },
                LOCALE.get("ConfirmQuitConfirmOption")
            );
            if (option == 0) {
                if (alwaysCheck.isSelected()) {
                    prefs.putBoolean(ConfirmQuitKey, false);
                }
            }
        }
        return (option == 0);
    }

    private static ComboFrame createNewComboFrame(ComboFrame parent) {
        ComboFrame frame = new ComboFrame();
        frame.addWindowListener(
            new WindowAdapter() {
                public void windowClosing(WindowEvent event) {
                    ComboFrame frame = (ComboFrame) event.getWindow();
                    // On the Mac, we can close the last frame without quitting.
                    if (Platform.getType() != Platform.MacOSX) {
                        if (Current.size() == 1) {
                            boolean confirmed = askConfirmQuit(frame);
                            if (! confirmed) {
                                return;
                            }
                        }
                    }
                    // Trigger the standard cleanup, which results in quit:
                    close(frame);
                }
            }
        );
        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        frame.pack();
        setNewWindowBounds(frame, parent);
        return frame;
    }

    private static ComboFrame show(Document doc, ComboFrame frame) {
        if (frame == null) {
            frame = createNewComboFrame(null);
        }
        Document old = frame.getDocument();
        frame.setDocument(doc);
        if (old != null) {
            old.dispose();
        }
        addToCurrent(frame);
        savePrefs();

        frame.setVisible(true);

        return frame;
    }

    private static void setInitialSize(Document doc) {
        doc.zoomToFit();
        ScaleModel scales = doc.getScaleModel();
        Scale s = scales.getCurrentScale();
        if (s.getFactor() > 1f) {
            s = new Scale(1, 1);
            scales.setScale(s);
        }
    }

    static SaveOptions getSaveOptions(Document doc) {
        SaveOptions options = doc.getSaveOptions();
        if (options == null) {
            ImageMetadata meta = doc.getMetadata();

            Preferences prefs = getPreferences();
            boolean byOriginal = prefs.getBoolean("SaveByOriginal", true);
            File dir;
            if (byOriginal || LastSaveOptions == null) {
                dir = meta.getFile().getParentFile();
            }
            else {
                dir = LastSaveOptions.getFile().getParentFile();
            }
            options = SaveOptions.getDefaultSaveOptions();

            ImageFileExportOptions export =
                (ImageFileExportOptions) SaveOptions.getExportOptions(options);
            ImageType type = export.getImageType();

            File file = new File(dir, meta.getFile().getName());
            String name = ExportNameUtility.getBaseName(file);
            name = name + "_lzn." + type.getExtensions()[0];
            file = new File(name);
            file = ExportNameUtility.ensureNotExists(file);

            // Code for the "actual size" save preference:
            if (export.resizeWidth.getValue() == 0 &&
                export.resizeHeight.getValue() == 0
            ) {
                Engine engine = doc.getEngine();
                Dimension size = engine.getNaturalSize();
                options.updateSize(size);
            }
            options.setFile(file);
        }
        return options;
    }

    private static void pageSetup(Document doc) {
        PrintLayoutModel layout = doc.getPrintLayout();
        PageFormat format = layout.getPageFormat();
        format = Platform.getPlatform().getPrinterLayer().pageDialog(format);
        if (format != null)
            layout.setPageFormat(format);
    }

    private static void printHeadless(ComboFrame frame, Document doc) {

        // The Printable:
        final Engine engine = doc.getEngine();

        // The layout info:
        PrintLayoutModel layout = doc.getPrintLayout();

        // The Engine's layout sub-info:
        final PrintSettings settings = layout.getPrintSettings();

        PrinterLayer printer = Platform.getPlatform().getPrinterLayer();

        // The PageFormat:
        printer.setPageFormat(layout.getPageFormat());
        final PageFormat format = printer.getPageFormat();

        // Make up a name for the print job:
        String jobName;
        File file = doc.getFile();
        if (file != null) {
            jobName = file.getName();
        }
        else {
            ImageMetadata meta = doc.getMetadata();
            jobName = meta.getFile().getName();
        }
        printer.setJobName(jobName);

        boolean doPrint = printer.printDialog();

        if (doPrint) {
            ProgressDialog dialog = Platform.getPlatform().getProgressDialog();
            ProgressThread thread = new ProgressThread(dialog) {
                public void run() {
                    try {
                        engine.print(this, format, settings);
                    }
                    catch (PrinterException e) {
                        throw new RuntimeException(e);
                    }
                }
                public void cancel() {
                    engine.cancelPrint();
                }
            };
            dialog.showProgress(
                frame, thread, LOCALE.get("PrintingMessage"), 0, 0, true
            );
            Throwable error = dialog.getThrown();
            if (error != null) {
                if (error instanceof RuntimeException) {
                    throw (RuntimeException) error;
                }
                if (error instanceof PrinterAbortException) {
                    // Means print was deliberately cancelled; do nothing.
                    return;
                }
                showError(
                    LOCALE.get("PrintError"), error, frame
                );
            }
        }
    }

    private static void maybeQuit() {
        if (Current.isEmpty()) {
            System.exit(0);
        }
    }

    private static void addToRecentFiles(File file) {
        RecentFiles.remove(file);
        RecentFiles.addFirst(file);
        while (RecentFiles.size() > RecentCount) {
            RecentFiles.removeLast();
        }
    }

    private static void addToRecentFolders(File file) {
        RecentFolders.remove(file);
        RecentFolders.addFirst(file);
        while (RecentFolders.size() > RecentCount) {
            File old = RecentFolders.removeLast();
            ComboFrame.clearFolder(old);
        }
    }

    private static void addToCurrent(ComboFrame frame) {
        Current.remove(frame);
        Current.addFirst(frame);
        WindowMenu.updateAll();
    }

    private static void removeFromCurrent(ComboFrame frame) {
        Current.remove(frame);
        WindowMenu.updateAll();
    }

    private static ComboFrame getActiveFrame() {
        for (ComboFrame frame : Current) {
            if (frame.isActive()) {
                return frame;
            }
        }
        // There is often no active window, like during event queue
        // tasks that have already disposed a modal dialog.  In these
        // cases, we use the most recently active ComboFrame.
        return ComboFrame.LastActiveComboFrame;
    }

    private static void verifyLibraries() {
        String[] libs = new String[] {
            "DCRaw", "Segment", "JAI", "FASTJAI", "fbf", "LCJPEG", "LCTIFF"
        };
        for (String lib : libs) {
            try {
                System.loadLibrary(lib);
            }
            catch (UnsatisfiedLinkError e) {
                showError(
                    "Couldn't link with native library: " + "\"" + lib, e, null
                );
            }
        }
        try {
            Env.loadLibraries();
        }
        catch (UnsatisfiedLinkError e) {
            showError(
                "Couldn't link with platform-specific native libraries", e, null
            );
        }
        try {
            // Run our expensive static initializers in JAIContext:
            Startup.startupMessage(LOCALE.get("StartupEngineMessage"));
            Class.forName("com.lightcrafts.jai.JAIContext");

            // preload jai_core.jar, jai_codec.jar, jai_imageio.jar:
            Startup.startupMessage(LOCALE.get("StartupClassesMessage"));
            Class.forName("com.lightcrafts.mediax.jai.JAI");
            Class.forName("com.lightcrafts.media.jai.codec.ImageCodec");
        }
        catch (ClassNotFoundException e) {
            showError(
                "Couldn't link with image processing class libraries", e, null
            );
        }
    }

    private static void scanProfiles() {
        // These Platform methods cache their results, which can be expensive
        // to determine the first time through.
        Env.getExportProfiles();
        Env.getPrinterProfiles();
    }

    private static void initLogging() {
        // Abbreviate metadata error messages, which can be scroll blinding.
        Logger logger = Logger.getLogger("com.lightcrafts.image.metadata");
        Handler handler = new TerseLoggingHandler(System.out);
        logger.addHandler(handler);
        logger.setUseParentHandlers(false);
    }

    private static void initDocumentDatabase() {
        // This associates images with LZNs, but can be very expensive:
//        DocumentDatabase.init(Startup);

        // Use the DocumentReader to allow the DocumentDatabase to recognize
        // sidecar JPEG, sidecar TIFF, and multilayer TIFF save formats:
        DocumentDatabase.addDocumentInterpreter(
            new DocumentInterpreter() {
                public File getImageFile(File file) {
                    DocumentReader.Interpretation interp =
                        DocumentReader.read(file);
                    return (interp != null) ? interp.imageFile : null;
                }
                public Collection<String> getSuffixes() {
                    Collection<String> tiffs = Arrays.asList(
                        TIFFImageType.INSTANCE.getExtensions()
                    );
                    Collection<String> jpegs = Arrays.asList(
                        JPEGImageType.INSTANCE.getExtensions()
                    );
                    Collection<String> all = new LinkedList<String>();
                    all.addAll(tiffs);
                    all.addAll(jpegs);
                    return all;
                }
            }
        );
    }

    private static void setNewWindowBounds(Frame frame, Frame parent) {
        final int inset = 20;
        // If no parent was supplied, use the "active frame" instead
        if (parent == null) {
            parent = getActiveFrame();
        }
        if ((parent != null) && (parent != frame)) { // First frame is "active"
            // First choice: down and right of the active frame
            Rectangle bounds = parent.getBounds();
            bounds = new Rectangle(
                bounds.x + inset, bounds.y + inset, bounds.width, bounds.height
            );
            // But don't exceed the screen bounds
            GraphicsConfiguration gc = parent.getGraphicsConfiguration();
            Rectangle screen = gc.getBounds();
            if (bounds.getMaxX() > (screen.getMaxX() - inset)) {
                bounds.width = (int) screen.getMaxX() - bounds.x - inset;
            }
            if (bounds.getMaxY() > (screen.getMaxY() - inset)) {
                bounds.height = (int) screen.getMaxY() - bounds.y - inset;
            }
            frame.setBounds(bounds);
        }
        else if ((InitialFrameBounds != null) &&
            (Displays.getVirtualBounds().intersects(InitialFrameBounds))
        ) {
            // Second choice: same as the last bounds saved in preferences
            frame.setBounds(
                InitialFrameBounds.x,
                InitialFrameBounds.y,
                InitialFrameBounds.width,
                InitialFrameBounds.height
            );
            frame.setExtendedState(InitialFrameState);
            // InitialFrameBounds is initialized from preferences, used once,
            // and then discarded
            InitialFrameBounds = null;
        }
        else {
            // Third choice: inset from the screen bounds
            GraphicsConfiguration gc = frame.getGraphicsConfiguration();
            Rectangle bounds = gc.getBounds();
            int x = inset;
            int y = inset;
            int width = bounds.width - 2 * inset;
            int height = bounds.height - 2 * inset;
            frame.setBounds(x, y, width, height);
            // InitialFrameBounds is initialized from preferences, used once,
            // and then discarded
            InitialFrameBounds = null;
        }
        frame.validate();
    }

    private static void setLookAndFeel() {
        LookAndFeel plafName = Env.getLookAndFeel();
        setLookAndFeel(plafName);
    }

    private final static String FirstLaunchTag = "FirstLaunch";

    private static void showFirstTimeHelp() {
        Preferences prefs = getPreferences();
        if (prefs.getBoolean(FirstLaunchTag, true)) {
            Env.showHelpTopic(HelpConstants.HELP_DISCOVER);
            prefs.putBoolean(FirstLaunchTag, false);
        }
        if (VideoLearningCenterDialog.shouldShowDialog()) {
            VideoLearningCenterDialog.showDialog();
        }
    }

    private final static String RecentFileTag = "RecentFile";
    private final static String RecentFolderTag = "RecentFolder";
    private final static String CurrentTag = "Current";
    private final static String OpenTag = "Open";
    private final static String SaveTag = "Save";
    private final static String PrintTag = "Print";
    private final static String ExportTag = "Export";
    private final static String FrameBoundsTag = "Bounds";
    private final static String FrameStateTag = "State";

    private static void savePrefs() {
        Preferences prefs = getPreferences();

        ComboFrame active = getActiveFrame();
        if (active != null) {
            Rectangle bounds = active.getUnmaximizedBounds();
            if (bounds != null) {
                prefs.putInt(FrameBoundsTag + "X", bounds.x);
                prefs.putInt(FrameBoundsTag + "Y", bounds.y);
                prefs.putInt(FrameBoundsTag + "W", bounds.width);
                prefs.putInt(FrameBoundsTag + "H", bounds.height);
            }
            int state = active.getExtendedState();
            prefs.putInt(FrameStateTag, state);
        }
        int n;
        n = 0;
        String key;
        for (File file : RecentFiles) {
            key = RecentFileTag + n;
            String value = file.getAbsolutePath();
            prefs.put(key, value);
            n++;
        }
        // Clear out old recent-docs entries:
        key = RecentFileTag + n++;
        while (prefs.get(key, null) != null) {
            prefs.remove(key);
            key = RecentFileTag + n++;
        }
        n = 0;
        for (File file : RecentFolders) {
            key = RecentFolderTag + n;
            String value = file.getAbsolutePath();
            prefs.put(key, value);
            n++;
        }
        // Clear out old recent-directories entries:
        key = RecentFolderTag + n++;
        while (prefs.get(key, null) != null) {
            prefs.remove(key);
            key = RecentFolderTag + n++;
        }
        // needed: application events
        // (document open, document close, document change)
        // The "windows" menu is relying on static references for updates,
        // and while the set of current documents should be persisted in prefs,
        // a prefs change does not equal a document change, e.g. a new document
        // that has never been saved (image import).
        n = 0;
        for (ComboFrame frame : Current) {
            Document doc = frame.getDocument();
            if (doc == null) {
                continue;
            }
            File file = doc.getFile();
            key = CurrentTag + n;
            if (file != null) {
                String value = file.getAbsolutePath();
                prefs.put(key, value);
                n++;
            }
        }
        // Clear out old current-docs entries:
        key = CurrentTag + n++;
        while (prefs.get(key, null) != null) {
            prefs.remove(key);
            key = CurrentTag + n++;
        }
        if (LastOpenPath != null) {
            String path = LastOpenPath.getAbsolutePath();
            prefs.put(OpenTag, path);
        }
        if (LastSaveOptions != null) {
            // We're remembering the whole SaveOptions object, but
            // SaveOptions.getDefaultSaveOptions() is the authoritative
            // reposity for sticky options.  The LastSaveOptions is only
            // used to remember the recent folder.
            XmlDocument doc = new XmlDocument(SaveTag);
            LastSaveOptions.save(doc.getRoot());
            saveXmlPrefs(SaveTag, doc);
        }
        if (LastPrintLayout != null) {
            XmlDocument doc = new XmlDocument(PrintTag);
            LastPrintLayout.save(doc.getRoot());
            saveXmlPrefs(PrintTag, doc);
        }
        if (LastExportOptions != null) {
            XmlDocument doc = new XmlDocument(ExportTag);
            LastExportOptions.write(doc.getRoot());
            saveXmlPrefs(ExportTag, doc);
        }
        try {
            prefs.sync();
        }
        catch (BackingStoreException e) {
            showError(LOCALE.get("PrefsWriteError"), e, null);
        }
    }

    private static boolean saveXmlPrefs(String tag, XmlDocument doc) {
        Preferences prefs = getPreferences();
        try {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            doc.write(out);
            String text = out.toString("UTF-8");
            prefs.put(tag, text);
            return true;
        }
        catch (IOException e) {
            System.err.print("Error saving preferences: ");
            System.err.print(e.getClass().getName() + " ");
            System.err.println(e.getMessage());
            return false;
        }
    }

    private static void restorePrefs() {
        Preferences prefs = getPreferences();
        try {
            // Restore default frame bounds:
            int x = prefs.getInt(FrameBoundsTag + "X", - Integer.MAX_VALUE);
            int y = prefs.getInt(FrameBoundsTag + "Y", - Integer.MAX_VALUE);
            int w = prefs.getInt(FrameBoundsTag + "W", -1);
            int h = prefs.getInt(FrameBoundsTag + "H", -1);
            if ((x > - Integer.MAX_VALUE) &&
                (y > - Integer.MAX_VALUE) &&
                (w > 0) &&
                (h > 0))
            {
                InitialFrameBounds = new Rectangle(x, y, w, h);
            }
            int state = prefs.getInt(FrameStateTag, Frame.NORMAL);
            if (state != Frame.ICONIFIED) {
                InitialFrameState = state;
            }
            else {
                InitialFrameState = Frame.NORMAL;
            }
            // Restore the RecentFile list:
            String[] keys = prefs.keys();
            Map<Integer, File> recentMap = new HashMap<Integer, File>();
            for (String key : keys) {
                if (key.startsWith(RecentFileTag)) {
                    String indexString = key.substring(RecentFileTag.length());
                    try {
                        Integer index = Integer.decode(indexString);
                        String value = prefs.get(key, null);
                        File file = new File(value);
                        recentMap.put(index, file);
                    }
                    catch (NumberFormatException e) {
                        // Bad recent-file pref, just drop it.
                    }
                }
            }
            for (int n=recentMap.size()-1; n>=0; n--) {
                File file = recentMap.get(n);
                if (file != null) {
                    addToRecentFiles(file);
                }
            }
            // Restore the RecentFolder list:
            keys = prefs.keys();
            recentMap = new HashMap<Integer, File>();
            for (String key : keys) {
                if (key.startsWith(RecentFolderTag)) {
                    String indexString =
                        key.substring(RecentFolderTag.length());
                    try {
                        Integer index = Integer.decode(indexString);
                        String value = prefs.get(key, null);
                        File file = new File(value);
                        recentMap.put(index, file);
                    }
                    catch (NumberFormatException e) {
                        // Bad recent-file pref, just drop it.
                    }
                }
            }
            for (int n=recentMap.size()-1; n>=0; n--) {
                File file = recentMap.get(n);
                if (file != null) {
                    addToRecentFolders(file);
                }
            }
            // Restore the last open path:
            String path;
            path = prefs.get(OpenTag, null);
            if (path != null) {
                LastOpenPath = new File(path);
            }
            // Restore default save options:
            XmlDocument doc;
            doc = restoreXmlPrefs(SaveTag);
            if (doc != null) {
                try {
                    LastSaveOptions = SaveOptions.restore(doc.getRoot());
                }
                catch (XMLException e) {
                    System.err.println(
                        "Malformed save preferences: " + e.getMessage()
                    );
                    LastSaveOptions = null;
                }
            }
            // Restore default print settings:
            doc = restoreXmlPrefs(PrintTag);
            if (doc != null) {
                try {
                    LastPrintLayout = new PrintLayoutModel(0, 0);
                    LastPrintLayout.restore(doc.getRoot());
                }
                catch (XMLException e) {
                    System.err.println(
                        "Malformed print preferences: " + e.getMessage()
                    );
                    LastPrintLayout = null;
                }
            }
            // Restore default export options:
            doc = restoreXmlPrefs(ExportTag);
            if (doc != null) {
                try {
                    LastExportOptions = ImageExportOptions.read(doc.getRoot());
                }
                catch (XMLException e) {
                    System.err.println(
                        "Malformed export preferences: " + e.getMessage()
                    );
                    LastExportOptions = null;
                }
            }
        }
        catch (BackingStoreException e) {
            showError(LOCALE.get("PrefsReadError"), e, null);
        }
    }

    private static XmlDocument restoreXmlPrefs(String tag) {
        Preferences prefs = getPreferences();
        String text = prefs.get(tag, null);
        if (text != null) {
            try {
                InputStream in = new ByteArrayInputStream(
                    text.getBytes("UTF-8")
                );
                XmlDocument doc = new XmlDocument(in);
                return doc;
            }
            catch (Exception e) {   // IOException, XMLException
                System.err.print("Error reading preferences: ");
                System.err.print(e.getClass().getName() + " ");
                System.err.println(e.getMessage());
                prefs.remove(tag);
            }
        }
        return null;
    }

    // Set up verbose focus event logging, for development purposes.
    private static void initFocusDebug() {
        KeyboardFocusManager focus =
            KeyboardFocusManager.getCurrentKeyboardFocusManager();
        focus.addPropertyChangeListener(
            new PropertyChangeListener() {
                public void propertyChange(PropertyChangeEvent evt) {
                    String propName = evt.getPropertyName();
                    Object oldValue = evt.getOldValue();
                    String oldName = (oldValue != null) ?
                        oldValue.getClass().getName() : "null";
                    Object newValue = evt.getNewValue();
                    String newName = (newValue != null) ?
                        newValue.getClass().getName() : "null";
                    System.out.println(
                        propName + ": " + oldName + " -> " + newName
                    );
                }
            }
        );
    }

    public static void main(final String[] args) {
        // Catch startup crashes that prevent launching:
        StartupCrash.checkLastStartupSuccessful();
        StartupCrash.startupStarted();

        ExceptionDialog.installHandler();

        // If a license was entered during the launcher phase, then be sure
        // the Basic mode preference is reset so the licensed functionality
        // will be apparent.
        if (LicenseChecker.enteredLicenseKey()) {
            ApplicationMode.resetPreference();
        }
        // This debug features streams all focus events to standard output.
        if (System.getProperty("lightcrafts.debug.focus") != null) {
            initFocusDebug();
        }
        // Initialize on the main thread, to allow the splash to display,
        // and to sleep for open events from the native event handlers:
        try {
            Startup.startupMessage(LOCALE.get("StartupLibsMessage"));
            verifyLibraries();
            Startup.startupMessage(LOCALE.get("StartupColorsMessage"));
            scanProfiles();
            Startup.startupMessage(LOCALE.get("StartupPrefsMessage"));
            restorePrefs();
            Startup.startupMessage(LOCALE.get("StartupLogsMessage"));
            initLogging();
            Startup.startupMessage(LOCALE.get("StartupScanMessage"));
            initDocumentDatabase();
            Startup.startupMessage(LOCALE.get("StartupOpeningMessage"));

            EventQueue.invokeLater(
                new Runnable() {
                    public void run() {
                        setLookAndFeel();
                        if (Platform.getType() == Platform.MacOSX) {
                            openMacPlaceholderFrame();
                        }
                        openEmpty();
                        Platform.getPlatform().readyToOpenFiles();

                        // Make sure this happens good and late, after a
                        // frame is visible.
                        EventQueue.invokeLater(
                            new Runnable() {
                                public void run() {
                                    showFirstTimeHelp();
                                }
                            }
                        );
                        // Wait twenty seconds after all initialization has
                        // completed before declaring a successful startup,
                        // since crashes that would be fixed by cleared
                        // settings sometimes happen much later, during
                        // queued browser thumbnail tasks.
                        new Thread(
                            new Runnable() {
                                public void run() {
                                    try {
                                        Thread.sleep(20000);
                                    }
                                    catch (InterruptedException e) {
                                        e.printStackTrace();
                                    }
                                    StartupCrash.startupEnded();
                                }
                            },
                            "StartupSuccessWait"
                        ).start();
                    }
                }
            );
            AwtWatchdog.spawn();
        }
        catch (Throwable e) {
            (new ExceptionDialog()).handle(e);
        }
    }
}
