package nxm.sys.lib;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

import nxm.sys.inc.MessageHandler;

/** A class to define the Midas registry. <br>
    <br>
    This is a single point of reference for a given instance of the JDK where named references
    to all windows and commands are stored. It allows commands to communicate with other commands
    by name, and for graphical primitives to re-parent their panels by name. <br>
    <br>
    This is particularly useful in the Browser context.

    @author Jeff Schoen
 */
public class Registry extends Object {

  /** The global commands/windows table.
      This field is now package private in 3.1.1, it was deprecated in 2.7.1
   */
  static Table stable = new Table();

  /** The global handlers table. */
  public static Table handlers = new Table();

  /** The local threads table */
  private final Map<Thread,Command> threads
    = Collections.synchronizedMap(new LinkedHashMap<Thread,Command>());

  /** The local commands/windows table. */
  public Table table = stable;

  /** Initialize the registry */
  public Registry () {
    super();
  }

  /** Put entry into the registry.
      @param name  The name to use.
      @param entry The entry to add.
   */
  public void put (String name, Object entry) {
    table.put(name,entry);
    if (entry instanceof Command) {
      Command cmd = (Command)entry;
      Thread thread = cmd.thread;
      if (thread!=null) threads.put(thread,cmd);
    }
  }

  /** Put entry with one-up instance counter to avoid collisions. <b>This method should only be
      called from {@link Command}</b>.
      @param name  The preferred name to use.
      @param entry The entry to add.
      @return The name actually used.
   */
  public synchronized String putInstance (String name, Object entry) {
    String instanceName = name;
    for (int i=0; get(instanceName)!=null; i++) instanceName = name+"_"+i;
    put(instanceName,entry);
    return instanceName;
  }

  /** Get a registry entry.
      @param name The name of the entry.
      @return The registry entry or null if not found.
   */
  public Object get (String name) {
    return get(table, name, 0);
  }

  /** Get a registry entry with timeout.
      @param name The name of the entry.
      @param wait The maximum time (seconds) to wait for the entry to show up if it is not already
                  there (zero for no wait). If set to -1, this method will throw an exception if
                  there is no matching registry entry when called.
      @return The registry entry or null if not found.
   */
  public Object get (String name, int wait) {
    return get(table, name, wait);
  }

  /** Get a registry entry with timeout.
      @param tbl  The registry table.
      @param name The name of the entry.
      @param wait The maximum time (seconds) to wait for the entry to show up if it is not already
                  there (zero for no wait). If set to -1, this method will throw an exception if
                  there is no matching registry entry when called.
      @return The registry entry or null if not found.
      @since NeXtMidas 3.3.0
   */
  private static Object get (Table tbl, String name, int wait) {
    if (name==null || name.length()==0) return null;
    if (name.equals("1"))  name = "MAIN";  // since 3.3.2, treat '1' == 'MAIN' (Bug 1364)
    Object entry = tbl.get(name);
    for (int i=wait*10; (entry == null) && (i > 0); i--) {
      Time.sleep(0.1);
      entry = tbl.get(name);
    }
    if ((wait == -1) && (entry == null)) {
      throw new MidasException("Could not find REG."+name); // since NeXtMidas 3.5.4
    }
    return entry;
  }

  /** Get an entry from the registry. This is the same as <code>get(name,wait)</code> except that it
      checks to make sure the entry is an instance of the given type. If the goal is to send
      messages to a given registry entry, the {@link #getMessageHandler(String,int)} is the proper
      method to use.
      @param <T>  The object type.
      @param name The name of the entry.
      @param type The object type.
      @param wait The maximum time to wait (in seconds). If set to -1, this method will throw
                  an exception if there is no matching registry entry when called.
      @return The registry entry or null if not found. <b>Unlike {@link #getCommand(String,int)}
              this method will NOT return null if an entry matches the given name, but is of the
              wrong type.</b>
      @since NeXtMidas 3.5.4
   */
  public <T extends Command> T get (String name, Class<T> type, int wait) {
    Object entry = get(name,wait);
    if (type.isInstance(entry) || (entry == null)) {
      return type.cast(entry);
    }
    throw new MidasException("Expected instance of "+type+" for REG."+name+" but found "+entry.getClass());
  }

  /** Get a command from the registry. This is the same as <code>get(name,wait)</code> except that it
      checks to make sure the entry is a {@link Command}. If the goal is to send messages to a
      given registry entry, the {@link #getMessageHandler(String,int)} is the proper method to use.
      @param name The name of the entry.
      @param wait The maximum time to wait (in seconds). If set to -1, this method will throw
                  an exception if there is no matching registry entry when called.
      @return The registry entry or null if not found. Also returns null if an entry matches the
              given name, but is not an instance of {@link Command}.
   */
  public Command getCommand (String name, int wait) {
    Object entry = get(name,wait);
    return (entry instanceof Command)? (Command)entry : null;
  }

  /** Get a MessageHandler from the registry. This is identical to <code>getMessageHandler(name,0)</code>.
      @param name The name of the entry.
      @return The MessageHandler from the registry entry or null if not found. Also returns null if
              an entry matches the given name, but is not an instance of {@link MessageHandler}.
      @since NeXtMidas 2.7.1
   */
  public MessageHandler getMessageHandler (String name) {
    return getMessageHandler(name, 0);
  }

  /** Get a command from the registry. This is similar to <code>get(name,wait)</code> except that it
      checks to make sure the entry is a {@link MessageHandler}. Also, this method is able to
      handle the <code>"XM.&lt;num&gt;"</code> syntax used when sending messages to X-Midas if
      NeXtMidas is being run from X-Midas.
      @param name The name of the entry.
      @param wait The maximum time to wait (in seconds). If set to -1, this method will throw
                  an exception if there is no matching registry entry when called.
      @return The registry entry or null if not found. Also returns null if an entry matches the
              given name, but is not an instance of {@link MessageHandler}.
   */
  public MessageHandler getMessageHandler (String name, int wait) {
    return getMessageHandler(name, wait, table);
  }

  /** Get a command from the registry. This is similar to <code>get(name,wait)</code> except that it
      checks to make sure the entry is a {@link MessageHandler}. Also, this method is able to
      handle the <code>"XM.&lt;num&gt;"</code> syntax used when sending messages to X-Midas if
      NeXtMidas is being run from X-Midas.
      @param name The name of the entry.
      @param wait The maximum time to wait (in seconds). If set to -1, this method will throw
                  an exception if there is no matching registry entry when called.
      @param initialRegTable initial Registry Table to start from (since 3.3.1)
      @return The registry entry or null if not found. Also returns null if an entry matches the
              given name, but is not an instance of {@link MessageHandler}.
      @since NeXtMidas 3.3.1 for initialRegTable parameter
   */
  private MessageHandler getMessageHandler (String name, int wait, Table initialRegTable) {
    if (name == null) return null;

    if (name.startsWith("XM."))
      return XmUtil.getXmMessageHandler(name);

    Table tbl = initialRegTable;
    if (name.startsWith("GLOBAL.")) {  // since 3.3.0: allow access to global registry
      name = name.substring(7);
      tbl  = stable;
    }
    else if (name.startsWith("PARENT.")) { // since 3.3.0: allow access to parent's registry
      Midas midas = Convert.ref2Midas(tbl.getO("MAIN"));

      if ((midas != null) && (midas.parent != null)) {
        name = name.substring(7);
        tbl  = midas.parent.registry.table;
        if (name.startsWith("PARENT."))
          return getMessageHandler(name, wait, tbl); // since 3.3.1: allow multiple PARENT.PARENT levels
      }
      else { // already at top level, handle same as "GLOBAL.<id>"
        name = name.substring(7);
        tbl  = stable;
      }
    }
    Object entry = get(tbl, name, wait);
    return (entry instanceof MessageHandler)? (MessageHandler)entry : null;
  }

  /** Remove entry. This also removes the current thread from the threads table.
      @param name The name of the entry.
   */
  public void remove (String name) {
    table.remove(name);
    threads.remove( Thread.currentThread() );
  }

  /** Clear the entire registry. */
  public void clear () {
    table.clear();
  }

  /** Get the registry entry of the calling thread.
      @return The registry entry of the calling thread.
   */
  public Command getThreadOwner () {
    return threads.get( Thread.currentThread() );
  }

  /** Return the class name of a handler.
      @param key The key in the handlers table.
      @return The class name from the handlers table or null if not found.
   */
  public static String getHandlerName (String key) {
    return (String)handlers.get(key);
  }

  /** Return an instance of a handler. This calls <code>getHandlerName(key)</code> and then
      uses <code>Shell.getInstanceForClassName(..)</code> to instantiate the handler.
      @param key The key in the handlers table.
      @return A new instance of the named class name from the handlers table or null if not found.
   */
  public static Object getHandler (String key) {
    String className = getHandlerName(key);
    if (className==null) return null;
    return Shell.getInstanceForClassName(className);
  }

}
