/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander 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 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander 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, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander.bookmark;

import com.mucommander.bookmark.file.BookmarkProtocolProvider;
import com.mucommander.commons.collections.AlteredVector;
import com.mucommander.commons.collections.VectorChangeListener;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.FileFactory;
import com.mucommander.commons.file.FileURL;
import com.mucommander.conf.PlatformManager;
import com.mucommander.io.backup.BackupInputStream;
import com.mucommander.io.backup.BackupOutputStream;

import java.io.*;
import java.util.WeakHashMap;

/**
 * This class manages the bookmark list and its parsing and storage as an XML file.
 * <p>
 * It monitors any changes made to the bookmarks and when changes are made, fires change events to registered
 * listeners.
 * </p>
 * @author Maxence Bernard, Nicolas Rinaudo
 */
public class BookmarkManager implements VectorChangeListener {
    /** Whether we're currently loading the bookmarks or not. */
    private static boolean isLoading = false;

    /** Bookmarks file location */
    private static AbstractFile bookmarksFile;

    /** Default bookmarks file name */
    private static final String DEFAULT_BOOKMARKS_FILE_NAME = "bookmarks.xml";

    /** Bookmark instances */
    private static AlteredVector<Bookmark> bookmarks = new AlteredVector<Bookmark>();

    /** Contains all registered bookmark listeners, stored as weak references */
    private static WeakHashMap<BookmarkListener, ?> listeners = new WeakHashMap<BookmarkListener, Object>();

    /** Specifies whether bookmark events should be fired when a change to the bookmarks is detected */
    private static boolean fireEvents = true;

    /** True when changes were made after the bookmarks file was last saved */
    private static boolean saveNeeded;

    /** Last bookmark change timestamp */
    private static long lastBookmarkChangeTime;

    /** Last event pause timestamp */
    private static long lastEventPauseTime;

    /** Create a singleton instance, needs to be referenced so that it's not garbage collected (AlteredVector
     * stores VectorChangeListener as weak references) */
    private static BookmarkManager singleton = new BookmarkManager();



    // - Initialisation --------------------------------------------------------
    // -------------------------------------------------------------------------
    static {
        // Listen to changes made to the bookmarks vector
        bookmarks.addVectorChangeListener(singleton);
    }

    /**
     * Prevents instanciation of <code>BookmarkManager</code>.
     */
    private BookmarkManager() {}



    // - Bookmark building -----------------------------------------------------
    // -------------------------------------------------------------------------
    /**
     * Passes messages about all known bookmarks to the specified builder.
     * @param  builder           where to send bookmark building messages.
     * @throws BookmarkException if an error occurs.
     */
    public static synchronized void buildBookmarks(BookmarkBuilder builder) throws BookmarkException {
        builder.startBookmarks();
        for(Bookmark bookmark : bookmarks) {
            builder.addBookmark(bookmark.getName(), bookmark.getLocation());
        }
        builder.endBookmarks();
    }



    // - Bookmark file access --------------------------------------------------
    // -------------------------------------------------------------------------
    /**
     * Returns the path to the bookmark file.
     * <p>
     * If it hasn't been changed through a call to {@link #setBookmarksFile(String)},
     * this method will return the default, system dependant bookmarks file.
     * </p>
     * @return             the path to the bookmark file.
     * @see    #setBookmarksFile(String)
     * @throws IOException if there was a problem locating the default bookmarks file.
     */
    public static synchronized AbstractFile getBookmarksFile() throws IOException {
        if(bookmarksFile == null)
            return PlatformManager.getPreferencesFolder().getChild(DEFAULT_BOOKMARKS_FILE_NAME);
        return bookmarksFile;
    }

    /**
     * Sets the path to the bookmarks file.
     * <p>
     * This is a convenience method and is strictly equivalent to calling <code>setBookmarksFile(FileFactory.getFile(file))</code>.
     * </p>
     * @param     path                  path to the bookmarks file
     * @exception FileNotFoundException if <code>path</code> is not accessible.
     * @see       #getBookmarksFile()
     */
    public static void setBookmarksFile(String path) throws FileNotFoundException {
        AbstractFile file;

        if((file = FileFactory.getFile(path)) == null)
            setBookmarksFile(new File(path));
        else
            setBookmarksFile(file);
    }

    /**
     * Sets the path to the bookmarks file.
     * <p>
     * This is a convenience method and is strictly equivalent to calling <code>setBookmarksFile(FileFactory.getFile(file.getAbsolutePath()))</code>.
     * </p>
     * @param     file                  path to the bookmarks file
     * @exception FileNotFoundException if <code>path</code> is not accessible.
     * @see       #getBookmarksFile()
     */
    public static void setBookmarksFile(File file) throws FileNotFoundException {setBookmarksFile(FileFactory.getFile(file.getAbsolutePath()));}

    /**
     * Sets the path to the bookmarks file.
     * @param     file                  path to the bookmarks file
     * @exception FileNotFoundException if <code>path</code> is not accessible.
     * @see       #getBookmarksFile()
     */

    public static synchronized void setBookmarksFile(AbstractFile file) throws FileNotFoundException {
        if(file.isBrowsable())
            throw new FileNotFoundException("Not a valid file: " + file.getAbsolutePath());
        bookmarksFile = file;
    }



    // - Bookmarks loading -----------------------------------------------------
    // -------------------------------------------------------------------------
    /**
     * Loads all available bookmarks.
     * @throws Exception if an error occurs.
     */
    public static synchronized void loadBookmarks() throws Exception {
        InputStream in;

        // Parse the bookmarks file
        in = null;
        isLoading = true;
        try {readBookmarks(in = new BackupInputStream(getBookmarksFile()), new Loader());}
        finally {
            if(in != null) {
                try {in.close();}
                catch(Exception e) {}
            }
            isLoading = false;
        }
    }

    /**
     * Reads bookmarks from the specified <code>InputStream</code>.
     * @param  in        where to read bookmarks from.
     * @throws Exception if an error occurs.
     */
    public static void readBookmarks(InputStream in) throws Exception {readBookmarks(in, new Loader());}

    /**
     * Reads bookmarks from the specified <code>InputStream</code> and passes messages to the specified {@link BookmarkBuilder}.
     * @param  in        where to read bookmarks from.
     * @param  builder   where to send builing messages to.
     * @throws Exception if an error occurs.
     */
    public static synchronized void readBookmarks(InputStream in, BookmarkBuilder builder) throws Exception {new BookmarkParser().parse(in, builder);}



    // - Bookmarks writing -----------------------------------------------------
    // -------------------------------------------------------------------------
    /**
     * Returns a {@link BookmarkBuilder} that will write all building messages as XML to the specified output stream.
     * @param out where to write the bookmarks' XML content.
     * @return             a {@link BookmarkBuilder} that will write all building messages as XML to the specified output stream.
     * @throws IOException if an IO related error occurs.
     */
    public static BookmarkBuilder getBookmarkWriter(OutputStream out) throws IOException {return new BookmarkWriter(out);}

    /**
     * Writes all known bookmarks to the bookmark {@link #getBookmarksFile() file}.
     * @param forceWrite if false, the bookmarks file will be written only if changes were made to bookmarks since
     * last write, if true the file will always be written
     * @throws IOException if an I/O error occurs.
     * @throws BookmarkException if an error occurs.
     */
    public static synchronized void writeBookmarks(boolean forceWrite) throws IOException, BookmarkException {
        OutputStream out;

        // Write bookmarks file only if changes were made to the bookmarks since last write, or if write is forced.
        if(!(forceWrite || saveNeeded))
            return;
        out = null;
        try {
            buildBookmarks(getBookmarkWriter(out = new BackupOutputStream(getBookmarksFile())));
            saveNeeded = false;
        }
        finally {
            if(out != null) {
                try {out.close();}
                catch(Exception e) {}
            }
        }
    }



    // - Bookmarks access ------------------------------------------------------
    // -------------------------------------------------------------------------
    /**
     * Returns an {@link AlteredVector} that contains all bookmarks.
     * 
     * <p>Important: the returned Vector should not directly be used to
     * add or remove bookmarks, doing so won't trigger any event to registered bookmark listeners.
     * However, it is safe to modify bookmarks individually, events will be properly fired.
     * @return an {@link AlteredVector} that contains all bookmarks.
     */
    public static synchronized AlteredVector<Bookmark> getBookmarks() {
        return bookmarks;
    }

    /**
     * Deletes the specified bookmark.
     * @param bookmark bookmark to delete from the list.
     */
    public static synchronized void removeBookmark(Bookmark bookmark) {bookmarks.remove(bookmark);}

    /**
     * Convenience method that looks for a Bookmark with the given name (case ignored) and returns it,
     * or null if none was found. If several bookmarks have the given name, the first one is returned.
     *
     * @param name the bookmark's name
     * @return a Bookmark instance with the given name, null if none was found
     */
    public static synchronized Bookmark getBookmark(String name) {
        int nbBookmarks = bookmarks.size();
        Bookmark b;
        for(int i=0; i<nbBookmarks; i++) {
            b = bookmarks.elementAt(i);
            if(b.getName().equalsIgnoreCase(name))
                return b;
        }

        return null;
    }

    /**
     * Convenience method that adds a bookmark to the bookmark list.
     *
     * @param b the Bookmark instance to add to the bookmark list.
     */
    public static synchronized void addBookmark(Bookmark b) {bookmarks.add(b);}


    /**
     * Check if a given URL represents a bookmark. 
     *
     * @param fileURL the URL to examine
     * @return true if the given URL represents a bookmark, false otherwise
     */
    public static boolean isBookmark(FileURL fileURL) {
    	return BookmarkProtocolProvider.BOOKMARK.equals(fileURL.getScheme());
    }

    // - Listeners -------------------------------------------------------------
    // -------------------------------------------------------------------------
    /**
     * Adds the specified BookmarkListener to the list of registered listeners.
     *
     * <p>Listeners are stored as weak references so {@link #removeBookmarkListener(BookmarkListener)}
     * doesn't need to be called for listeners to be garbage collected when they're not used anymore.
     *
     * @param listener the BookmarkListener to add to the list of registered listeners.
     * @see   #removeBookmarkListener(BookmarkListener)
     */
    public static void addBookmarkListener(BookmarkListener listener) {synchronized(listeners) {listeners.put(listener, null);}}

    /**
     * Removes the specified BookmarkListener from the list of registered listeners.
     *
     * @param listener the BookmarkListener to remove from the list of registered listeners.
     * @see   #addBookmarkListener(BookmarkListener)
     */
    public static void removeBookmarkListener(BookmarkListener listener) {synchronized(listeners) {listeners.remove(listener);}}

    /**
     * Notifies all the registered bookmark listeners of a bookmark change. This can be :
     * <ul>
     * <li>A new bookmark which has just been added
     * <li>An existing bookmark which has been modified
     * <li>An existing bookmark which has been removed
     * </ul>
     */
    public static void fireBookmarksChanged() {
        // Bookmarks file will need to be saved
        if(!isLoading)
            saveNeeded = true;

        lastBookmarkChangeTime = System.currentTimeMillis();

        // Do not fire event if events are currently disabled
        if(!fireEvents)
            return;

        synchronized(listeners) {
            // Iterate on all listeners
            for(BookmarkListener listener : listeners.keySet())
                listener.bookmarksChanged();
        }
    }

    /**
     * Specifies whether bookmark events should be fired when a change in the bookmarks is detected. This allows
     * to temporarily suspend events firing when a lot of them are made, for example when editing the bookmarks list.
     *
     * <p>If true is speicified, any subsequent calls to fireBookmarksChanged will be ignored, until this method is
     * called again with false.</p>
     * @param b whether to fire events.
     */
    public static synchronized void setFireEvents(boolean b) {
        if(b) {
            // Fire a bookmarks changed event if bookmarks were modified during event pause
            if(!fireEvents && lastBookmarkChangeTime >= lastEventPauseTime) {
                fireEvents = true;
                fireBookmarksChanged();
            }
        }
        else {
            // Remember pause start time
            if(fireEvents) {
                fireEvents = false;
                lastEventPauseTime = System.currentTimeMillis();
            }
        }
    }

    /////////////////////////////////////////
    // VectorChangeListener implementation //
    /////////////////////////////////////////

    public void elementsAdded(int startIndex, int nbAdded) {
        fireBookmarksChanged();
    }

    public void elementsRemoved(int startIndex, int nbRemoved) {
        fireBookmarksChanged();
    }

    public void elementChanged(int index) {
        fireBookmarksChanged();
    }



    // - Bookmark loading ------------------------------------------------------
    // -------------------------------------------------------------------------
    private static class Loader implements BookmarkBuilder {
        public void startBookmarks() {}
        public void endBookmarks() {}
        public void addBookmark(String name, String location) {BookmarkManager.addBookmark(new Bookmark(name, location));}
    }
}
