/**
 *#########################################################################
 *
 * A component of the Gatherer application, part of the Greenstone digital
 * library suite from the New Zealand Digital Library Project at the
 * University of Waikato, New Zealand.
 *
 * Author: John Thompson, Greenstone Digital Library, University of Waikato
 *
 * Copyright (C) 1999 New Zealand Digital Library Project
 *
 * This program 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 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *########################################################################
 */
package org.greenstone.gatherer.cdm;

import java.util.*;
import javax.swing.*;
import org.greenstone.gatherer.DebugStream;
import org.greenstone.gatherer.Gatherer;
import org.greenstone.gatherer.util.StaticStrings;
import org.greenstone.gatherer.util.XMLTools;
import org.w3c.dom.*;

/** This class provides ListModel like access to a list of nodes within a DOM model.
 * @author John Thompson, Greenstone Digital Library, University of Waikato
 * @version 2.3d
 */
public class DOMProxyListModel
extends AbstractListModel {
    protected Element root;
    private DOMProxyListEntry class_type;
    private HashMap cache = new HashMap ();
    private NodeList children = null;
    private String tag_name;
    
    /** Constructor.
     * @param root the Element at the root of the subtree to be searched for appropriate child elements
     * @param tag_name the name of appropriate elements as a String
     * @param class_type the type of object to wrap the elements returned in, as a DOMProxyListEntry
     */
    public DOMProxyListModel (Element root, String tag_name, DOMProxyListEntry class_type) {
        this.class_type = class_type;
        this.root = root;
        this.tag_name = tag_name;
        this.children = this.root.getElementsByTagName (this.tag_name);
    }
    
    /** Used to add an element into the underlying dom, and fire the appropriate repaint events. This version always adds the new element at the very head of the DOM. */
    public synchronized void add (DOMProxyListEntry entry) {
        Element element = entry.getElement ();
        if(root.hasChildNodes ()) {
            Node sibling = root.getFirstChild ();
            root.insertBefore (element, sibling);
            sibling = null;
        }
        else {
            root.appendChild (element);
        }
        element = null;
        // Regardless fire update event
        cache.clear ();
        fireIntervalAdded (this, 0, 0);
    }
    
    /** Used to add an element into the underlying dom, and fire the appropriate repaint events.
     * @param index the index where the element should be inserted (relative of the other elements in this proxy list)
     * @param entry the <strong>DOMProxyListEntry</strong> to be inserted
     */
    public synchronized void add (int index, DOMProxyListEntry entry) {
        ///atherer.println("Add entry at " + index + " where size = " + getSize());
        Element element = entry.getElement ();
        // retrieve the node where we want to insert
        if(index < children.getLength ()) {
            Node sibling = children.item (index);
            // Find the parent node
            Node parent_node = sibling.getParentNode ();
            parent_node.insertBefore (element, sibling);
            sibling = null;
        }
        // If the index is too large, we are adding to the end of our list of entries. However you have to remember that this list is only a viewport on the entire DOM so there might be entries following this group that we actually want to insert before (not append at the very end!)
        else {
            // Retrieve the currently last entry
            index = children.getLength () - 1;
            Node sibling = null;
            Node parent_node = null;
            if(index >= 0) {
                sibling = children.item (index);
                parent_node = sibling.getParentNode ();
                sibling = sibling.getNextSibling ();
            }
            if(sibling != null && parent_node != null) {
                parent_node.insertBefore (element, sibling);
            }
            // Add to the root node
            else {
                index = 0;
                root.appendChild (element);
            }
        }
        // Regardless fire update event
        cache.clear ();
        fireIntervalAdded (this, index, index);
    }
    
    /** Used to add an element into the underlying dom, and fire the appropriate repaint events. This version inserts the new entry immediately -after- the given entry in the DOM.
     * @param entry the DOMProxyListEntry to be inserted
     * @param preceeding_entry the DOMProxyListEntry immediately before where we want the new entry
     */
    public synchronized void addAfter (DOMProxyListEntry entry, DOMProxyListEntry preceeding_entry) {
        Element element = entry.getElement ();
        Element preceeding_sibling = preceeding_entry.getElement ();
        Node parent_node = preceeding_sibling.getParentNode ();
        Node following_sibling = preceeding_sibling.getNextSibling ();
        if(following_sibling != null) {
            parent_node.insertBefore (element, following_sibling);
        }
        else {
            parent_node.appendChild (element);
        }
        // Regardless fire update event
        cache.clear ();
        int index = indexOf (entry);
        fireIntervalAdded (this, index, index);
    }
    
    /** Used to add an element into the underlying dom, and fire the appropriate repaint events. This version inserts the new entry immediately -before- the given entry in the DOM.
     * @param entry the DOMProxyListEntry to be inserted
     * @param following_entry the DOMProxyListEntry immediately after where we want the new entry
     */
    public synchronized void addBefore (DOMProxyListEntry entry, DOMProxyListEntry following_entry) {
        Element element = entry.getElement ();
        Element following_sibling = following_entry.getElement ();
        Node parent_node = following_sibling.getParentNode ();
        parent_node.insertBefore (element, following_sibling);
        // Regardless fire update event
        cache.clear ();
        int index = indexOf (entry);	
	fireIntervalAdded (this, index, index);		     
    }

    // Had to override this to ensure AbstractListModel.fireIntervalAdded() is run on EDT
    // Conferred with Dr Bainbridge that the synchronized keyword to the method here is
    // not a problem. It may at worst be overkill, as already synchronized methods call this.
    public synchronized void fireIntervalAdded(Object source, int index0, int index1) {
	Gatherer.invokeInEDT_replacesProceedInCurrThread(
		 "DOMProxyListModel - overridden inherited fireIntervalAdded()",
		 Gatherer.ASYNC,
		 new Runnable() {
		     public void run() {
			 // https://stackoverflow.com/questions/2686181/how-to-access-a-superclass-method-from-a-nested-class
			 DOMProxyListModel.super.fireIntervalAdded (source, index0, index1);
		     }
		 });
    }
    
    public synchronized void add (Node parent, DOMProxyListEntry entry, Node sibling) {
        Element child = entry.getElement ();
        if(sibling != null) {
            parent.insertBefore (child, sibling);
        }
        else {
            parent.appendChild (child);
        }
        cache.clear ();
        int index = indexOf (entry);
        
        
        fireIntervalAdded (this, index, index);
    }
    
    /** Used to add an element into the underlying dom, and fire the appropriate repaint events. This version always adds the new element at the end of the children. */
    public synchronized void append (DOMProxyListEntry entry) {
        Element element = entry.getElement ();
        root.appendChild (element);
        element = null;
        // Regardless fire update event
        cache.clear ();
        fireIntervalAdded (this, 0, 0);
    }
    
    public synchronized ArrayList children () {
        ArrayList child_list = new ArrayList ();
        int child_count = children.getLength ();
        for(int i = 0; i < child_count; i++) {
            child_list.add (getElementAt (i));
        }
        return child_list;
    }
    
    public synchronized boolean contains (Object entry) {
        boolean found = false;
        int size = getSize ();
        
        for(int i = 0; !found && i < size; i++) {
            DOMProxyListEntry sibling = (DOMProxyListEntry) getElementAt (i);
            if(sibling.equals (entry)) {
                
                found = true;
            }
        }
        return found;
    }
    
    public synchronized Object getElementAt (int index) {
        /** There are times when the length of the 'cache' is not as same as the length of the 'children', etc. not up to date. 
            eg, when a classifier has been deleted. So we rather not have this efficiency for Format4gs3.java.*/       
        if (class_type instanceof Format4gs3) {
            Element element = (Element) children.item (index);
            // Now wrap it in the object of the users choice
            Object object = class_type.create (element);
            return object;
        }
        
        Object object = cache.get (Integer.valueOf(index));
        if (object != null) {
            return object;
        }
        
        // Retrieve the required element
        Element element = (Element) children.item (index);
	if (element == null) {
	  return null;
	}
        //DebugStream.println ("Element at index " + index + " not in cache: " + element);
        
        // Now wrap it in the object of the users choice
        object = class_type.create (element);
        cache.put (Integer.valueOf(index), object);
        return object;
    }
    
    public synchronized int indexOf (DOMProxyListEntry entry) {
        Element element = entry.getElement ();
        int children_length = children.getLength ();
        for(int i = 0; i < children_length; i++) {
            Node node = children.item (i);
            if(element == node) {
                return i;
            }
        }
        return -1;
    }
    
    public synchronized int getSize () {
        
        if(children == null) {
            children = root.getElementsByTagName (tag_name);
        }
        return children.getLength ();
    }
    
    public synchronized void refresh () {
        fireContentsChanged (this, 0, getSize ());
    }
    
    public synchronized void refresh (DOMProxyListEntry entry) {
        int index = indexOf (entry);
        fireContentsChanged (this, index, index);
    }
    
    public synchronized void remove (DOMProxyListEntry entry) {
        remove (indexOf (entry));
    }
    
    public synchronized void remove (int index) {
        // Retrieve the required element
        Node node = children.item (index);
        // Find its parent
        Node parent_node = node.getParentNode ();
        // Now remove it
        parent_node.removeChild (node);
        // Refresh model
        cache.clear ();
            
        fireIntervalRemoved (this, index, index);
    }
    
    
    /** Changes the 'root' element that this list sources its information from.
     * @param  root the new root Element
     */
    public synchronized void setRoot (Element root) {
        this.children = null;
        cache.clear ();
        this.root = root;
        this.children = this.root.getElementsByTagName (this.tag_name);
        fireContentsChanged (this, 0, getSize ());
    }
    
    public synchronized void setAssigned (boolean assigned) {
        if (assigned) {
            root.setAttribute (StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.TRUE_STR);
        }
        else {
            root.setAttribute (StaticStrings.ASSIGNED_ATTRIBUTE, StaticStrings.FALSE_STR);
        }
    }
    
    public boolean isAssigned () {
        return (root.getAttribute (StaticStrings.ASSIGNED_ATTRIBUTE).equals ("") || root.getAttribute (StaticStrings.ASSIGNED_ATTRIBUTE).equals (StaticStrings.TRUE_STR));
    }

    public void setClassType(DOMProxyListEntry class_type) {
	this.class_type = class_type;
    }

    public DOMProxyListEntry getClassType() {
	return this.class_type;
    }
}
