/**
 *#########################################################################
 *
 * 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.util.StaticStrings;
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);
    }

    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)
    {
	Object object = cache.get(new Integer(index));
	if (object != null) {
	    return object;
	}

	// Retrieve the required element
	Element element = (Element) children.item(index);
	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(new Integer(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));
    }
}
