package org.elkoserver.foundation.actor;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.elkoserver.foundation.json.Deliverer;
import org.elkoserver.foundation.json.DispatchTarget;
import org.elkoserver.foundation.json.MessageDispatcher;
import org.elkoserver.foundation.json.MessageHandlerException;
import org.elkoserver.foundation.json.TypeResolver;
import org.elkoserver.json.JSONObject;
import org.elkoserver.json.Referenceable;

/**
 * A mapping from object reference strings (as they would be used in JSON
 * message parameters) to the objects they refer to.  This mapping may be
 * modified at any time by adding or removing objects.  The class also supports
 * the direct invocation (from JSON messages) of JSON methods on the mapped
 * objects.
 *
 * <p>By convention, the object references mapped by this table take the form:
 *
 *    <blockquote><i>type</i>-<i>ref</i></blockquote>
 *
 * <p>where: 'type' designates the kind of object being referred to, while
 * 'ref' designates a specific object of that type.  The 'ref' and accompanying
 * hyphen separator are optional (actually, you are not required to follow this
 * convention at all, but various classes provide convenience methods that are
 * helpful if you do).  The specific forms of 'type' and 'ref' themselves are
 * unconstrained (other than the use of a hyphen as separator).
 */
public class RefTable implements Iterable<DispatchTarget> {
    /** Mapped Objects, indexed by reference. */
    private Map<String, DispatchTarget> myObjects;

    /** Collections of objects sharing a common root reference string (used in
        the object cloning mechanism: clones are generated by suffixing a
        counter to the reference string of the parent object). */
    private Map<String, List<DispatchTarget>> myObjectGroups;

    /** Mapping from message verbs to MethodInvoker objects (which contain
        precomputed Java reflection objects). */
    private MessageDispatcher myDispatcher;

    /**
     * Constructor.  Creates an empty reference table.
     *
     * @param resolver  Type resolver for the type tags of JSON encoded
     *    object descriptors.
     */
    public RefTable(TypeResolver resolver) {
        myObjects = new HashMap<String, DispatchTarget>();
        myObjectGroups = new HashMap<String, List<DispatchTarget>>();
        myDispatcher = new MessageDispatcher(resolver);
        addRef(new ErrorHandler());
    }

    /**
     * Internal error handler object that is the target of debug JSON messages.
     * A single instance of this is created and stored in the lookup table when
     * it is created, so that received error and debug messages have someplace
     * to go.
     */
    private class ErrorHandler extends BasicProtocolHandler {
        /** Constructor */
        ErrorHandler() {
        }

        /**
         * This object is always called 'error' (there should only ever be one
         * instance).
         *
         * @return a reference string for this object.
         */
        public String ref() {
            return "error";
        }
    }

    /**
     * Add JSON method dispatch information for a Java class to the table's
     * message dispatcher, independent of any particular object instance.
     *
     * @param targetClass  Java class whose JSON methods are to be added.
     */
    public void addClass(Class targetClass) {
        myDispatcher.addClass(targetClass);
    }

    /**
     * Add an object to the table, explicitly specifying its reference string.
     *
     * @param ref  The reference string for the object.
     * @param target  The object referenced by the reference string.
     */
    public void addRef(String ref, DispatchTarget target) {
        myObjects.put(ref, target);
        myDispatcher.addClass(target.getClass());
        String groupRef = rootRef(ref);
        List<DispatchTarget> group = myObjectGroups.get(groupRef);
        if (group == null) {
            group = new LinkedList<DispatchTarget>();
            myObjectGroups.put(groupRef, group);
        }
        group.add(target);
    }

    /**
     * Add an object to the table, using the reference string it knows for
     * itself.
     *
     * @param target  The object referenced by the reference string.
     */
    public void addRef(Referenceable target) {
        addRef(target.ref(), (DispatchTarget) target);
    }

    /**
     * Get a list of all objects in the table that have a common root reference
     * string.
     *
     * @param ref  Reference string designating the object(s) of interest.
     *
     * @return a list of all objects in the table denoted by 'ref'.  If 'ref'
     *    is the root reference of a clone group, returns a list of all the
     *    clones with that root reference.  If 'ref' designates a non-clone
     *    object, returns a single element list containing that unique object.
     *    If there are no objects corresponding to 'ref', an empty list is
     *    returned.
     */
    public List<DispatchTarget> clones(String ref) {
        List<DispatchTarget> group = myObjectGroups.get(ref);
        if (group == null) {
            DispatchTarget object = myObjects.get(ref);
            if (object != null) {
                return Collections.singletonList(object);
            } else {
                return Collections.emptyList();
            }
        } else {
            return Collections.unmodifiableList(group);
        }
    }

    /**
     * Dispatch a JSON message directly to the appropriate method of a given
     * object.
     *
     * @param from  Alleged sender of the message.
     * @param target  The object to which the message is being sent.
     * @param message  The message itself.
     *
     * @throws MessageHandlerException if there was some kind of problem
     *    handling the message.
     */
    public void dispatchMessage(Deliverer from, DispatchTarget target,
                                JSONObject message)
        throws MessageHandlerException
    {
        myDispatcher.dispatchMessage(from, target, message);
    }

    /**
     * Dispatch a JSON message to the appropriate method on the object that the
     * message says it is addressed to.
     *
     * @param from  Alleged sender of the message.
     * @param message  The message itself.
     *
     * @throws MessageHandlerException if there was some kind of problem
     *    handling the message.
     */
    public void dispatchMessage(Deliverer from, JSONObject message)
        throws MessageHandlerException
    {
        String targetRef = message.target();
        if (targetRef != null) {
            DispatchTarget target = get(targetRef);
            if (target != null) {
                myDispatcher.dispatchMessage(from, target, message);
            } else {
                throw new MessageHandlerException("target object '" +
                                                  targetRef + "' not found");
            }
        } else {
            throw new MessageHandlerException("no target in message");
        }
    }

    /**
     * Look up an object by reference string.
     *
     * @param ref  Reference string denoting the object sought.
     *
     * @return the object designated by 'ref', or null if there is no such
     *    object.
     */
    public DispatchTarget get(String ref) {
        return myObjects.get(ref);
    }

    /**
     * Support iteration over all objects in the table.
     *
     * @return an {@link Iterator} over all the objects.
     */
    public Iterator<DispatchTarget> iterator() {
        return myObjects.values().iterator();
    }

    /**
     * Remove an object from the table, explicitly specifying its reference
     * string.  It is permissible to specify a reference string that isn't
     * actually there.
     *
     * @param ref  Reference string for the object to be removed.
     */
    public void remove(String ref) {
        Referenceable object = (Referenceable) myObjects.get(ref);
        if (object != null) {
            myObjects.remove(ref);
            String groupRef = rootRef(ref);
            List<DispatchTarget> group = myObjectGroups.get(groupRef);
            if (group != null) {
                group.remove(object);
                if (group.isEmpty()) {
                    myObjectGroups.remove(groupRef);
                }
            }
        }
    }

    /**
     * Remove an object from the table, using the reference string it knows
     * for itself.  It is permissible to remove an object that isn't actually
     * there.
     *
     * @param object  The object to be removed.
     */
    public void remove(Referenceable object) {
        remove(object.ref());
    }

    /**
     * Extract the root from a reference string.  This is everything before the
     * second '-' character, or the whole string if there are fewer than two
     * '-' characters in it.  This is used in the addressing of clone objects.
     *
     * @param ref  The reference string whose root is sought.
     *
     * @return the root reference string extracted from 'ref'.
     */
    public static String rootRef(String ref) {
        int delim = 0;
        int count = 0;
        while (delim >= 0) {
            delim = ref.indexOf('-', delim);
            if (delim >= 0) {
                ++count;
                if (count == 2) {
                    return ref.substring(0, delim);
                }
                ++delim;
            }
        }
        return ref;
    }
}
