package users;

import java.io.Serializable;

import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
import java.util.Collections;
import java.util.Iterator;

import javax.swing.*;

import users.events.*;

import util.*;

/**
  * A user, having a name, a password for log-in purposes, and a set of capabilities.
  *
  * <p><code>User</code> objects are used to store all information associated with a user. As a
  * default users have a name, a password for log-in purposes, and a set of capabilities
  * that can be used to restrict the users usage of the application. Additional information
  * stored in subclasses of <code>User</code> could include statistics on application usage, bonus data
  * etc.</p>
  *
  * @see UserManager
  * @see Capability
  *
  * @author Steffen Zschaler
  * @version 2.0 05/05/1999
  * @since v2.0
  */
public class User extends Object implements Serializable {

  /**
    * The user's name. This is an immutable value and cannot be changed once the user
    * has been created.
    *
    * @serial
    */
  private final String m_sName;

  /**
    * The user's log-in password. This should normally be stored in garbled form as it
    * may be serialized and thus there is the potential risk of it being read by unauthorized
    * persons.
    *
    * @serial
    */
  private String m_sPassWd;

  /**
    * The user's capabilities.
    *
    * @see Capability
    *
    * @serial
    */
  private Map m_mpCapabilities = new HashMap();

  /**
    * The list of all listeners that showed an interest in this user.
    *
    * @serial See <a href="#util.ListenerHelper">ListenerHelper's serializable form</a> for more information on
    * what listeners get a chance to be serialized.
    */
  protected ListenerHelper m_lhListeners = new ListenerHelper();

  /**
    * Create a new User with a given name. The password will initially be the empty
    * string and there will be no capabilities.
    *
    * @param sName the new user's name.
    */
  public User (String sName) {

    super();

    m_sName = sName;
    m_sPassWd = "";
  }

  /**
    * Retrieve the name of this user.
    *
    * @return the name of the user.
    *
    * @override Never
    */
  public final String getName() {
    return m_sName;
  }

  /**
    * Check whether a given string is identical to the password of this user.
    *
    * <p>For security reasons there is no <code>getPassWd()</code> method. The only way to
    * check a user's password is this method. The string you pass as a parameter will be
    * compared to the user's password as it is stored, i.e. if the password is stored in
    * a garbled form (recommended) the string you pass as a parameter must also be in
    * garbled form.</p>
    *
    * @param sPassWd the string to be compared to the user's password. Must be in the
    * same form as the actual password, i.e. esp. it must be garbled if the actual password
    * is.
    *
    * @return true if the password and the string passed as a parameter are equal.
    *
    * @see #garblePassWD
    *
    * @override Never
    */
  public final boolean isPassWd (String sPassWd) {
    return m_sPassWd.equals (sPassWd);
  }

  /**
    * Set the password of this user.
    *
    * <p>The password is stored exactly as given, i.e. no garbling of any kind is performed.
    * It is strongly recommended, though, that you pass a garbled password, so that
    * passwords are not stored as plain text.</p>
    *
    * @param sPassWd the new password
    *
    * @see #garblePassWD
    * @see PassWDGarbler
    *
    * @override Never
    */
  public final void setPassWd (String sPassWd) {
    m_sPassWd = sPassWd;
  }

  /**
    * Check whether the given object equals this user.
    *
    * <p>Two users are considered equal if their names are equal.</p>
    *
    * @param o the object to be compared to.
    *
    * @return true if the given object equals this user.
    *
    * @override Sometimes Override this method if you need to implement a different notion of equality between
    * users.
    */
  public boolean equals (Object o) {
    if (o instanceof User) {
      return ((User) o).getName().equals (this.getName());
    }
    else
      return false;
  }

  /**
    * Return a String representation.
    *
    * @return the {@link #getName name} of the user.
    *
    * @override Sometimes
    */
  public String toString() {
    return getName();
  }

  /**
    * Set a range of the user's capabilities to new values.
    *
    * <p>Sets all capabilities from <code>mpCapabilities</code> to the new values.
    * This will fire <code>capabilitiesAdded</code> events, and <code>capabilitiesReplaced</code>
    * events if capabilities were changed.</p>
    *
    * <p><strong>Attention:</strong> A capability that has been set cannot be removed
    * again. Capabilities have two states (Granted and Not Granted). If you want to
    * remove a certain capability, set its state to Not Granted.</p>
    *
    * @param mpCapabilities the capabilities to be set. The keys of this map must be the
    * names of the capabilities to be set, whereas the corresponding values must be the
    * actual Capability objects.
    *
    * @see Capability
    * @see #setCapability
    * @see users.events.CapabilityDataListener
    *
    * @override Never
    */
  public synchronized void setCapabilities (Map mpCapabilities) {

    // distinguish added and replaced capabilities to make sure we fire
    // the correct events.
    Set stReplacements = new HashSet (mpCapabilities.keySet());
    stReplacements.retainAll (m_mpCapabilities.keySet());

    Set stAdded = new HashSet (mpCapabilities.keySet());
    stAdded.removeAll (m_mpCapabilities.keySet());

    // store the capabilities
    m_mpCapabilities.putAll (mpCapabilities);

    // fire the events
    fireCapabilitiesAdded (Collections.unmodifiableSet (stAdded));
    fireCapabilitiesReplaced (Collections.unmodifiableSet (stReplacements));
  }

  /**
    * Set one capability.
    *
    * <p><strong>Attention:</strong> A capability that has been set cannot be removed
    * again. Capabilities have two states (Granted and Not Granted). If you want to
    * remove a certain capability, set its state to Not Granted.</p>
    *
    * <p>This will fire a <code>capabilitiesAdded</code> or a <code>capabilitiesReplaced</code>
    * event.</p>
    *
    * @param cap the capability to be set.
    *
    * @return the previous value of the capability or <code>null</code> if none.
    *
    * @override Never
    */
  public synchronized Capability setCapability (Capability cap) {
    if (cap != null) {
      Capability capReturn = (Capability) m_mpCapabilities.remove (cap.getName());

      m_mpCapabilities.put (cap.getName(), cap);

      if (capReturn != null) {
        fireCapabilitiesReplaced (Collections.singleton (cap.getName()));
      }
      else {
        fireCapabilitiesAdded (Collections.singleton (cap.getName()));
      }

      return capReturn;
    }

    return null;
  }

  /**
    * Retrieve one of this user's capabilities.
    *
    * <p>Retrieves the capability of this user that is identified by <code>sCapName</code>.</p>
    *
    * @param sCapName the name of the capability to be returned.
    *
    * @return the capability associated with the given name or <code>null</code> if none.
    *
    * @see Capability
    *
    * @override Never
    */
  public synchronized Capability getCapability (String sCapName) {
    return (Capability) m_mpCapabilities.get (sCapName);
  }

  /**
    * Return a checkbox that can be used to visualize and change the value of a certain
    * capability of this user.
    *
    * <p>The checkbox will be backed by the capability, i.e. changes of the capability
    * will be directly reflected in the checkbox and vice-versa. There will be a
    * <code>NullPointerException</code> if the specified capability does not exist.</p>
    *
    * @param sCapName the name of the capability to be visualized by the checkbox.
    *
    * @return a checkbox that can be used to visualize and change the capability.
    *
    * @exception NullPointerException if Capability does not exist.
    *
    * @see javax.swing.JCheckBox
    * @see Capability
    * @see Capability#getDisplayName
    *
    * @override Never
    */
  public JCheckBox getCapabilityCheckBox (final String sCapName) {

    class CapabilityButtonModel extends JToggleButton.ToggleButtonModel implements CapabilityDataListener,
                                                                                   HelpableListener,
                                                                                   SerializableListener {

      {
        // replace listener list for special support
        listenerList = new ListenerHelper (this);

        updateModel();
      }

      private Capability capModelled;

      // ButtonModel interface methods
      public boolean isSelected() {
        ((ListenerHelper) listenerList).needModelUpdate();

        return capModelled.isGranted();
      }

      public void setSelected (boolean bSelect) {
        if (bSelect != capModelled.isGranted()) {
          setCapability (capModelled.getToggled());
        }
      }

      // CapabilityDataListener interface methods
      public void capabilitiesAdded (CapabilityDataEvent e) {}

      public void capabilitiesReplaced (CapabilityDataEvent e) {
        if (e.affectsCapability (capModelled.getName())) {
          capModelled = e.getCapability (capModelled.getName());

          fireStateChanged();
        }
      }

      // HelpableListener interface methods
      public void updateModel() {
        capModelled = getCapability (sCapName);
      }

      public void subscribe() {
        User.this.addCapabilityDataListener (CapabilityButtonModel.this);
        Debug.print ("CapabilityButtonModel.subscribe", -1);
      }

      public void unsubscribe() {
        User.this.removeCapabilityDataListener (CapabilityButtonModel.this);
        Debug.print ("CapabilityButtonModel.unsubscribe", -1);
      }
    }

    Capability cap = getCapability (sCapName);
    JCheckBox jcbReturn = new JCheckBox (cap.getDisplayName(), cap.isGranted());
    jcbReturn.setModel (new CapabilityButtonModel());

    return jcbReturn;
  }

  // Event handling
  /**
    * Add a CapabilityDataListener. CapabilityDataListeners receive events whenever a
    * user's capability list changed.
    *
    * @param cdl the CapabilityDataListener to add.
    *
    * @override Never
    */
  public void addCapabilityDataListener (CapabilityDataListener cdl) {
    m_lhListeners.add (CapabilityDataListener.class, cdl);
  }

  /**
    * Remove a CapabilityDataListener. CapabilityDataListeners receive events whenever a
    * user's capability list changed.
    *
    * @param cdl the CapabilityDataListener to remove.
    *
    * @override Never
    */
  public void removeCapabilityDataListener (CapabilityDataListener cdl) {
    m_lhListeners.remove (CapabilityDataListener.class, cdl);
  }

  /**
    * Fire a <code>capabilitiesAdded</code> event.
    *
    * @param stCapNames the set of capability names that where added.
    *
    * @see users.events.CapabilityDataListener#capabilitiesAdded
    *
    * @override Never
    */
  protected void fireCapabilitiesAdded (Set stCapNames) {
    CapabilityDataEvent cde = null;

    // Guaranteed to return a non-null array
    Object[] listeners = m_lhListeners.getListenerList();

    // Process the listeners last to first, notifying
    // those that are interested in this event
    for (int i = listeners.length-2; i>=0; i-=2) {
      if (listeners[i]==CapabilityDataListener.class) {
        // Lazily create the event:
        if (cde == null)
          cde = new CapabilityDataEvent (this, stCapNames);

        ((CapabilityDataListener) listeners[i+1]).capabilitiesAdded (cde);
      }
    }
  }

  /**
    * Fire a <code>capabilitiesReplaced</code> event.
    *
    * @param stCapNames the set of capability names that where replaced.
    *
    * @see users.events.CapabilityDataListener#capabilitiesReplaced
    *
    * @override Never
    */
  protected void fireCapabilitiesReplaced (Set stCapNames) {
    CapabilityDataEvent cde = null;

    // Guaranteed to return a non-null array
    Object[] listeners = m_lhListeners.getListenerList();

    // Process the listeners last to first, notifying
    // those that are interested in this event
    for (int i = listeners.length-2; i>=0; i-=2) {
      if (listeners[i]==CapabilityDataListener.class) {
        // Lazily create the event:
        if (cde == null)
          cde = new CapabilityDataEvent (this, stCapNames);

        ((CapabilityDataListener) listeners[i+1]).capabilitiesReplaced (cde);
      }
    }
  }

  /**
    * Method called by the UserManager when the user was associated with some object.
    *
    * @param oTo the object this user was associated with.
    *
    * @see UserManager
    *
    * @override Sometimes Override this method if you need to be informed when the user was logged on to some
    * object.
    */
  public void loggedOn (Object oTo) {}

  /**
    * Method called by the UserManager when the user was disassociated from some object.
    *
    * @param oFrom the object this user was disassociated from.
    *
    * @see UserManager
    *
    * @override Sometimes Override this method if you need to be informed when the user was logged off from
    * some object.
    */
  public void loggedOff (Object oFrom) {}

  ///////////////////////////////////////////////////////////////////////////////////////////////
  /// STATIC PART
  ///////////////////////////////////////////////////////////////////////////////////////////////

  /**
    * The default password garbler.
    *
    * <p>The default password garbling algorithm is very simple and should only be used if no real security
    * concerns are present. It will take the input String and perform a one-complement and add 7 for each byte
    * in the String.</p>
    */
  public static final PassWDGarbler DEFAULT_PASSWORD_GARBLER = new PassWDGarbler() {
    public String garblePassWD (String sPassWD) {
      byte[] baBytes = sPassWD.getBytes();

      byte bConverter = (byte) 0xFF;

      for (int i = 0; i < baBytes.length; i++) {
        baBytes[i] ^= bConverter;

        if (baBytes[i] <= Byte.MAX_VALUE - 7) {
          baBytes[i] += 7;
        }
        else {
          // wrap around
          baBytes[i] = (byte) ((Byte.MIN_VALUE + 7) - (Byte.MAX_VALUE - baBytes[i]));
        }

        if (baBytes[i] < 0) {
          if (baBytes[i] > Byte.MIN_VALUE) {
            baBytes[i] *= -1;
          }
          else {
            baBytes[i] = Byte.MAX_VALUE;
          }
        }
      }

      return new String (baBytes);
    }
  };

  /**
    * The global password garbler. It defaults to {@link #DEFAULT_PASSWORD_GARBLER}.
    */
  private static PassWDGarbler s_pwdgGlobal = DEFAULT_PASSWORD_GARBLER;

  /**
    * Set the global password garbler.
    *
    * <p>The global password garbler can be used as a central instance for garbling
    * your users' passwords. It defaults to {@link #DEFAULT_PASSWORD_GARBLER}.</p>
    *
    * @param pwdgNew the new global password garbler.
    *
    * @return the previous global password garbler.
    */
  public synchronized static PassWDGarbler setGlobalPassWDGarbler (PassWDGarbler pwdgNew) {
    PassWDGarbler pwdg = s_pwdgGlobal;

    s_pwdgGlobal = pwdgNew;

    return pwdg;
  }

  /**
    * Get the global password garbler.
    *
    * @return the global password garbler.
    */
  public synchronized static PassWDGarbler getGlobalPassWDGarbler() {
    return s_pwdgGlobal;
  }

  /**
    * Garble a password using the global password garbler, if any.
    *
    * <p>If no global password garbler is installed, the password
    * is returned unchanged. Otherwise the garbled password is returned.</p>
    *
    * @param sPassWD the password to garble
    *
    * @return the garbled password.
    */
  public synchronized static String garblePassWD (String sPassWD) {
    return ((s_pwdgGlobal == null)?(sPassWD):(s_pwdgGlobal.garblePassWD (sPassWD)));
  }
}