/* Copyright (C) 1999-2002 by Peter Eastman

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

package artofillusion;

import java.util.*;

/** A ModelEvent informs about changes in the data of the application.
 * The usage differs from the normal java event handling:
 *
 * Each object which wants to send model events holds a public
 * ModelEvent object. To fire an event, it uses one of the
 * send() methods.
 *
 * TODO: Rewrite to use WeakReferences
 *
 * @author Michael Butscher
 */

public class ModelEvent implements Cloneable
{
  protected Vector listeners = new Vector();
  protected Object source = null;
  protected Object cause = null;
  protected Hashtable properties = null;
  protected ModelEvent parent = null;
//  protected String [] keys = null;
//  protected Object [] values = null;

  /** A Hashtable can't handle null as a value, so internally we use this constant.
    When a key has NULL as associated value, #getValue will return null instead. */
  public static final Object NULL = new Object();

  public static final String CAUSE_UI = "user interface";
  public static final String CAUSE_PROGRAM = "programmatic";

  /** Change occurred, no further information given.
      Needed for "happened during" property in a "... changed" event. */
  public static final String CHANGEDUR_DONTKNOW = "dont know";

  /** Change occurred when "OK" was pressed in an object editor.
      Needed for "happened during" property in a "... changed" event. */
  public static final String CHANGEDUR_OBJECTEDITORCOMMIT = "object editor commit";

  /** Change occurred when "Cancel" was pressed in an object editor.
      Needed for "happened during" property in a "... changed" event. */
  public static final String CHANGEDUR_OBJECTEDITORROLLBACK = "object editor rollback";

  /** Change occurred during a drag operation in an object editor.
      Needed for "happened during" property in a "... changed" event. */
  public static final String CHANGEDUR_OBJECTEDITORDRAG = "object editor drag";

  /** Change occurred after or without a drag operation in an object editor.
      Needed for "happened during" property in a "... changed" event. */
  public static final String CHANGEDUR_OBJECTEDITOR = "object editor";

  /** Change occurred because animation time was changed.
      Needed for "happened during" property in a "... changed" event. */
  public static final String CHANGEDUR_TIMECHANGE = "time change";


  /** Creates a new instance of ModelEvent */
  public ModelEvent()
  {
  }

  public Object getCause()
  {
    return cause;
  }

  public Object getSource()
  {
    return source;
  }

//  public String[] getKeys()
//  {
//    return keys;
//  }
//
//  public Object[] getValues()
//  {
//    return values;
//  }
//
//  /** Get first key (for convenience). */
//  public String getKey()
//  {
//    return keys[0];
//  }
//
//  /** Get first value (for convenience). */
//  public Object getValue()
//  {
//    return values[0];
//  }
//
//  /** Return index of key in key list or -1 if not found. */
//  public int findKey(String key)
//  {
//    for(int i = 0; i < keys.length; ++i)
//    {
//      if (keys[i].equals(key))
//        return i;
//    }
//
//    return -1;
//  }

  /** Return value for specified key or null if not found.
    Be careful: The value itself may be null. */
  public Object getValue(String key)
  {
    Object result = properties.get(key);
    if (result == NULL)
      result = null;

    return result;
  }

  /** Has the event the specified key? */
  public boolean hasKey(String key)
  {
    return properties.containsKey(key);
  }

  /** The ModelEvent which was called to fire this clone. If it returns null this is not a clone.*/
  public ModelEvent getParent()
  {
    return parent;
  }

  public Object clone()
  {
    ModelEvent result;
    try
    {
      result = (ModelEvent)super.clone();
    }
    catch (CloneNotSupportedException e)
    {
      e.printStackTrace();
      return null;
    }

    result.listeners = (Vector)listeners.clone();
    if (properties != null)
      result.properties = (Hashtable) properties.clone();

    return result;
  }


  /** A ModelEvent manages the listener list itself. */
  public void addListener(ModelListener listener)
  {
    if (!listeners.contains(listener))
      listeners.add(listener);
  }

  public void removeListener(ModelListener listener)
  {
    listeners.remove(listener);
  }


  /** Add a key-value pair to the internal Hashtable.
    <B>Can't be called on an original ModelEvent, must be a clone.</B>

    @return  this, so you can chain the call: event = event.put("a", a).put("foo", bar);

    @throws NullPointerException       if key is null
    @throws IllegalArgumentException   if this is not a clone  */

  public ModelEvent put(String key, Object value)
  {
    if (getParent() == null)
      throw new IllegalArgumentException();  // This is not a clone   // TODO(MB) Create/Find a better exception
    if (value == null)
      value = NULL;     // Hashtable can't handle null

    properties.put(key, value);

    return this;
  }
  
  /** Remove a key from the internal property-table.
      <B>Can't be called on an original ModelEvent, must be a clone.</B>
   
      @return  this, so you can chain the call: event = event.put("a", a).put("foo", bar);

  */
  
  public ModelEvent remove(String key)
  {
     if (getParent() == null)
       throw new IllegalArgumentException();  // This is not a clone   // TODO(MB) Create/Find a better exception
     properties.remove(key);
     
     return this;
  }
  
  /** Same as put(key, null). */
  public ModelEvent put(String key)
  {
    return put(key, null);
  }

  /** Called on the clone to dispatch itself to first, then to all listeners.
    <B>Can't be called on an original ModelEvent, must be a clone.</B>

    @param first   the first listener the event dispatches before dispatching to remaining listeners. A null value is ignored.
    @throws IllegalArgumentException   if this is not a clone  */

  public void processSend(ModelListener first)
  {
    if (getParent() == null)
      throw new IllegalArgumentException();  // This is not a clone   // TODO(MB) Create/Find a better exception
    if (first != null)
      first.modelEventHappened(this);

    Object[] l=listeners.toArray();
    for (int i=0; i<l.length; ++i)
      ((ModelListener)l[i]).modelEventHappened(this);
  }

  /** Called on the clone to dispatch itself to all listeners.
    <B>Can't be called on an original ModelEvent, must be a clone.</B>

    @throws IllegalArgumentException  if this is not a clone  */

  public void processSend()
  {
    processSend(null);
  }

  /** Creates a clone with the appropriate data, so dispatching can be done later.<BR>
      Some methods can be called only on a cloned ModelEvent. To add properties, use the #put method.
      The parent of the cloned event is this event.

      @param _source   The object which will dispatch the event
      @param _cause    The cause (caller) of the event.  */

  public ModelEvent createClone(Object _source, Object _cause)
  {
    ModelEvent event=(ModelEvent)clone();
    if (event.properties == null)
      event.properties = new Hashtable();

    event.source=_source;
    event.cause=_cause;
    event.parent=this;

    return event;
  }

  /** Creates a clone with the appropriate data, so dispatching can be done later.<BR>
      Some methods can be called only on a cloned ModelEvent.
      The parent of the cloned event is this event.

    @param _source   The object which will dispatch the event
    @param _keys     Array of keys
    @param _values   Array of values for the keys. Its length must not be less than length of _keys array.
    @param _cause    The cause (caller) of the event.
  */
  public ModelEvent createClone(Object _source, String[] _keys, Object[] _values, Object _cause)
  {
    ModelEvent event=createClone(_source, _cause);

    for(int i = 0; i < _keys.length; ++i)
      event.put(_keys[i], _values[i]);

    return event;
  }

  public ModelEvent createClone(Object _source, String[] _keys, Object _cause)
  {
    Object [] values = new Object [_keys.length];
    return createClone(_source, _keys, values, _cause);
  }

  public ModelEvent createClone(Object _source, String _key, Object _value, Object _cause)
  {
    String [] keys = new String [] {_key};
    Object [] values = new Object [] {_value};
    return createClone(_source, keys, values, _cause);
  }

  public ModelEvent createClone(Object _source, String _key, Object _cause)
  {
    String [] keys = new String [] {_key};
    Object [] values = new Object [1];
    return createClone(_source, keys, values, _cause);
  }


  /** A ModelEvent sends a clone of itself to the listeners. */
  public void send(Object _source, String[] _keys, Object[] _values, Object _cause)
  {
    createClone(_source, _keys, _values, _cause).processSend();
  }

  public void send(Object _source, String[] _keys, Object _cause)
  {
    createClone(_source, _keys, _cause).processSend();
  }

  public void send(Object _source, String _key, Object _value, Object _cause)
  {
    createClone(_source, _key, _value, _cause).processSend();
  }

  public void send(Object _source, String _key, Object _cause)
  {
    createClone(_source, _key, _cause).processSend();
  }


}
