/* AbstractDocument.java --
   Copyright (C) 2002, 2004, 2005  Free Software Foundation, Inc.

This file is part of GNU Classpath.

GNU Classpath 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, or (at your option)
any later version.

GNU Classpath 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 GNU Classpath; see the file COPYING.  If not, write to the
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301 USA.

Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.

As a special exception, the copyright holders of this library give you
permission to link this library with independent modules to produce an
executable, regardless of the license terms of these independent
modules, and to copy and distribute the resulting executable under
terms of your choice, provided that you also meet, for each linked
independent module, the terms and conditions of the license of that
module.  An independent module is a module which is not derived from
or based on this library.  If you modify this library, you may extend
this exception to your version of the library, but you are not
obligated to do so.  If you do not wish to do so, delete this
exception statement from your version. */


package javax.swing.text;

import java.io.PrintStream;
import java.io.Serializable;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.Hashtable;
import java.util.Vector;

import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.EventListenerList;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.tree.TreeNode;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoableEdit;

public abstract class AbstractDocument
  implements Document, Serializable
{
  private static final long serialVersionUID = -116069779446114664L;
  
  protected static final String BAD_LOCATION = "document location failure";
  
  public static final String BidiElementName = "bidi level";
  public static final String ContentElementName = "content";
  public static final String ParagraphElementName = "paragraph";
  public static final String SectionElementName = "section";
  public static final String ElementNameAttribute = "$ename";

  Content content;
  AttributeContext context;
  DocumentFilter documentFilter;

  /** The documents properties. */
  Dictionary properties;

  protected EventListenerList listenerList = new EventListenerList();

  protected AbstractDocument(Content doc)
  {
    this(doc, StyleContext.getDefaultStyleContext());
  }

  protected AbstractDocument(Content doc, AttributeContext ctx)
  {
    content = doc;
    context = ctx;
  }

  // These still need to be implemented by a derived class:
  public abstract Element getParagraphElement(int pos);

  public abstract Element getDefaultRootElement();

  protected Element createBranchElement(Element parent,
					AttributeSet attributes)
  {
    return new BranchElement(parent, attributes);
  }

  protected Element createLeafElement(Element parent, AttributeSet attributes,
				      int start, int end)
  {
    return new LeafElement(parent, attributes, start, end);
  }

  public Position createPosition(final int offset) throws BadLocationException
  {
    if (offset < 0 || offset > getLength())
      throw new BadLocationException(getText(0, getLength()), offset);

    return new Position()
      {
	public int getOffset()
	{
	  return offset;
	}
      };
  }

  protected void fireChangedUpdate(DocumentEvent event)
  {
    DocumentListener[] listeners = getDocumentListeners();

    for (int index = 0; index < listeners.length; ++index)
      listeners[index].changedUpdate(event);
  }

  protected void fireInsertUpdate(DocumentEvent event)
  {
    DocumentListener[] listeners = getDocumentListeners();

    for (int index = 0; index < listeners.length; ++index)
      listeners[index].insertUpdate(event);
  }

  protected void fireRemoveUpdate(DocumentEvent event)
  {
    DocumentListener[] listeners = getDocumentListeners();

    for (int index = 0; index < listeners.length; ++index)
      listeners[index].removeUpdate(event);
  }

  protected void fireUndoableEditUpdate(UndoableEditEvent event)
  {
    UndoableEditListener[] listeners = getUndoableEditListeners();

    for (int index = 0; index < listeners.length; ++index)
      listeners[index].undoableEditHappened(event);
  }

  public int getAsynchronousLoadPriority()
  {
    return 0;
  }

  protected AttributeContext getAttributeContext()
  {
    return context;
  }

  public Element getBidiRootElement()
  {
    return null;
  }

  protected Content getContent()
  {
    return content;
  }

  protected Thread getCurrentWriter()
  {
    return null;
  }

  public Dictionary getDocumentProperties()
  {
    // FIXME: make me thread-safe
    if (properties == null)
      properties = new Hashtable();

    return properties;
  }

  public Position getEndPosition()
  {
    return new Position() 
      {        
        public int getOffset() 
        { 
          return getLength(); 
        } 
      };
  }

  public int getLength()
  {
    return content.length() - 1;
  }

  public EventListener[] getListeners(Class listenerType)
  {
    return listenerList.getListeners(listenerType);
  }

  public Object getProperty(Object key)
  {
    // FIXME: make me thread-safe
    Object value = null;
    if (properties != null)
      value = properties.get(key);

    return value;
  }

  public Element[] getRootElements()
  {
    Element[] elements = new Element[1];
    elements[0] = getDefaultRootElement();
    return elements;
  }

  public Position getStartPosition()
  {
    return new Position() 
      {        
        public int getOffset() 
        { 
          return 0; 
        } 
      };
  }

  public String getText(int offset, int length) throws BadLocationException
  {
    return content.getString(offset, length);
  }

  public void getText(int offset, int length, Segment segment)
    throws BadLocationException
  {
    content.getChars(offset, length, segment);
  }

  public void insertString(int offset, String text, AttributeSet attributes)
    throws BadLocationException
  {
    // Just return when no text to insert was given.
    if (text == null || text.length() == 0)
      return;
    
    DefaultDocumentEvent event =
      new DefaultDocumentEvent(offset, text.length(),
			       DocumentEvent.EventType.INSERT);
    content.insertString(offset, text);
    insertUpdate(event, attributes);
    fireInsertUpdate(event);
  }

  protected void insertUpdate(DefaultDocumentEvent chng, AttributeSet attr)
  {
  }

  protected void postRemoveUpdate(DefaultDocumentEvent chng)
  {
  }

  public void putProperty(Object key, Object value)
  {
    // FIXME: make me thread-safe
    if (properties == null)
      properties = new Hashtable();

    properties.put(key, value);
  }

  public void readLock()
  {
  }

  public void readUnlock()
  {
  }

  public void remove(int offset, int length) throws BadLocationException
  {
    DefaultDocumentEvent event =
      new DefaultDocumentEvent(offset, length,
			       DocumentEvent.EventType.REMOVE);
    removeUpdate(event);
    content.remove(offset, length);
    postRemoveUpdate(event);
    fireRemoveUpdate(event);
  }

  /**
   * Replaces some text in the document.
   *
   * @since 1.4
   */
  public void replace(int offset, int length, String text,
		      AttributeSet attributes)
    throws BadLocationException
  {
    remove(offset, length);
    insertString(offset, text, attributes);
  }

  /**
   * Adds a <code>DocumentListener</code> object to this document.
   *
   * @param listener the listener to add
   */
  public void addDocumentListener(DocumentListener listener)
  {
    listenerList.add(DocumentListener.class, listener);
  }

  /**
   * Removes a <code>DocumentListener</code> object from this document.
   *
   * @param listener the listener to remove
   */
  public void removeDocumentListener(DocumentListener listener)
  {
    listenerList.remove(DocumentListener.class, listener);
  }

  /**
   * Returns add added <code>DocumentListener</code> objects.
   *
   * @return an array of listeners
   */
  public DocumentListener[] getDocumentListeners()
  {
    return (DocumentListener[]) getListeners(DocumentListener.class);
  }

  /**
   * Adds a <code>UndoableEditListener</code> object to this document.
   *
   * @param listener the listener to add
   */
  public void addUndoableEditListener(UndoableEditListener listener)
  {
    listenerList.add(UndoableEditListener.class, listener);
  }

  /**
   * Removes a <code>UndoableEditListener</code> object from this document.
   *
   * @param listener the listener to remove
   */
  public void removeUndoableEditListener(UndoableEditListener listener)
  {
    listenerList.remove(UndoableEditListener.class, listener);
  }

  /**
   * Returns add added <code>UndoableEditListener</code> objects.
   *
   * @return an array of listeners
   */
  public UndoableEditListener[] getUndoableEditListeners()
  {
    return (UndoableEditListener[]) getListeners(UndoableEditListener.class);
  }

  protected void removeUpdate(DefaultDocumentEvent chng)
  {
  }

  public void render(Runnable r)
  {
  }

  public void setAsynchronousLoadPriority(int p)
  {
  }

  public void setDocumentProperties(Dictionary x)
  {
    // FIXME: make me thread-safe
    properties = x;
  }

  protected void writeLock()
  {
  }

  protected void writeUnlock()
  {
  }

  /**
   * @since 1.4
   */
  public DocumentFilter getDocumentFilter()
  {
    return documentFilter;
  }

  /**
   * @since 1.4
   */
  public void setDocumentFilter(DocumentFilter filter)
  {
    this.documentFilter = filter;
  }

  public void dump(PrintStream out)
  {
    ((AbstractElement) getDefaultRootElement()).dump(out, 0);
  }

  public interface AttributeContext
  {
    AttributeSet addAttribute(AttributeSet old, Object name, Object value);

    AttributeSet addAttributes(AttributeSet old, AttributeSet attributes);

    AttributeSet getEmptySet();

    void reclaim(AttributeSet attributes);

    AttributeSet removeAttribute(AttributeSet old, Object name);

    AttributeSet removeAttributes(AttributeSet old, AttributeSet attributes);

    AttributeSet removeAttributes(AttributeSet old, Enumeration names);
  }

  public interface Content
  {
    Position createPosition(int offset) throws BadLocationException;

    int length();

    UndoableEdit insertString(int where, String str)
      throws BadLocationException;

    UndoableEdit remove(int where, int nitems) throws BadLocationException;

    String getString(int where, int len) throws BadLocationException;

    void getChars(int where, int len, Segment txt) throws BadLocationException;
  }

  public abstract class AbstractElement
    implements Element, MutableAttributeSet, TreeNode, Serializable
  {
    private static final long serialVersionUID = 1265312733007397733L;
    int count;
    int offset;

    AttributeSet attributes;

    Element element_parent;

    TreeNode tree_parent;
    Vector tree_children;

    public AbstractElement(Element p, AttributeSet s)
    {
      element_parent = p;
      attributes = s;
    }

    // TreeNode implementation

    public abstract Enumeration children();
      
    public abstract boolean getAllowsChildren();
      
    public TreeNode getChildAt(int index)
    {
      return (TreeNode) tree_children.get(index);
    }
      
    public int getChildCount()
    {
      return tree_children.size();
    }
      
    public int getIndex(TreeNode node)
    {
      return tree_children.indexOf(node);
    }

    public TreeNode getParent()
    {
      return tree_parent;
    }

    public abstract boolean isLeaf();


    // MutableAttributeSet support

    public void addAttribute(Object name, Object value)
    {
      attributes = getAttributeContext().addAttribute(attributes, name, value);
    }

    public void addAttributes(AttributeSet attrs)
    {
      attributes = getAttributeContext().addAttributes(attributes, attrs);
    }

    public void removeAttribute(Object name)
    {
      attributes = getAttributeContext().removeAttribute(attributes, name);
    }

    public void removeAttributes(AttributeSet attrs)
    {
      attributes = getAttributeContext().removeAttributes(attributes, attrs);
    }

    public void removeAttributes(Enumeration names)
    {
      attributes = getAttributeContext().removeAttributes(attributes, names);
    }

    public void setResolveParent(AttributeSet parent)
    {
      attributes = getAttributeContext().addAttribute(attributes, ResolveAttribute, parent);
    }


    // AttributeSet interface support

    public boolean containsAttribute(Object name, Object value)
    {
      return attributes.containsAttribute(name, value);
    }

    public boolean containsAttributes(AttributeSet attrs)
    {
      return attributes.containsAttributes(attrs);
    }

    public AttributeSet copyAttributes()
    {
      return attributes.copyAttributes();
    }

    public Object getAttribute(Object key)
    {
      return attributes.getAttribute(key);
    }

    public int getAttributeCount()
    {
      return attributes.getAttributeCount();
    }
      
    public Enumeration getAttributeNames()
    {
      return attributes.getAttributeNames();
    }
      
    public AttributeSet getResolveParent()
    {
      return attributes.getResolveParent();
    }

    public boolean isDefined(Object attrName)
    {
      return attributes.isDefined(attrName);
    }
      
    public boolean isEqual(AttributeSet attrs) 
    {
      return attributes.isEqual(attrs);
    }

    // Element interface support

    public AttributeSet getAttributes()
    {
      return attributes;
    }

    public Document getDocument()
    {
      return AbstractDocument.this;
    }
      
    public abstract Element getElement(int index);
      
    public String getName()
    {
      return (String) getAttribute(NameAttribute);
    }
      
    public Element getParentElement()
    {
      return element_parent;
    }
      
    public abstract int getEndOffset();
      
    public abstract int getElementCount();
      
    public abstract int getElementIndex(int offset);
      
    public abstract int getStartOffset();

    private void dumpElement(PrintStream stream, String indent, Element element)
    {
      System.out.println(indent + "<" + element.getName() +">");
      
      if (element.isLeaf())
	{
	  int start = element.getStartOffset();
	  int end = element.getEndOffset();
	  String text = "";
	  try
	    {
	      text = getContent().getString(start, end - start);
	    }
	  catch (BadLocationException e)
	    {
	    }
	  System.out.println(indent + "  ["
			     + start + ","
			     + end + "]["
			     + text + "]");
	}
      else
	{
	  for (int i = 0; i < element.getElementCount(); ++i)
	    dumpElement(stream, indent + "  ", element.getElement(i));
	}
    }
    
    public void dump(PrintStream stream, int indent)
    {
      String indentStr = "";
      for (int i = 0; i < indent; ++i)
	indentStr += "  ";
      dumpElement(stream, indentStr, this);
    }
  }

  public class BranchElement extends AbstractElement
  {
    private static final long serialVersionUID = -8595176318868717313L;
    
    private Element[] children = new Element[0];

    public BranchElement(Element parent, AttributeSet attributes)
    {
      super(parent, attributes);
    }

    public Enumeration children()
    {
      if (children.length == 0)
        return null;

      Vector tmp = new Vector();

      for (int index = 0; index < children.length; ++index)
	tmp.add(children[index]);
      
      return tmp.elements();
    }

    public boolean getAllowsChildren()
    {
      return true;
    }

    public Element getElement(int index)
    {
      if (index < 0 || index >= children.length)
	return null;

      return children[index];
    }

    public int getElementCount()
    {
      return children.length;
    }

    public int getElementIndex(int offset)
    {
      // XXX: There is surely a better algorithm
      // as beginning from first element each time.
      for (int index = 0; index < children.length; ++index)
        {
	  Element elem = children[index];

	  if ((elem.getStartOffset() <= offset)
	      && (offset < elem.getEndOffset()))
	    return index;
        }

      return 0;
    }

    public int getEndOffset()
    {
      return children[children.length - 1].getEndOffset();
    }

    public String getName()
    {
      return ParagraphElementName;
    }

    public int getStartOffset()
    {
      return children[0].getStartOffset();
    }

    public boolean isLeaf()
    {
      return false;
    }

    public Element positionToElement(int position)
    {
      // XXX: There is surely a better algorithm
      // as beginning from first element each time.
      for (int index = 0; index < children.length; ++index)
        {
	  Element elem = children[index];

	  if ((elem.getStartOffset() <= position)
	      && (position < elem.getEndOffset()))
	    return elem;
        }

      return null;
    }

    public void replace(int offset, int length, Element[] elements)
    {
      Element[] target = new Element[children.length - length
				     + elements.length];
      System.arraycopy(children, 0, target, 0, offset);
      System.arraycopy(elements, 0, target, offset, elements.length);
      System.arraycopy(children, offset + length, target,
		       offset + elements.length,
		       children.length - offset - length);
      children = target;
    }

    public String toString()
    {
      return ("BranchElement(" + getName() + ") "
	      + getStartOffset() + "," + getEndOffset() + "\n");
    }
  }

  public class DefaultDocumentEvent extends CompoundEdit
    implements DocumentEvent
  {
    private static final long serialVersionUID = -7406103236022413522L;
    
    private int offset;
    private int length;
    private DocumentEvent.EventType type;

    public DefaultDocumentEvent(int offset, int length,
				DocumentEvent.EventType type)
    {
      this.offset = offset;
      this.length = length;
      this.type = type;
    }

    public Document getDocument()
    {
      return AbstractDocument.this;
    }

    public int getLength()
    {
      return length;
    }

    public int getOffset()
    {
      return offset;
    }

    public DocumentEvent.EventType getType()
    {
      return type;
    }

    public DocumentEvent.ElementChange getChange(Element elem)
    {
      return null;
    }
  }

  public static class ElementEdit extends AbstractUndoableEdit
    implements DocumentEvent.ElementChange
  {
    private static final long serialVersionUID = -1216620962142928304L;

    private Element elem;
    private int index;
    private Element[] removed;
    private Element[] added;
    
    public ElementEdit(Element elem, int index,
		       Element[] removed, Element[] added)
    {
      this.elem = elem;
      this.index = index;
      this.removed = removed;
      this.added = added;
    }

    public Element[] getChildrenAdded()
    {
      return added;
    }
    
    public Element[] getChildrenRemoved()
    {
      return removed;
    }

    public Element getElement()
    {
      return elem;
    }

    public int getIndex()
    {
      return index;
    }
  }

  public class LeafElement extends AbstractElement
  {
    private static final long serialVersionUID = 5115368706941283802L;
    int start;
    int end;

    public LeafElement(Element parent, AttributeSet attributes, int start,
                       int end)
    {
      super(parent, attributes);
      this.start = start;
      this.end = end;
    }

    public Enumeration children()
    {
      return null;
    }

    public boolean getAllowsChildren()
    {
      return false;
    }

    public Element getElement(int index)
    {
      return null;
    }

    public int getElementCount()
    {
      return 0;
    }

    public int getElementIndex(int offset)
    {
      return -1;
    }

    public int getEndOffset()
    {
      return end;
    }

    public String getName()
    {
      return ContentElementName;
    }

    public int getStartOffset()
    {
      return start;
    }

    public boolean isLeaf()
    {
      return true;
    }

    public String toString()
    {
      return ("LeafElement(" + getName() + ") "
	      + getStartOffset() + "," + getEndOffset() + "\n");
    }
  }
}
