/*
 * 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.ui.main.tree;

import java.util.Arrays;

import javax.swing.Icon;
import javax.swing.SwingUtilities;
import javax.swing.event.EventListenerList;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.filter.FileFilter;
import com.mucommander.commons.file.util.FileComparator;
import com.mucommander.ui.icon.FileIcons;
import com.mucommander.ui.icon.SpinningDial;

/**
 * A tree model for files.
 * This class contains a tree structure defined by AbstractFile objects.

 * @author Mariusz Jakubowski
 * 
 */
public class FilesTreeModel implements TreeModel, CachedDirectoryListener {

    private DirectoryCache cache;
    
    /** Comparator used to sort folders */
    private FileComparator sort;
    
    /** Listeners. */
    protected EventListenerList listenerList = new EventListenerList();

    /** Root of the directory tree. */
    private AbstractFile root;

    /** number of caching children at the time, used to control spinning icon */
    private int cachingNum = 0;

    /** icon used to show that a children of a directory are being cached */
    private SpinningDial spinningIcon = new SpinningDial(16, 16, false);

    /** filter out the files that appear in the tree view */
    private FileFilter filter;

    public FilesTreeModel(FileFilter filter, FileComparator sort) {
        this.filter = filter;
        setFilenameLocale(sort);
    }

    void setFilenameLocale(FileComparator sort) {
        this.sort = sort;
        cache = new DirectoryCache(filter, sort);
        cache.addCachedDirectoryListener(this);
    }

    /**
     * Changes the current root of a tree
     * Fires 'tree structure changed' event.
     * @param newRoot the new root of a tree
     */
    public void setRoot(AbstractFile newRoot) {
        final CachedDirectory cachedRoot = new CachedDirectory(newRoot, cache);
        cachedRoot.setCachedIcon(FileIcons.getFileIcon(newRoot));
        SwingUtilities.invokeLater(() -> {
            root = cachedRoot.getProxiedFile();
            cache.clear();
            cache.put(root, cachedRoot);
            TreePath path = new TreePath(root);
            fireTreeStructureChanged(this, path);
        });
    }
    
    public Object getRoot() {
        return root;
    }
    

    /**
     * Returns children folders of a parent folder sorted by name.
     * @param parent parent folder
     * @return children folders
     */
    private AbstractFile[] getChildren(AbstractFile parent) {
        CachedDirectory cachedDir = cache.getOrAdd(parent);
        if (cachedDir.isCached())
            return cachedDir.get();
        else
            return null;
    }

    public Object getChild(Object parent, int index) {
        AbstractFile[] children = getChildren((AbstractFile) parent);
        if (children != null) {
            return children[index];
        }
        return null;
    }

    public int getChildCount(Object parent) {
        AbstractFile[] children = getChildren((AbstractFile) parent);
        if (children != null) {
            return children.length;
        }
        return 0;
    }

    @Override
    public int getIndexOfChild(Object parent, Object child) {
        AbstractFile[] children = getChildren((AbstractFile) parent);
        if (children != null) {
            return Arrays.binarySearch(children, (AbstractFile)child, sort);
        }
        return 0;
    }

    public boolean isLeaf(Object node) {
        return false;
    }

    public void valueForPathChanged(TreePath path, Object newValue) {
    }

    /**
     * Notifies all listeners that have registered interest for notification on
     * this event type.
     * @param source the node where the tree model has changed
     * @param path the path to the root node
     * @see EventListenerList
     */
    void fireTreeStructureChanged(Object source, TreePath path) {
        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();
        TreeModelEvent e = null;
        // Process the listeners last to first, notifying
        // those that are interested in this event
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == TreeModelListener.class) {
                // Lazily create the event:
                if (e == null) {
                    e = new TreeModelEvent(source, path);
                }
                ((TreeModelListener) listeners[i + 1]).treeStructureChanged(e);
            }
        }
    }

    /**
     * Builds the parents of node up to and including the root node,
     * where the original node is the last element in the returned array.
     * The length of the returned array gives the node's depth in the
     * tree.
     * 
     * @param aNode the TreeNode to get the path for
     */
    public AbstractFile[] getPathToRoot(AbstractFile aNode) {
        return getPathToRoot(aNode, 0);
    }

    /**
     * Builds the parents of node up to and including the root node,
     * where the original node is the last element in the returned array.
     * The length of the returned array gives the node's depth in the
     * tree.
     * 
     * @param aNode  the TreeNode to get the path for
     * @param depth  an int giving the number of steps already taken towards
     *        the root (on recursive calls), used to size the returned array
     * @return an array of TreeNodes giving the path from the root to the
     *         specified node 
     */
    protected AbstractFile[] getPathToRoot(AbstractFile aNode, int depth) {
        AbstractFile[]              retNodes;
    // This method recurses, traversing towards the root in order
    // size the array. On the way back, it fills in the nodes,
    // starting from the root and working back to the original node.

        /* Check for null, in case someone passed in a null node, or
           they passed in an element that isn't rooted at root. */
        if(aNode == null) {
            if(depth == 0)
                return null;
            else
                retNodes = new AbstractFile[depth];
        }
        else {
            depth++;
            if(aNode == root) {
                retNodes = new AbstractFile[depth];
            } else {
                retNodes = getPathToRoot(aNode.getParent(), depth);
            }
            retNodes[retNodes.length - depth] = aNode;
            cache.getOrAdd(aNode).isCached();       // ensures that a path is in cache
        }
        return retNodes;
    }
    
    public void addTreeModelListener(TreeModelListener l) {
        listenerList.add(TreeModelListener.class, l);
    }

    public void removeTreeModelListener(TreeModelListener l) {
        listenerList.remove(TreeModelListener.class, l);
    }

    /**
     * Refreshes tree model from given path.
     * @param path a path to refresh
     */
    public void refresh(TreePath path) {
        AbstractFile folder = (AbstractFile) path.getLastPathComponent();
        CachedDirectory cached = cache.get(folder);
        Icon cachedIcon = cached.getCachedIcon();        
        cache.removeWithChildren(folder);
        cached = cache.getOrAdd(folder);
        cached.setCachedIcon(cachedIcon);
        fireTreeStructureChanged(this, path);
    }

    public void cachingStarted(AbstractFile parent) {
        cachingNum++;
        if (cachingNum == 1) {
            spinningIcon.setAnimated(true);
        }
    }

    public void cachingEnded(AbstractFile parent) {
        cachingNum--;
        if (cachingNum == 0) {
            spinningIcon.setAnimated(false);
        }
        TreePath path = new TreePath(getPathToRoot(parent));
        fireTreeStructureChanged(this, path);
    }
    
    /**
     * Returns an icon of this directory or spinning icon if this directory is
     * being cached.
     * @return an icon of this directory or spinning icon if this directory is
     *         being cached.
     */
    public Icon getCurrentIcon(AbstractFile file) {
        CachedDirectory cached = cache.get(file);
        if (cached != null) {
            if (cached.isReadingChildren()) {
                return spinningIcon;
            }
            return cached.getCachedIcon();
        }
        return spinningIcon;
    }


}
