/*
 * 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.table;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Vector;
import java.util.WeakHashMap;

import javax.swing.DefaultListSelectionModel;
import javax.swing.ListSelectionModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableColumnModelListener;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;

/**
 * Used to keep track of a file table's columns position and visibility settings.
 * @author Nicolas Rinaudo, Maxence Bernard
 */
public class FileTableColumnModel implements TableColumnModel, PropertyChangeListener {
    // - Class constants -----------------------------------------------------------------
    // -----------------------------------------------------------------------------------
    /** If {@link #widthCache} is set to this, it needs to be recalulated. */
    private static final int                CACHE_OUT_OF_DATE = -1;
    /** Even though we're not using column selection, the table API forces us to return this instance or will crash. */
    private static final ListSelectionModel SELECTION_MODEL   = new DefaultListSelectionModel();


    // - Instance fields -----------------------------------------------------------------
    // -----------------------------------------------------------------------------------
    /** All registered listeners. */
    private WeakHashMap<TableColumnModelListener, ?> listeners  = new WeakHashMap<TableColumnModelListener, Object>();
    /** Cache for the table's total width. */
    private int                                      widthCache = CACHE_OUT_OF_DATE;
    /** All available columns. */
    private List<TableColumn>                        columns    = new Vector<TableColumn>(Column.values().length);
    /** Enabled state of each column. */
    private boolean[]     enabled = new boolean[Column.values().length];
    /** Visibility state of each column. */
    private boolean[]     visibility = new boolean[Column.values().length];
    /** Cache for the number of available columns. */
    private int           countCache;
    /** Whether the column sizes were set already. */
    private boolean       columnSizesSet;



    // - Initialization ------------------------------------------------------------------
    // -----------------------------------------------------------------------------------
    /**
     * Creates a new file table column model.
     */
    public FileTableColumnModel(FileTableConfiguration conf) {
        // The name column is always visible, so we know that the column count is always
        // at least 1.
        countCache = 1;
        FileTableHeaderRenderer headerRenderer = FileTable.createHeaderRenderer();

        // Initializes the columns.
        for(Column c : Column.values()) {
            int columnIndex = c.ordinal();

            TableColumn column = new TableColumn(columnIndex);
            columns.add(column);
            column.setCellEditor(null);
            column.setHeaderValue(c.getLabel());

            column.setHeaderRenderer(headerRenderer);

            column.addPropertyChangeListener(this);

            // Sets the column's initial width.
            if(conf.getWidth(c) != 0)
                column.setWidth(conf.getWidth(c));

            // Initialises the column's visibility and minimum width.
            if(c == Column.NAME) {
                enabled[columnIndex] = true;
            }
            else {
                if((enabled[columnIndex] = conf.isEnabled(c)))
                    countCache++;
            }
            column.setMinWidth(c.getMinimumColumnWidth());

            // Init visibility state to enabled state, FileTable will adjust the values for conditional columns later
            visibility[columnIndex] = enabled[columnIndex];
        }

        // Sorts the columns.
        Collections.sort(columns, new ColumnSorter(conf));
    }



    // - Configuration -------------------------------------------------------------------
    // -----------------------------------------------------------------------------------
    public synchronized FileTableConfiguration getConfiguration() {
        FileTableConfiguration conf = new FileTableConfiguration();
        for(Column c : Column.values()) {
            TableColumn column = columns.get(c.ordinal());
            Column modelC = Column.valueOf(column.getModelIndex());
            int modelCIndex = modelC.ordinal();

            conf.setEnabled(modelC, enabled[modelCIndex]);
            conf.setPosition(modelC, c.ordinal());
            conf.setWidth(modelC, column.getWidth());
        }

        return conf;
    }



    // - Enabled state -------------------------------------------------------------------
    // -----------------------------------------------------------------------------------

    /**
     * Returns <code>true</code> if the specified column is enabled.
     * @param column column, see {@link com.mucommander.ui.main.table.Column} for possible values
     * @return true if the column is enabled, false if disabled.
     */
    public synchronized boolean isColumnEnabled(Column column) {
        return enabled[column.ordinal()];
    }

    /**
     * Sets the specified column's enabled state.
     * @param column column, see {@link com.mucommander.ui.main.table.Column} for possible values
     * @param enabled true to enable the column, false to disable it.
     */
    public synchronized void setColumnEnabled(Column column, boolean enabled) {
        this.enabled[column.ordinal()] = enabled;
    }


    // - Visibility state ----------------------------------------------------------------
    // -----------------------------------------------------------------------------------

    /**
     * Sets the specified column's visibility state.
     * @param column column, see {@link com.mucommander.ui.main.table.Column} for possible values
     * @param visible whether the column should be visible or not.
     */
    synchronized void setColumnVisible(Column column, boolean visible) {
        // Ignores calls that won't actually change anything.
        int columnVal = column.ordinal();
        if(visibility[columnVal] != visible) {
            visibility[columnVal] = visible;
            widthCache = CACHE_OUT_OF_DATE;

            // Adds the column.
            if(visible) {
                countCache++;
                triggerColumnAdded(new TableColumnModelEvent(this, columnVal, columnVal));
            }

            // Removes the column.
            else {
                countCache--;
                triggerColumnRemoved(new TableColumnModelEvent(this, columnVal, columnVal));
            }
        }
    }

    /**
     * Returns <code>true</code> if the specified column is visible.
     * @param column column, see {@link com.mucommander.ui.main.table.Column} for possible values
     * @return <code>true</code> if the specified column is visible, <code>false</code> otherwise.
     */
    public synchronized boolean isColumnVisible(Column column) {return visibility[column.ordinal()];}

    /**
     * Adds the specified column to the model.
     * @param column column to add to the model.
     */
    public void addColumn(TableColumn column) {setColumnVisible(Column.valueOf(column.getModelIndex()), true);}

    /**
     * Removes the specified column from the model.
     * @param column column to remove from the model.
     */
    public void removeColumn(TableColumn column) {setColumnVisible(Column.valueOf(column.getModelIndex()), false);}



    // - Column retrieval ----------------------------------------------------------------
    // -----------------------------------------------------------------------------------
    private synchronized int getInternalIndex(int index) {
        int         visibleIndex;
        TableColumn column;

        // Looks for the visible column of index 'index'.
        visibleIndex = -1;
        for(int i = 0; i < visibility.length; i++) {
            column = columns.get(i);
            if(visibility[column.getModelIndex()])
                if(++visibleIndex == index)
                    return i;
        }
        // Index doesn't exist.
        throw new ArrayIndexOutOfBoundsException(Integer.toString(index));
    }

    /**
     * Returns the specified column.
     * @param  index index of the column in the model.
     * @return       the requested column.
     */
    public synchronized TableColumn getColumn(int index) {return columns.get(getInternalIndex(index));}

    public synchronized TableColumn getColumnFromId(int id) {return columns.get(id);}

    public synchronized int getColumnPosition(int id) {
        for(int i = 0; i < visibility.length; i++)
            if(columns.get(i).getModelIndex() == id)
                return i;
        return -1;
    }

    /**
     * Returns the number of columns currently displayed.
     * @return the number of columns currently displayed.
     */
    public synchronized int getColumnCount() {return countCache;}

    /**
     * Moves a column.
     * @param from index of the column to move.
     * @param to   where to move the column.
     */
    public synchronized void moveColumn(int from, int to) {
        // We need to trigger these for the file table to display the 'column dragging' animation.
        if(from == to) {
            triggerColumnMoved(new TableColumnModelEvent(this, from, to));
            return;
        }

        TableColumn column; // Buffer for the table to remove.
        int         index;  // Used to store the internal index of 'from' and 'to'.

        // Locates the internal index of the requested column
        // and removes that column
        index  = getInternalIndex(from);
        column = columns.get(index);
        columns.remove(index);

        // If the column needs to be moved at the end of the set,
        // no need to locate its correct index.
        if(to == countCache - 1)
            columns.add(column);

        // Otherwise, finds the column's internal index and inserts
        // it there.
        else {
            index  = getInternalIndex(to);
            columns.add(index, column);
        }

        // Notifies listeners and stores the new configuration.
        triggerColumnMoved(new TableColumnModelEvent(this, from, to));
    }

    public int getColumnIndex(Object identifier) {return 0;}



    // - Columns width -------------------------------------------------------------------
    // -----------------------------------------------------------------------------------
    /**
     * Returns the index of the column at the specified position.
     * @param  x position of the column to look for.
     * @return   the index of the column at the specified position, <code>-1</code> if not found.
     */
    public int getColumnIndexAtX(int x) {
        int count;

        count = getColumnCount();
        for(int i = 0; i < count; i++) {
            x = x - getColumn(i).getWidth();
            if(x < 0)
            return i;
            }
        return -1;
    }

    /**
     * Returns the total width of the table column model.
     * @return the total width of the table column model.
     */
    public int getTotalColumnWidth() {
        if(widthCache == CACHE_OUT_OF_DATE)
            computeWidthCache();
        return widthCache;
    }

    /**
     * Computes the model's width.
     */
    private void computeWidthCache() {
        Enumeration<TableColumn> elements;

        elements = getColumns();
        widthCache = 0;
        while(elements.hasMoreElements())
            widthCache += elements.nextElement().getWidth();
    }

    /**
     * Invalidates the width cache if a column's width has changed.
     */
    public void propertyChange(PropertyChangeEvent event) {
        String name;

        name = event.getPropertyName();
        if(name.equals("width")) {
                columnSizesSet = true;
            widthCache = CACHE_OUT_OF_DATE;
                // Notifies the table that columns width have changed and that it should repaint itself.
                triggerColumnMarginChanged(new ChangeEvent(this));
        }
    }

    boolean wereColumnSizesSet() {return columnSizesSet;}


    // - Columns margin ------------------------------------------------------------------
    // -----------------------------------------------------------------------------------
    // Column margin is fixed to 0.

    /**
     * Returns 0.
     * @return 0.
     */
    public int getColumnMargin() {return 0;}

    /**
     * Ignored.
     */
    public void setColumnMargin(int margin) {}



    // - Listeners -----------------------------------------------------------------------
    // -----------------------------------------------------------------------------------
    /**
     * Registers the specified column model listener.
     * @param listener listener to register.
     */
    public void addColumnModelListener(TableColumnModelListener listener) {listeners.put(listener, null);}

    /**
     * Removes the specified column model listener.
     * @param listener listener to remove.
     */
    public void removeColumnModelListener(TableColumnModelListener listener) {listeners.remove(listener);}

    /**
     * Calls all registered listeners' <code>columnAdded(event)</code>.
     * @param event event to propagate.
     */
    private void triggerColumnAdded(TableColumnModelEvent event) {
        for(TableColumnModelListener listener: listeners.keySet())
            listener.columnAdded(event);
    }

    /**
     * Calls all registered listeners' <code>columnMarginChanged(event)</code>.
     * @param event event to propagate.
     */
    private void triggerColumnMarginChanged(ChangeEvent event) {
        for(TableColumnModelListener listener: listeners.keySet())
            listener.columnMarginChanged(event);
    }

    /**
     * Calls all registered listeners' <code>columnMoved(event)</code>.
     * @param event event to propagate.
     */
    private void triggerColumnMoved(TableColumnModelEvent event) {
        for(TableColumnModelListener listener: listeners.keySet())
            listener.columnMoved(event);
    }

    /**
     * Calls all registered listeners' <code>columnRemoved(event)</code>.
     * @param event event to propagate.
     */
    private void triggerColumnRemoved(TableColumnModelEvent event) {
        for(TableColumnModelListener listener: listeners.keySet())
            listener.columnRemoved(event);
    }



    // - Column selection ----------------------------------------------------------------
    // -----------------------------------------------------------------------------------
    // Column selection is not allowed, this methods are ignored or return default values.

    /**
     * Returns <code>false</code>.
     * @return <code>false</code>.
     */
    public boolean getColumnSelectionAllowed() {return false;}

    /**
     * Returns <code>0</code>.
     * @return <code>0</code>.
     */
    public int getSelectedColumnCount() {return 0;}

    /**
     * Returns an integer array of size 0.
     * @return an integer array of size 0.
     */
    public int[] getSelectedColumns() {return new int[0];}

    /**
     * Ignored.
     */
    public void setColumnSelectionAllowed(boolean flag) {}

    /**
     * Returns a default list selection model.
     * <p>
     * Ideally, we'd like to return <code>null</code> here, but the table API takes a dim view
     * of this and we're forced to keep a useless reference.
     * </p>
     * @return a default list selection model.
     */
    public ListSelectionModel getSelectionModel() {return SELECTION_MODEL;}

    /**
     * Ignored.
     */
    public void setSelectionModel(ListSelectionModel model) {}



    // - Column enumeration --------------------------------------------------------------
    // -----------------------------------------------------------------------------------
    /**
     * Returns an enumeration on all visible columns.
     * @return an enumeration on all visible columns.
     */
    @Override
    public Enumeration<TableColumn> getColumns() {
        return new ColumnEnumeration();
    }

    public Iterator<TableColumn> getAllColumns() {
        return columns.iterator();
    }

    /**
     * Browses through the model's visible columns
     * <p>
     * This will enumerate all the elements of {@link FileTableColumnModel#columns}, skipping
     * over any that's marked as invisible.
     * </p>
     * @author Nicolas Rinaudo
     */
    private class ColumnEnumeration implements Enumeration<TableColumn> {
        // - Instance fields -------------------------------------------------------------
        // -------------------------------------------------------------------------------
        /** Index of the next available element in the enumeration. */
        private int nextIndex;



        // - Initialisation --------------------------------------------------------------
        // -------------------------------------------------------------------------------
        /**
         * Creates a new column enumeration.
         */
        public ColumnEnumeration() {
            nextIndex = -1;
            findNextElement();
        }

        /**
         * Finds the next visible element in the model.
         */
        private void findNextElement() {
            TableColumn column;

            for(nextIndex++; nextIndex < visibility.length; nextIndex++) {
                column = columns.get(nextIndex);
                if(visibility[column.getModelIndex()])
                    break;
            }
        }



        // - Enumeration methods ---------------------------------------------------------
        // -------------------------------------------------------------------------------
        /**
         * Returns <code>true</code> if there's a next element in the enumeration.
         * @return <code>true</code> if there's a next element in the enumeration, <code>false</code> otherwise.
         */
        public boolean hasMoreElements() {return nextIndex < visibility.length;}

        /**
         * Returns the next element in the enumeration.
         * @return                        the next element in the enumeration.
         * @throws NoSuchElementException if there is no next element in the enumeration.
         */
        public TableColumn nextElement() {
            // Makes sure we have at least one more element to return.
            if(!hasMoreElements())
                throw new NoSuchElementException();

            // Retrieves the next element.
            TableColumn column;
            column = columns.get(nextIndex);

            // Looks for the next one.
            findNextElement();

            return column;
        }
    }



    // - Column sorting ------------------------------------------------------------------
    // -----------------------------------------------------------------------------------
    /**
     * Used to sort the model's columns at boot time.
     * <p>
     * The sort is done by first comparing each column's index as defined in the configuration and,
     * if there's a conflict, by comparing each column's identifier.
     * </p>
     * @author Nicolas Rinaudo
     */
    private static class ColumnSorter implements Comparator<TableColumn> {
        // - Instance fields -------------------------------------------------------------
        // -------------------------------------------------------------------------------
        /** Defines the columns order. */
        private FileTableConfiguration conf;



        // - Initialisation --------------------------------------------------------------
        // -------------------------------------------------------------------------------
        /**
         * Loads the columns order as defined in the configuration.
         */
        public ColumnSorter(FileTableConfiguration conf) {this.conf = conf;}



        // - Comparator code -------------------------------------------------------------
        // -------------------------------------------------------------------------------
        /**
         * Compares <code>o1</code> and <code>o2</code>.
         */
        public int compare(TableColumn tc1, TableColumn tc2) {
            int id1;    // Identifier of the first column.
            int id2;    // Identifier of the second column.
            int index1; // Index of the first column as defined in the configuration.
            int index2; // Index of the second column as defined in the configuration.

            // Retrieves the two columns' indexes and identifiers.
            index1 = conf.getPosition(Column.valueOf(id1 = tc1.getModelIndex()));
            index2 = conf.getPosition(Column.valueOf(id2 = tc2.getModelIndex()));

            // Sort by index, then by identifier.
            if(index1 < index2)
                return -1;
            if(index1 == index2) {
                if(id1 < id2)
                    return -1;
                if (id1 == id2)
                    return 0;
            }
            return 1;
        }
    }
}
