package com.limegroup.gnutella.gui.search;

import java.io.IOException;

import javax.swing.BoxLayout;
import javax.swing.JList;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import org.xml.sax.SAXException;

import com.limegroup.gnutella.MediaType;
import com.limegroup.gnutella.gui.BoxPanel;
import com.limegroup.gnutella.xml.LimeXMLDocument;
import com.limegroup.gnutella.xml.SchemaNotFoundException;

/**
 * The Panel that contains the various FilterBoxes.
 */
class FilterPanel extends BoxPanel {
    
	/**
	 * The current uniqueID.
	 */
	private static int uniqueIdCount = 0;

	/**
	 * The unique identifier (different than the GUID, which can change)
	 * for this FilterPanel.
	 */
	private final String ID;
	
    /**
     * The first filter box.
     */
    private final FilterBox BOX_1;
    
    /**
     * The second filter box.
     */
    private final FilterBox BOX_2;
    
    /**
     * The third filter box.
     */
    private final FilterBox BOX_3;
        
    /**
     * The ResultPanel this is filtering.
     */
    private final ResultPanel RESULTS;
    
    /**
     * The MetadataModel this is getting selectors from.
     */
    private final MetadataModel MODEL;
    
    /**
     * Constructs a new FilterPanel for the specified ResultPanel.
     *
     * The exact FilterBoxes added to FilterPanel are dependent on prior
     * user preferences.
     *
     */
    public FilterPanel(final ResultPanel results) {
        super(BoxLayout.Y_AXIS);
        
        String searchType = results.getMediaType().getMimeType();
        Selector one = SelectorsHandler.getSelector(searchType, 0);
        Selector two = SelectorsHandler.getSelector(searchType, 1);
        Selector three = SelectorsHandler.getSelector(searchType, 2);
        
        MetadataModel model = results.getMetadataModel();
        
        BOX_1 = new FilterBox(model, one);
        BOX_2 = new FilterBox(model, two);
        BOX_3 = new FilterBox(model, three);
        RESULTS = results;
        MODEL = model;
        
        // Create a single listener for all boxes and dispatch
        // the event depending on which box it came from.
        new MinimizeManager(new SelectorListener());
        
        add(BOX_1.getComponent());
        add(BOX_2.getComponent());
        add(BOX_3.getComponent());
		
        setMatchingValues(null);

        ID = "" + uniqueIdCount++;
    }
    
    /**
     * Returns a description unique to this FilterPanel.
     */
    String getUniqueDescription() {
       return ID;
    }     

	/**
	 * Returns an array of the three {@FilterBox filter boxes} managed by
	 * this FilterPanel.
	 * @return
	 */
	public FilterBox[] getBoxes() {
		return new FilterBox[] { BOX_1, BOX_2, BOX_3 };
	}
	
    /**
     * Attempts to set any matching values
     * in the filter boxes.  If 'box' is null, matches against
     * all boxes.  Otherwise matches only against 'box'.
     */
    private void setMatchingValues(FilterBox box) {
        SearchInformation info = RESULTS.getSearchInformation();
        // only select on keyword searches.
        if(info.isKeywordSearch()) {
            LimeXMLDocument doc = null;
            String xml = RESULTS.getRichQuery();
            if(xml != null) {
                try {
                    doc = new LimeXMLDocument(xml);
                }
                catch(SAXException se) {}
                catch(SchemaNotFoundException snfe) {}
                catch(IOException ioe) {}
            }

            if(box != null) {
                selectMatchingValues(box, doc, info);
            } else {
                selectMatchingValues(BOX_1, doc, info);
                selectMatchingValues(BOX_2, doc, info);
                selectMatchingValues(BOX_3, doc, info);
            }
        }
    }
    
    /**
     * Selects any values in the FilterBox if they match the document.
     */
    private void selectMatchingValues(FilterBox box, LimeXMLDocument doc,
                                      SearchInformation info) {
        // only works for field selectors.
        if(!box.getSelector().isFieldSelector())
            return;
            
        String xml = info.getXML();
        String query = info.getQuery();
        MediaType media = info.getMediaType();
            
        String value = null;
        
        // if there was a XMLDocument for searching, use that for matching
        if(doc != null) {
            // only select if the selector's schema matches the doc's schema    
            String schema = box.getSelector().getSchema();
            if(!schema.equals(doc.getSchemaDescription()))
                return;
            
            String field = box.getSelector().getValue();
            // Get the value of the selector's field from the document.
            value = doc.getValue(field);
        } else if(media == MediaType.getAnyTypeMediaType()) {
            // otherwise, if they did an any-type search,
            // try and match up in any of the boxes.
            value = query;
        }

        // If we have a matching value, use it.
        if(value != null) {
            box.setRequestedValue(value);
        }
    }
    
    /**
     * Manages when minimizing the boxes is/isn't allowed.
     */
    private class MinimizeManager implements ChangeListener {
        final ChangeListener SAVER;
        
        /**
         * Adds itself as a stateChangeListener on the boxes.
         */
        MinimizeManager(ChangeListener cl) {
            BOX_1.setStateChangeListener(this);
            BOX_2.setStateChangeListener(this);
            BOX_3.setStateChangeListener(this);
            SAVER = cl;
            stateChanged(null);
        }
        
        /**
         * Notification that a FilterBox has become minimized or restored.
         */
        public void stateChanged(ChangeEvent event) {
            boolean one, two, three;
            one = BOX_1.isMinimized();
            two = BOX_2.isMinimized();
            three = BOX_3.isMinimized();
            
            // if all three somehow got minimized,
            // make the first one restored again.
            if(one && two && three) {
                BOX_1.setStateChangeListener(null);
                BOX_1.restore();
                one = false;
                BOX_1.setStateChangeListener(this);
            }

            if(one && two) {
                BOX_3.setCanMinimize(false);
            } else if(one && three) {
                BOX_2.setCanMinimize(false);
            } else if(two && three) {
                BOX_1.setCanMinimize(false);
            } else {
                BOX_1.setCanMinimize(true);
                BOX_2.setCanMinimize(true);
                BOX_3.setCanMinimize(true);
            }

            removeAll();            
            add(BOX_1.getComponent());
            add(BOX_2.getComponent());
            add(BOX_3.getComponent());
            invalidate();
            revalidate();
            repaint();
            SAVER.stateChanged(event);
        }
    }       
    
    /**
     * A listener for the filter boxes.
     *
     * Dispatches events based on the source of the selection.
     */
    private class SelectorListener implements ListDataListener, 
                                       ListSelectionListener, ChangeListener {
        
        /**
         * "boolean marker" used to prevent contentChanged method from
         * triggering a chain of methods calls which calls itself
         * again. <pre>triggered</pre> is set to true in contentsChanged before
         * calling updateBoxes, so when it gets called again, it returns
         * immediately. Hacky but effective. The variable is reset after the
         * call.
         */
        private boolean triggered = false;
        
        /**
         * Constructs a new SelectorListener for the specified boxes.
         */
        SelectorListener() {
            BOX_1.addSelectionListener(this, this);
            BOX_2.addSelectionListener(this, this);
            BOX_3.addSelectionListener(this, this);
            
            BOX_1.setSelectorChangeListener(this);
            BOX_2.setSelectorChangeListener(this);
            BOX_3.setSelectorChangeListener(this);
        }
        
        /**
         * Notification that the selectors have changed for a box.
         * TODO: Update only the depth that changed.
         */
        public void stateChanged(ChangeEvent event) {
            String type = RESULTS.getMediaType().getMimeType();
            SelectorsHandler.setSelector(type, 0, BOX_1.getSelector());
            SelectorsHandler.setSelector(type, 1, BOX_2.getSelector());
            SelectorsHandler.setSelector(type, 2, BOX_3.getSelector());
            if(event != null) {
                FilterBox box = (FilterBox)event.getSource();
                setMatchingValues(box);
            }
        }

        /**
         * Notification that the selected value has changed.
         *
         * Updates the selector on the ResultPanel and the list of the
         * latter filter boxes.
         *
         * The event is ignored if the value is adjusting (the mouse is still
         * down and moving).
         */ 
        public void valueChanged(ListSelectionEvent event) {
            // extraneous events. 
            if(event.getValueIsAdjusting())
                return; 

            // Because the source of the event is the JList,
            // we must figure out which FilterBox it came from
            // based on the list of the boxes.
            JList source = (JList)event.getSource();
            FilterBox box = null;
            int depth = -1;

            if(source == BOX_1.getList()) {
                box = BOX_1;
                depth = 1;
            } else if(source == BOX_2.getList()) {
                box = BOX_2;
                depth = 2;
            } else if(source == BOX_3.getList()) {
                box = BOX_3;
                depth = 3;
            } else {
                throw new IllegalStateException("invalid source: " + source);
            }
            
            Selector selector = box.getSelector();
            Object value = box.getSelectedValue();
            boolean isAll = event.getFirstIndex() == 0 &&
                            event.getLastIndex() == 0;
            // only update the latter boxes only if the selection actually
            // changed or we selected 'All'.  we must special-case All
            // because the filter doesn't change when we go from no selection
            // to an All selection, but we want to erase the lower selections.
            if(isAll || selectionChanged(selector, value, depth))
                updateBoxes(depth, box, true);
        }

        /**
         * Notification that contents of a FilterBox have changed, we are going
         * to use this information to update the box above the one in which
         * items were added, which will cause this box to the updated as well.
         */
        public void contentsChanged(ListDataEvent event) {
            if(triggered)
                return;

            Object source = event.getSource();
            FilterBox box = null;
            int depth = -1;

            if(source == BOX_1.getList().getModel()) {
                box = null;
                depth = -1;
            } else if(source == BOX_2.getList().getModel()) {
                box = BOX_1;
                depth = 0;
            } else if(source == BOX_3.getList().getModel()) {
                box = BOX_2;
                depth = 1;
            } else {
                throw new IllegalStateException("invalid source: " + source);
            }
            
            if(depth > -1) {
                triggered= true;
                updateBoxes(depth, box, false);
                triggered=false;
            }
        }
        
        /** Stubbed out method for ListDataListener */
        public void intervalRemoved(ListDataEvent e) { }

        /** Stubbed out method for ListDataListener */
        public void intervalAdded(ListDataEvent e) { }

        /**
         * If the box to be updated is box1 changes the models of box2 and box3,
         * otherwise, if box2 is changed, changes the model of box3. Also
         * handles the condition when the the "all" option is selected in any of
         * the boxes.
         */
        private void updateBoxes(int depth, FilterBox box, boolean clear) {

            box.updateTitle();
            
            // If box1 changed, we must change the model of box2 & box3.
            // If box2 changed, we only change the model of box3.
            Object sel1 = BOX_1.getSelectedValue();
            boolean all1 = sel1 == null || MODEL.isAll(sel1);
            
            if(box == BOX_1) {
                if(all1)
                    allPossible(BOX_2);
                else 
                    changeModel(BOX_1, BOX_2, sel1);
                if(clear)
                    BOX_2.clearSelection();
            }
            
            Object sel2 = BOX_2.getSelectedValue();
            boolean all2 = sel2 == null || MODEL.isAll(sel2);
            if(box == BOX_2 || box == BOX_1) {
                if(all2) {
                    if(all1)
                        allPossible(BOX_3);
                    else
                        changeModel(BOX_1, BOX_3, sel1);
                } else {
                    changeModel(BOX_2, BOX_3, sel2);
                }
                if(clear)
                    BOX_3.clearSelection();                
            }
        }
        
        /**
         * Changes the model of the box to be all possible selections for
         * this box.
         */
        private void allPossible(FilterBox box) {
            box.setModel(MODEL.getListModelMap(box.getSelector()));
        }

        /**
         * Changes the model of child to a cross section of the two boxes.
         */
        private void changeModel(FilterBox parent, FilterBox child,
                                 Object value) {
            child.setModel(
                MODEL.getCrossSection(
                    parent.getModel(),
                    value,
                    MODEL.getListModelMap(child.getSelector())
                )
            );
        }
        
        /**
         * Creates a new filter to give the ResultPanel.
         */
        private boolean selectionChanged(Selector selector,
                                      Object value,
                                      int depth) {
            TableLineFilter lineFilter = null;
    
            if(value == null || MODEL.isAll(value))
                lineFilter = AllowFilter.instance();
            else {                  
                switch(selector.getSelectorType()) {
                case Selector.SCHEMA:
                    lineFilter = new SchemaFilter((NamedMediaType)value);
                    break;
                case Selector.FIELD:
                    lineFilter = new FieldFilter(selector.getSchema(),
                                                 selector.getValue(),
                                                 (String)value);
                    break;
                case Selector.PROPERTY:
                    lineFilter = new PropertyFilter(selector.getValue(),
                                                    value);
                }
            }
            
            return RESULTS.filterChanged(lineFilter, depth);
        }
    }
}
