/*
 * 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.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mucommander.commons.conf.ConfigurationEvent;
import com.mucommander.commons.conf.ConfigurationListener;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.filter.AndFileFilter;
import com.mucommander.commons.file.filter.AttributeFileFilter;
import com.mucommander.commons.file.filter.AttributeFileFilter.FileAttribute;
import com.mucommander.commons.file.util.FileComparator;
import com.mucommander.conf.MuConfigurations;
import com.mucommander.conf.MuPreferences;
import com.mucommander.desktop.ActionType;
import com.mucommander.ui.action.ActionProperties;
import com.mucommander.ui.action.impl.RefreshAction;
import com.mucommander.ui.event.LocationEvent;
import com.mucommander.ui.event.LocationListener;
import com.mucommander.ui.main.ConfigurableFolderFilter;
import com.mucommander.ui.main.FolderPanel;
import com.mucommander.ui.main.tabs.FileTableTab;
import com.mucommander.ui.main.tabs.FileTableTabs;
import com.mucommander.ui.theme.ColorChangedEvent;
import com.mucommander.ui.theme.FontChangedEvent;
import com.mucommander.ui.theme.ThemeCache;
import com.mucommander.ui.theme.ThemeListener;

/**
 * A panel which contains a directory tree. This panel is attached to the left
 * side of the files table. It allows for a quick navigation in a directory
 * tree. Selecting folder on the tree changes folder in files folder.
 * 
 * @author Mariusz Jakubowski
 * 
 */
public class FoldersTreePanel extends JPanel implements TreeSelectionListener, 
							LocationListener, FocusListener, ThemeListener, 
							TreeModelListener, ConfigurationListener {
	private static final Logger LOGGER = LoggerFactory.getLogger(FoldersTreePanel.class);
	
    /** Directory tree */
    private JTree tree;

    /** Folder panel to which this tree is attached */
    private FolderPanel folderPanel;

    /** A model with a directory tree */
    private FilesTreeModel model;

    /** A timer that fires a directory change */
    private ChangeTimer changeTimer = new ChangeTimer();

    static {
        TreeIOThreadManager.getInstance().start();
    }

   
    /**
     * Creates a panel with directory tree attached to a specified folder panel.
     * @param folderPanel a folder panel to attach tree
     */
    public FoldersTreePanel(FolderPanel folderPanel) {
        super();
        this.folderPanel = folderPanel;
        
        setLayout(new BorderLayout());

        // Filters out the files that should not be displayed in the tree view
        AndFileFilter treeFileFilter = new AndFileFilter(
            new AttributeFileFilter(FileAttribute.DIRECTORY),
            new ConfigurableFolderFilter()
        );

        FileComparator sort = new FileComparator(FileComparator.NAME_CRITERION, true, true, folderPanel.getFileTable().getFileTableModel().getNameFunc());
        model = new FilesTreeModel(treeFileFilter, sort);
        tree = new JTree(model);
		tree.setFont(ThemeCache.tableFont);
        tree.setBackground(ThemeCache.backgroundColors[ThemeCache.INACTIVE][ThemeCache.NORMAL]);

        tree.getSelectionModel().setSelectionMode(
                TreeSelectionModel.SINGLE_TREE_SELECTION);
        tree.setExpandsSelectedPaths(true);
        tree.getModel().addTreeModelListener(this);

        JScrollPane sp = new JScrollPane(tree);
        // JScrollPane usually comes with a tiny border, remove it
        sp.setBorder(null);

        add(sp, BorderLayout.CENTER);

        // Create tree renderer. We're not using default tree renderer, because
        // AbstractFile.toString method returns full path, and we want to
        // display only a file name.
        FoldersTreeRenderer renderer = new FoldersTreeRenderer(tree);
        tree.setCellRenderer(renderer);

        tree.addTreeSelectionListener(this);
        tree.addFocusListener(this);

        // add a popup menu
        final JPopupMenu popup = new JPopupMenu();
        // refresh action
        JMenuItem item = new JMenuItem(
        		ActionProperties.getActionLabel(ActionType.Refresh),
                KeyEvent.VK_R);
        item.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                model.refresh(tree.getSelectionPath());
//                model.fireTreeStructureChanged(tree, tree.getSelectionPath());
            }
        });
        popup.add(item);
        tree.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                maybeShowPopup(e);
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                maybeShowPopup(e);
            }

            private void maybeShowPopup(MouseEvent e) {
                if (e.isPopupTrigger()) {
                    popup.show(e.getComponent(), e.getX(), e.getY());
                }
            }
        });
        
        ThemeCache.addThemeListener(this);
        
        MuConfigurations.addPreferencesListener(this);
    }

    
    
    /** 
     * Listens to certain configuration variables.
     */
    public void configurationChanged(ConfigurationEvent event) {
        String var = event.getVariable();
        if (var.equals(MuPreferences.SHOW_HIDDEN_FILES) ||
                var.equals(MuPreferences.SHOW_DS_STORE_FILES) ||
                var.equals(MuPreferences.SHOW_SYSTEM_FOLDERS)) {
            Object root = model.getRoot();
            if (root != null) {
                TreePath path = new TreePath(root);
                model.refresh(path);
            }
        }
    }

    /**
     * Adds or removes location change listeners depending on the tree
     * visibility.
     */
    @Override
    public void setVisible(boolean flag) {
        super.setVisible(flag);
        if (flag) {
            updateSelectedFolder();
            folderPanel.getLocationManager().addLocationListener(this);
            // tree.requestFocus();
        } else {
            folderPanel.getLocationManager().removeLocationListener(this);
        }
    }


	/**
     * Updates selection in a tree to the current folder. When necessary updates
     * the current root of a tree. Invoked when location on folder pane has changed or 
     * when a tree has been updated (when directories have been loaded).
     */
    private void updateSelectedFolder() {
        final AbstractFile currentFolder = folderPanel.getCurrentFolder();

        // get selected directory (ignore archives - TODO make archives browsable (option))
        AbstractFile tempFolder = currentFolder;
        AbstractFile tempParent;
        while (!tempFolder.isDirectory()) {
            tempParent = tempFolder.getParent();
            if(tempParent==null)
                break;

            tempFolder = tempParent;
        }

        // compare selection on tree and panel
        final AbstractFile selectedFolder = tempFolder;
        TreePath selectionPath = tree.getSelectionPath();
        if (selectionPath != null) {
            if (selectionPath.getLastPathComponent() == currentFolder)
                return;
        }

        // check if root has changed
        final AbstractFile currentRoot = selectedFolder.getRoot();
        if (!currentRoot.equals(model.getRoot())) {
            model.setRoot(currentRoot);
        }
        // refresh selection on tree
        SwingUtilities.invokeLater(() -> {
            try {
                TreePath path = new TreePath(model.getPathToRoot(selectedFolder));
                tree.expandPath(path);
                tree.setSelectionPath(path);
                tree.scrollPathToVisible(path);
            } catch (Exception e) {
                LOGGER.debug("Caught exception", e);
            }
        });
    }

    /**
     * Refreshes folder after a change (e.g. mkdir).
     * @param folder a folder to refresh on the tree
     */
    public void refreshFolder(AbstractFile folder) {
        if (!isVisible())
            return;
        model.fireTreeStructureChanged(tree, new TreePath(model.getPathToRoot(folder)));
    }
    
    /**
     * Changes focus to tree.
     */
    @Override
    public void requestFocus() {
        tree.requestFocus();
    }

    /**
     * Returns tree component.
     * @return tree component
     */
    public JTree getTree() {
        return tree;
    }

    

    // - TreeSelectionListener code --------------------------------------------
    // -------------------------------------------------------------------------
    
    /**
     * This class is used to change folder after a user selects a folder in
     * tree. This change occurs after small delay (1 sec) to allow a user to
     * navigate a tree using keyboard.
     * 
     * @author Mariusz Jakubowski
     * 
     */
    private class ChangeTimer extends Timer {
        private transient AbstractFile folder;

        public ChangeTimer() {
            super(1000, null);
            setRepeats(false);
        }

        @Override
        public void fireActionPerformed(ActionEvent ae) {
            if (!folderPanel.getCurrentFolder().equals(folder)) {
                FileTableTabs tabs = folderPanel.getTabs();
                FileTableTab tab = tabs.getCurrentTab();
                if (tab.isLocked())
                    tabs.add(folder);
                else
                    folderPanel.tryChangeCurrentFolder(folder);
            }
        }
    }
    

    /**
     * Changes the current folder in an associated folder panel, depending on
     * the current selection in tree.
     */
    public void valueChanged(TreeSelectionEvent e) {
        TreePath path = e.getNewLeadSelectionPath();
        if (path != null) {
            AbstractFile f = (AbstractFile) path.getLastPathComponent();
            if (f != null && f.isBrowsable() && f != folderPanel.getCurrentFolder()) {
                changeTimer.folder = f;
                changeTimer.restart();
            }
        }
    }

    // - LocationListener code -------------------------------------------------
    // -------------------------------------------------------------------------

    public void locationChanged(LocationEvent locationEvent) {
        updateSelectedFolder();
    }

    // - FocusListener code ----------------------------------------------------
    // -------------------------------------------------------------------------
    
    public void focusGained(FocusEvent e) {
		tree.setBackground(ThemeCache.backgroundColors[ThemeCache.ACTIVE][ThemeCache.NORMAL]);	
	}

	public void focusLost(FocusEvent e) {
		tree.setBackground(ThemeCache.backgroundColors[ThemeCache.INACTIVE][ThemeCache.NORMAL]);	
	}

	
    // - ThemeListener code ----------------------------------------------------
    // -------------------------------------------------------------------------
	
	public void colorChanged(ColorChangedEvent event) {
		if (tree.hasFocus()) {
			tree.setBackground(ThemeCache.backgroundColors[ThemeCache.ACTIVE][ThemeCache.NORMAL]);	
		} else {
			tree.setBackground(ThemeCache.backgroundColors[ThemeCache.INACTIVE][ThemeCache.NORMAL]);	
		}
		tree.repaint();
	}

	public void fontChanged(FontChangedEvent event) {
		tree.setFont(ThemeCache.tableFont);
		tree.repaint();
	}

    // - TreeModelListener code ------------------------------------------------
    // -------------------------------------------------------------------------

	public void treeNodesChanged(TreeModelEvent e) {
    }

    public void treeNodesInserted(TreeModelEvent e) {
    }

    public void treeNodesRemoved(TreeModelEvent e) {
    }

    public void treeStructureChanged(TreeModelEvent e) {
        // ensures that a selection is repainted correctly
        // after nodes have been inserted                
        if (!changeTimer.isRunning()) {        
            updateSelectedFolder();
            tree.repaint();
        }
    }

}
