package com.limegroup.gnutella.gui;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;

import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.WriterAppender;
import org.apache.log4j.spi.LoggerRepository;

import com.limegroup.gnutella.gui.themes.ThemeFileHandler;
import com.limegroup.gnutella.gui.themes.ThemeMediator;
import com.limegroup.gnutella.gui.themes.ThemeObserver;
import com.limegroup.gnutella.settings.ConsoleSettings;

/**
 * A Console for log/any output
 */
public class Console extends JPanel implements ThemeObserver {

    private final int idealSize;
    private final int maxExcess;
    
    private JScrollPane scrollPane;

    private JTextArea output;

    private JButton apply;
    private JButton clear;
    
    private JComboBox loggerComboBox;

    private JComboBox levelComboBox;

    private boolean scroll = true;
    
    public Console() {
        
        idealSize = ConsoleSettings.CONSOLE_IDEAL_SIZE.getValue();
        maxExcess = ConsoleSettings.CONSOLE_MAX_EXCESS.getValue();
        
        output = new JTextArea();
        output.setEditable(false);

        scrollPane = new JScrollPane(output);
        scrollPane.getVerticalScrollBar().addAdjustmentListener(
                new AdjustmentListener() {
                    public void adjustmentValueChanged(AdjustmentEvent e) {
                        if (e.getValueIsAdjusting()) {
                            scroll = false;
                        } else {
                            scroll = true;
                        }
                    }
                });

        loggerComboBox = new JComboBox(new LoggerComboBoxModel());
        levelComboBox = new JComboBox(new LevelComboBoxModel());
        
        loggerComboBox.setAutoscrolls(true);
        loggerComboBox.setMaximumRowCount(20);
        loggerComboBox.addItemListener(new ItemListener() {
            public void itemStateChanged(ItemEvent evt) {
                selectLoggerLevel();
            }
        });
        
        loggerComboBox.addPopupMenuListener(new PopupMenuListener() {
            public void popupMenuWillBecomeVisible(PopupMenuEvent evt) {
                refreshLoggers();
            }
            
            public void popupMenuCanceled(PopupMenuEvent evt) {}
            public void popupMenuWillBecomeInvisible(PopupMenuEvent evt) {}
        });
        
        levelComboBox.setAutoscrolls(true);
        
        apply = new JButton(GUIMediator.getStringResource("GENERAL_APPLY_BUTTON_LABEL"));
        apply.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
                applyLevel();
            }
        });

        clear = new JButton(GUIMediator.getStringResource("GENERAL_CLEAR_BUTTON_LABEL"));
        clear.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
                clear();
            }
        });
        
        setLayout(new BorderLayout());
        add(BorderLayout.CENTER, scrollPane);

        JPanel controlsPanel = new JPanel();
        controlsPanel.setLayout(new GridBagLayout());

        GridBagConstraints gbc = new GridBagConstraints();

        gbc.gridx = 0;
        gbc.gridy = 0;
        gbc.gridwidth = 1;
        gbc.gridheight = 1;
        gbc.weightx = 2;
        gbc.weighty = 0;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        controlsPanel.add(loggerComboBox, gbc);

        gbc.gridx = 1;
        gbc.gridy = 0;
        gbc.gridwidth = 1;
        gbc.gridheight = 1;
        gbc.weightx = 0;
        gbc.weighty = 0;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        controlsPanel.add(levelComboBox, gbc);

        gbc.gridx = 2;
        gbc.gridy = 0;
        gbc.gridwidth = 1;
        gbc.gridheight = 1;
        gbc.weightx = 0;
        gbc.weighty = 0;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        controlsPanel.add(apply, gbc);
        
        gbc.gridx = 3;
        gbc.gridy = 0;
        gbc.gridwidth = 1;
        gbc.gridheight = 1;
        gbc.weightx = 0;
        gbc.weighty = 0;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        controlsPanel.add(clear, gbc);
        
        add(BorderLayout.SOUTH, controlsPanel);

        updateTheme();
        ThemeMediator.addThemeObserver(this);

        refreshLoggers();
        attachLogs();
    }
    
    /**
     * 
     */
    private void attachLogs() {
        WriterAppender append = new WriterAppender(new PatternLayout(
                ConsoleSettings.CONSOLE_PATTERN_LAYOUT.getValue()), new ConsoleWriter());
        LogManager.getRootLogger().addAppender(append);
    }
    
    /**
     * Rebuilds the Logger ComboBox
     */
    private void refreshLoggers() {
        LoggerRepository repository = LogManager.getLoggerRepository();
        Enumeration currentLoggers = repository.getCurrentLoggers();
        
        LoggerComboBoxModel loggerModel = (LoggerComboBoxModel) loggerComboBox.getModel();
        int loggerIndex = loggerComboBox.getSelectedIndex();
        LoggerNode currentLogger = (loggerIndex >= 0) ? loggerModel.getLogger(loggerIndex) : null;
        
        /*
         * Step 1: Create a Tree of Packages and Classes
         */
        ArrayList pkgList = new ArrayList();
        HashMap pkgMap = new HashMap();
        while (currentLoggers.hasMoreElements()) {
            Logger lggr = (Logger)currentLoggers.nextElement();
            
            String pkg = PackageNode.getPackage(lggr);
            PackageNode node = (PackageNode)pkgMap.get(pkg);
            if (node == null) {
                node = new PackageNode(pkg);
                pkgMap.put(pkg, node);
                pkgList.add(node);
            }
            node.add(lggr);
        }
        
        /*
         * Step 2: Sort the Packages by name
         */
        Collections.sort(pkgList, new Comparator() {
            public int compare(Object o1, Object o2) {
                return ((PackageNode) o1).getName().compareTo(((PackageNode) o2).getName());
            }
        });
        
        /*
         * Step 3: Turn the Tree into a flat List of
         * 
         * Package
         *     Class
         *     Class
         * Package
         *     Class
         *     ...
         */
        loggerIndex = -1;
        ArrayList nodes = new ArrayList();
        for(Iterator it = pkgList.iterator(); it.hasNext(); ) {
            PackageNode pkgNode = (PackageNode)it.next();
            pkgNode.sort();
            nodes.add(pkgNode);
            
            if (loggerIndex == -1
                    && pkgNode.equals(currentLogger)) {
                loggerIndex = nodes.size()-1;
            }
            
            for(Iterator it2 = pkgNode.getNodes().iterator(); it2.hasNext(); ) {
                ClassNode classNode = (ClassNode)it2.next();
                nodes.add(classNode);
                
                if (loggerIndex == -1 
                        && classNode.equals(currentLogger)) {
                    loggerIndex = nodes.size()-1;
                }
            }
        }
        
        loggerModel.refreshLoggers(nodes);
        
        boolean empty = nodes.isEmpty();
        loggerComboBox.setEnabled(!empty);
        levelComboBox.setEnabled(!empty);
        apply.setEnabled(!empty);
        
        if (!empty) {
            loggerComboBox.setSelectedIndex(loggerIndex >= 0 ? loggerIndex : 0);
            selectLoggerLevel();
        }
    }

    /**
     * Selects the Level of the currently selected Logger
     */
    private void selectLoggerLevel() {
        LoggerComboBoxModel loggerModel = (LoggerComboBoxModel) loggerComboBox.getModel();
        LevelComboBoxModel levelModel = (LevelComboBoxModel) levelComboBox.getModel();
        
        int loggerIndex = loggerComboBox.getSelectedIndex();
        if (loggerIndex < 0)
            return;
        
        Level level = getLevel(loggerModel.getLogger(loggerIndex));

        levelModel.setSelectedItem(level);
    }
    
    /**
     * Applies the currently selected logging level
     */
    private void applyLevel() {
        LoggerComboBoxModel loggerModel = (LoggerComboBoxModel) loggerComboBox.getModel();
        LevelComboBoxModel levelModel = (LevelComboBoxModel) levelComboBox.getModel();

        int loggerIndex = loggerComboBox.getSelectedIndex();
        if (loggerIndex < 0)
            return;
        
        LoggerNode logger = loggerModel.getLogger(loggerIndex);
        Level currentLevel = getLevel(logger);

        int levelIndex = levelComboBox.getSelectedIndex();
        Level newLevel = (levelIndex > 0) ? levelModel.getLevel(levelIndex) : null;

        if (!currentLevel.equals(newLevel)) {
            logger.setLevel(newLevel);
            loggerComboBox.setSelectedIndex(loggerIndex); // update the ComboxBox  (the text)
            loggerModel.updateIndex(loggerIndex);
        }
    }

    /**
     * Appends text to the console.
     * 
     * @param text
     *            The text to be appended
     */
    public void appendText(final String text) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                output.append(text);

                int excess = output.getDocument().getLength() - idealSize;
                if (excess >= maxExcess) {
                    output.replaceRange("", 0, excess);
                }
                if (scroll)
                    output.setCaretPosition(output.getText().length());
            }
        });
    }

    /**
     * Clears the console.
     */
    public void clear() {
        output.setText(null);
    }

    /**
     * Updates the appearance of this panel based on the current theme.
     */
    public void updateTheme() {
        Color tableColor = ThemeFileHandler.TABLE_BACKGROUND_COLOR.getValue();
        scrollPane.getViewport().setBackground(tableColor);
    }

    /**
     * Returns Level.OFF instead of null if logging is turned off
     */
    private static final Level getLevel(LoggerNode logger) {
        Level level = logger.getLevel();
        if (level == null)
            level = Level.OFF;
        return level;
    }
    
    private final class ConsoleWriter extends Writer {

        private StringBuffer buffer = new StringBuffer();

        public void write(char[] cbuf, int off, int len) {
            buffer.append(cbuf, off, len);
        }

        public void close() {
            buffer = null;
        }

        public void flush() {
            Console.this.appendText(buffer.toString());
            buffer.setLength(0);
        }
    }
    
    /**
     * Logger ComboBox model
     */
    private static class LoggerComboBoxModel extends DefaultComboBoxModel {
        
        private static final String SPACER = "    ";
        
        private List nodes = Collections.EMPTY_LIST;
        
        private void updateIndex(int index) {
            fireContentsChanged(this, index, index);
        }
        
        private void refreshLoggers(List nodes) {
            this.nodes = nodes;
            fireContentsChanged(this, 0, nodes.size());
        }

        public int getSize() {
            return nodes.size();
        }

        private LoggerNode getLogger(int index) {
            return (LoggerNode)nodes.get(index);
        }

        public Object getElementAt(int index) {
            LoggerNode logger = getLogger(index);
            Level level = getLevel(logger);
            
            if (level.equals(Level.OFF)) {
                if (logger.isLeaf()) {
                    return SPACER + logger.getName();
                } else {
                    return logger.getName();
                }
            } else {
                if (logger.isLeaf()) {
                    return SPACER + logger.getName() + " [" + level + "]";
                } else {
                    return logger.getName();
                }
            }
        }
    }

    /**
     * Logging level ComboBox model
     */
    private class LevelComboBoxModel extends DefaultComboBoxModel {

        private final Level[] levels = new Level[] { 
                Level.OFF, 
                Level.ALL,
                Level.DEBUG, 
                Level.ERROR, 
                Level.FATAL, 
                Level.INFO, 
                Level.WARN 
        };

        public int getSize() {
            return levels.length;
        }

        private Level getLevel(int index) {
            return levels[index];
        }

        public Object getElementAt(int index) {
            return getLevel(index).toString();
        }
    }
    
    /**
     * A interface to build a very simple Tree of 
     * Packages and Classes
     */
    private interface LoggerNode {
        boolean isLeaf();
        Level getLevel();
        void setLevel(Level level);
        String getName();
    }
    
    private static class PackageNode implements LoggerNode {
        
        private String pkg;
        private ArrayList classNodes = new ArrayList();
        
        private PackageNode(String pkg) {
            this.pkg = pkg;
        }
        
        public void add(Logger logger) {
            classNodes.add(new ClassNode(this, logger));
        }
        
        public Level getLevel() {
            return Level.OFF;
        }
        
        public void setLevel(Level level) {
            for(int i = classNodes.size()-1; i >= 0; i--) {
                ((ClassNode)classNodes.get(i)).setLevel(level);
            }
        }
        
        public boolean isLeaf() {
            return false;
        }
        
        public void sort() {
            Collections.sort(classNodes, new Comparator() {
                public int compare(Object o1, Object o2) {
                    return ((ClassNode) o1).getName().compareTo(((ClassNode) o2).getName());
                }
            });
        }
        
        public List getNodes() {
            return classNodes;
        }
        
        public int hashCode() {
            return pkg.hashCode();
        }
        
        public boolean equals(Object o) {
            if (!(o instanceof PackageNode)) {
                return false;
            }
            return pkg.equals(((PackageNode)o).pkg);
        }
        
        public String getName() {
            return pkg;
        }
        
        public String toString() {
            return getName();
        }
        
        private static String getPackage(Logger logger) {
            String name = logger.getName();
            return name.substring(0, name.lastIndexOf('.')) + ".*";
        }
    }
    
    private static class ClassNode implements LoggerNode {
        
        private PackageNode parent;
        private Logger logger;
        
        private ClassNode(PackageNode parent, Logger logger) {
            this.parent = parent;
            this.logger = logger;
        }
        
        public PackageNode getParent() {
            return parent;
        }
        
        public Logger getLogger() {
            return logger;
        }
        
        public Level getLevel() {
            return logger.getLevel();
        }
        
        public void setLevel(Level level) {
            logger.setLevel(level);
        }
        
        public boolean isLeaf() {
            return true;
        }
        
        public String getName() {
            return logger.getName();
        }
        
        public int hashCode() {
            return getName().hashCode();
        }
        
        public boolean equals(Object o) {
            if (!(o instanceof ClassNode)) {
                return false;
            }
            return getName().equals(((ClassNode)o).getName());
        }
        
        public String toString() {
            return getName();
        }
    }
}
