package org.jcon.ba.system;

import org.jcon.param.Param;
import org.jcon.param.ParamDriven;
import org.jcon.param.ParamDrivenInfo;
import org.jcon.ba.system.router.EventRouterEvent;
import org.jcon.ba.system.router.EventRouterListener;
import org.jcon.util.DataLib;
import org.jcon.util.GenLib;
import org.jcon.util.msg.Message;
import org.jcon.util.msg.MessageDef;
import org.jcon.util.msg.MessageListener;
import org.jcon.util.msg.MessageRouter;
import org.jcon.util.msg.MessageService;
import org.jcon.util.msg.MessageSource;
import org.jcon.util.minor.BetterHashtable;
import org.jcon.util.service.ContainerServices;
import org.jcon.util.service.ContainerServicesUser;
import java.io.File;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

/**
 * A Basket is a container of Items. A Basket performs a
 * task. The root Basket runs the entire system.
 * <p>
 * It's important to realize that a Basket automatically
 * instantiates an Item when needed, such as invocations or
 * to send it a Message. Baskets are automatically started
 * when a message is received.
 * <p>
 * The order of a Basket's items are important only for
 * visual understanding. Forward or backward config
 * references are fully supported. Circular references are
 * also supported.
 *
 * @author Jack Harich
 */
public class Basket implements java.io.Serializable,
    MessageListener, MessageSource, EventRouterListener,
    ParamDriven {

//---------- Private Fields ------------------------------
// Key = itemName, Object = Item. Contains live items.
private BetterHashtable items = new BetterHashtable();

// Key = itemName, Object = irrelevant
private Hashtable invocatingItemNames = new Hashtable();

private BasketDef      basketDef = new BasketDef();
private Item           ownerItem;
private BasketServices basketServices = new BasketServices(this);
private MessageRouter  messageRouter = new MessageRouter();
private ContainerServices containerServices = new ContainerServices();
private boolean        branchOpened;
private boolean        basketStarted;

private static String BEAN_PREFIX = "$";

//---------- MessageListener Implementation --------------
/**
 * A Basket may listen for events within it's containing
 * Basket. This plus add and removeMessageListener()
 * propagates Messages in the item hierarchy.
 *
 * Whenever a Basket receives a message it is started if
 * not already started. This is the crucial mechanism for
 * delayed Basket initialization. Otherwise an entire
 * system would be started when the root is started.
 */
public void processMessage(Message message) {
    if (! basketStarted) ownerItem.start(); // Delayed initialization
    messageRouter.fire(message, this);
}
// My interests are my items' listeners == this
// ie I'm interested in messages from them
public String[] loadMessageInterests() {
    // Build interestsVector
    Vector interestsVector = new Vector();
    Enumeration enum = items.elements();
    while (enum.hasMoreElements()) {
        Item item = (Item)enum.nextElement();
        Object instance = item.getInvocationInstance();

        if (instance instanceof MessageSource) {
            MessageSource source = (MessageSource)instance;
            MessageRouter router = source.getMessageRouter();
            String[] names = router.getEventNamesWithListener(this);
            for (int i = 0; i < names.length; i++) {
                interestsVector.addElement(names[i]);
            }
        }
    }
    // Convert to array and sort
    return DataLib.sortStringVectorToArray(interestsVector);
}
//---------- MessageSource Implementation ----------------
public void addMessageListener(String eventName, MessageListener listener) {
    messageRouter.addListener(eventName, listener);
}
public void removeMessageListener(String eventName, MessageListener listener) {
    messageRouter.removeListener(eventName, listener);
}
public MessageRouter getMessageRouter() {
    return messageRouter;
}
// Only names are supplied. properties are unknown ***
public Vector getMessageDefs() {
    Vector defs = new Vector();
    MessageDef def;
    // My signals are all my listeners
    String[] eventNames = messageRouter.getEventNames();
    for (int i = 0; i < eventNames.length; i++) {
        def = new MessageDef(eventNames[i], this);
        defs.addElement(def);
    }
    return defs;
}
//---------- EventRouterListener Implementation ----------
public void beforeEventRouterMulticast(EventRouterEvent event) {
    if (! basketStarted) ownerItem.start(); // Delayed initialization
}
//---------- ParamDriven Implementation ------------------
public void setParam(Param param) {
    basketDef.setParam(param);
    param.setParamDriven((ParamDriven)this);
}
public Param getParam() {
    return basketDef.getParam();
}
/**
 * It's serious business to apply a newParam. This will: <BR>
 *  - close() the Basket, which closes the entire branch <BR>
 *  - Rebuild its BasketDef from the newParam            <BR>
 *  - restore itself to its current itemState, such as RUNNING
 *
 * Returns true for success or false for failure.
 * If a failure occurs we attempt to restore ourself to
 * the previous Param and itemState.
 *
 * A future version may preserve some state, such as items
 * that are not changed by the newParam.
 */
public boolean applyNewParam(Param newParam) {

    int       oldState = ownerItem.getItemState();
    boolean   oldBranchOpened = branchOpened;
    BasketDef oldBasketDef = basketDef;
    try {
        close();
        basketDef.setParam(newParam);
        toItemState(oldState, oldBranchOpened);
        return true;

    } catch(Exception ex) {
        GenLib.exception("Basket.applyNewParam()",
            "Shucks, cannot apply new Param. \nWill attempt to rollback.", ex);
        close();
        // Use old values to rollback
        basketDef = oldBasketDef;
        toItemState(oldState, oldBranchOpened);
        return false;
    }
}
public ParamDrivenInfo getParamDrivenInfo() {
    return null;
}
private void toItemState(int newState, boolean newBranchOpened) {
    if (newState == Item.CREATED) {
        // Do nothing
    } else if (newState == Item.INITIALIZED) {
        // Do nothing
    } else if (newState == Item.RUNNING) {
        start();
    } else if (newState == Item.PAUSED) {
        start();
        pause();
    }
    branchOpened = newBranchOpened;
    if (branchOpened) openBranch();
}
//---------- Properties ----------------------------------
/**
* The OwnerItem is the Item that "owns" this instance.
* OwnerItem is never null. It is NOT this instance.
*/
public void setOwnerItem(Item ownerItem) { // Called by ItemSystem.createBasket()
    this.ownerItem = ownerItem;
    // Maintain containerServices's parent
    if (ownerItem.isRoot()) {
        containerServices.setParent(null);
    } else {
        Basket parentBasket = ownerItem.getParentItem().getBasket();
        containerServices.setParent(parentBasket.containerServices);
    }
}
public BasketServices getBasketServices() {
    return basketServices;
}
public int getItemCount() {
    return basketDef.getItemCount();
}
public BasketDef getBasketDef() {
    return basketDef;
}
public String[] getItemNames() {
    return basketDef.getItemNames();
}
public ContainerServices getContainerServices() {
    return containerServices;
}
//---------- Public Methods ------------------------------
//----- containerServices, delegation
public void setObjectService(String name, Object service) {
    //print(".setObjectService() - " + name + ", " + service);
    containerServices.setPublicService(name, service);
}
public Object removeObjectService(String name) {
    return containerServices.removePublicService(name);
}
/**
* Sets the MessageService for the messageName. Only one 
* MessageService at a time can service a messageName. 
* Setting multiple ones will cause only the last to be in
* effect, just like any standard "setter" method.
*/
public void setMessageService(String messageName, MessageService handler) {
    containerServices.setMessageHandler(messageName, handler);
}    
public void removeMessageService(String messageName) {
    containerServices.removeMessageHandler(messageName);
}
//----- Other
public void openChildren() {
    loadAllItems();
}
/**
 * Opens the entire branch starting from this node. There
 * is no harm in calling this if already called before.
 */
public void openBranch() { // RECURSIVE
    loadAllItems();
    // Call openBranch() on my baskets
    String[] itemNames = basketDef.getItemNames();
    for (int i = 0; i < itemNames.length; i++) {
        Item item = getLiveItem(itemNames[i]);
        if (item.isContainer()) item.getBasket().openBranch();
    }
    branchOpened = true;
}
// RECURSIVE
public void loadBranchBeanInstances(Vector instances) {
    if (! basketStarted) start();
    Enumeration enum = items.elements();
    while (enum.hasMoreElements()) {
        Item item = (Item)enum.nextElement();
        if (item.isBeanWrapper()) {
            instances.addElement(item.getBeanInstance());
        }
        if (item.isContainer()) {
            item.getBasket().loadBranchBeanInstances(instances);
        }
    }
}
//---------- Package Methods -----------------------------
//----- The "Big 6"
// No init() since BasketDef supplied when created
/**
 * Starts this module's items with these steps: <p> <pre>
 *  1. Instantiate all items and providing Param if used
 *  2. Invoke configs for this module
 *  3. Invoke configs for all items
 *  4. Call init() for each item
 *  5. Invoke PrimordialInvocations <p> </pre>
 *
 * Note that start() is never called (in this method) on
 * an item unless itemName.start() is in PrimordialInvocations.
 * In this case the item start() is called, not the instance's start().
 * We highly recommend calling only start() and tempoary
 * unit tests in PrimordialInvocations, because start() is
 * not called with reflection, and thus debugging is easier.
 */
void start() {
    //print(".start() - entered");
    //--- 1. Instantiate all items, provide Param if used
    // Note subsequent steps use the items collection
    //print(".start() - Before loadAllItems() " + BenchMark.getTimePoint(0));
    loadAllItems();
    //print(".start() - After  loadAllItems() " + BenchMark.getTimePoint(0));
    //print(".start() - step 1 done");

    //--- 2. Invoke configs for this module
    doInvocations("this", false); // false for isPrimordial      
    //print(".start() - step 2 done");

    //--- 3. Invoke configs for all items, in order added.
    Enumeration names = items.getOrderedKeys();
    while (names.hasMoreElements()) {
        String name = (String)names.nextElement();
        doInvocations(name, false); // false for isPrimordial  
    }
    //print(".start() - step 3 done");

    //--- 4. Call init() for each item
    Enumeration enumItems = items.elements();
    while (enumItems.hasMoreElements()) {
        Item item = (Item)enumItems.nextElement();
        //print(".start() - calling init() on " + item.getItemName());
        item.init();
    }
    //print(".start() - step 4 done");

    //--- 5. Invoke PrimordialInvocations
    doInvocations("#Primordial#", true); // true for isPrimordial

    //--- Done
    basketStarted = true;
    //print(".start() - completed");
}
// finally fails, apparent Java bug, finally is not reentrant.
// Used catch instead as workaround. JH 9/2/98 Java 1.1.5
/**
* Does the invocation lines if not already done.
* itemName = (irrelevant) if isPrimordial, in which 
* case the itemName must be supplied in the parex. In all
* other cases the itemName is NOT supplied and so is 
* appended to the invocation line here. We support circular
* reference infinite loop avoidance.
*/
private void doInvocations(String itemName, boolean isPrimordial) {
try {
    // Avoid infinite loop due to circular reference
    if (invocatingItemNames.containsKey(itemName)) return;
    
    // Get invocations, return if alread invocated
    Vector invocations;
    if (isPrimordial) {
        if (basketDef.isPrimordialInvocated()) return;
        invocations = basketDef.getPrimordialInvocations();
    } else {
        if (basketDef.getItemDef(itemName).isInvocated()) return;
        invocations = basketDef.getItemDef(itemName).getConfigLines();
    }
    // Do invocations
  //print(".doInvocations() - Invocating " + itemName);    
    invocatingItemNames.put(itemName, "");
    
    for (int i = 0; i < invocations.size(); i++) {
        String line = (String)invocations.elementAt(i);
        if (! isPrimordial) line = itemName + "." + line;
        new Invocation(line).invoke(this);
    }
    // Set invocated flag
    if (isPrimordial) {
        basketDef.setPrimordialInvocated(true);
    } else {
        basketDef.getItemDef(itemName).setInvocated(true);
    }
    // Done
    //print(".doInvocations() - Removing " + itemName);
    invocatingItemNames.remove(itemName);
} catch(Exception ex) { 
   
//} finally {  // <-----<<< fails  
    print(".doInvocations() - Exception, itemName = " + itemName);
    ex.printStackTrace();
    invocatingItemNames.remove(itemName);       
}    
} // End method
    
void pause() {
    Enumeration enumItems = items.elements();
    while (enumItems.hasMoreElements()) {
        Item item = (Item)enumItems.nextElement();
        item.pause();
    }
}
void resume() {
    Enumeration enumItems = items.elements();
    while (enumItems.hasMoreElements()) {
        Item item = (Item)enumItems.nextElement();
        item.resume();
    }
}
String canClose() {
    Enumeration enumItems = items.elements();
    while (enumItems.hasMoreElements()) {
        Item item = (Item)enumItems.nextElement();
        String message = item.canClose();
        if (message != null) return message;
    }
    return null; // null means yes I can close
}
/**
 * Closing a basket results in closing all its children,
 * emptying the basket's items, updating the tree, and
 * resetting internal state fields. The result is the
 * basket is right back where it was when first created.
 *
 * This method is designed to be called before a basket is
 * dereferenced. It has no effect if the basket is already
 * closed.
 */
void close() {
    // Close children and remove from tree
    Enumeration enumItems = items.elements();
    while (enumItems.hasMoreElements()) {
        Item item = (Item)enumItems.nextElement();
        item.close();
        item.getNode().removeFromTree();
        ownerItem.getItemSystem().removeItem(item);

        if (item.isContainer()) {
            messageRouter.removeListener((MessageListener)item.getBasket());
        }
        if (item.isBeanWrapper()) {
            Object bean = item.getBeanInstance();
            if (bean instanceof MessageListener) {
                messageRouter.removeListener((MessageListener)bean);
            }
        }
    }
    messageRouter.removeListener(this);
    items.clear();
    containerServices.removeAllServices();
    basketStarted = false;
    branchOpened = false;
    basketDef.close();
}
//----- Other
/**
* Returns the Item named itemName, instantiating and 
* adding if necessary. A $ prefix is ignored.
*
* ****** public to support EventRouterBIP, this will
* probably change back to package. DO NOT USE THIS METHOD.
*/
public Item getLiveItem(String itemName) {
    if (itemName.equals("this")) return ownerItem;

    if (itemName.startsWith(BEAN_PREFIX)) itemName = itemName.substring(1);
    Item item = (Item)items.get(itemName); // getItem
    if (item == null) {
        // Instantiate but do not start
        item = addItem(itemName);
    }
    return item;
}
/**
* Returns the actual named instance associated with an
* Item in this Basket, which is a bean or Basket. An
* itemName of "this" returns this Basket instance.
* If the item has not yet been invocated it is invocated,
* which supports forward and backward references, as well
* as "Auto" features.
* <p>
* If the itemName starts with $ and the item is both a
* Container and BeanWrapper, the BeanWrapper instance is
* returned. Otherwise a $ prefix is ignored
*/
Object getItemInstance(String itemName) {
    // Set instance and itemName
    Object instance;
    if (itemName.equals("this")) {
        instance = this;
    } else {
        boolean beanPreferred = false;
        if (itemName.startsWith(BEAN_PREFIX)) {
            itemName = itemName.substring(1);
            beanPreferred = true;
        }
        Item item = getLiveItem(itemName);
        if (item.isContainer() && item.isBeanWrapper()) {
            if (beanPreferred) {
                instance = item.getBeanInstance();
            } else {
                instance = item.getBasket();
            }
        } else if (item.isContainer()) {
            instance = item.getBasket();
        } else {
            instance = item.getBeanInstance();
        }
    }
    // DoInvocations if not yet invocated
    doInvocations(itemName, false); // false for isPrimordial

    // Done
    return instance;
}
Item getOwnerItem() {
    return ownerItem;
}
//---------- Private Methods -----------------------------
private Item addItem(String itemName) {
    ItemDef itemDef = basketDef.getItemDef(itemName);
    if (items.containsKey(itemName)) {
        GenLib.error("Basket.addItem()", "itemName '" + itemName + "' is already in use.");
        return null;
    }
    //print(".addItem() - '" + itemName + "' " + this);
    try {
        // ownerItem becomes the parent of the new item
        Item item = ownerItem.getItemSystem().createItem(
            itemName, itemDef, ownerItem);
        items.put(itemName, item); // <-----<<<
        return item;

    } catch(Exception ex) {
        GenLib.exception("Basket.addItem()",
            "Cannot add item itemName " + itemName, ex);
        return null;
    }
}
/**
* Loads all items and then runs the AutoConfig feature.
* If the parex contains "AutoConfig is: this, item1, item2"
* then these items will be invocated in that order. Please
* note that this will provoke other item invocations if
* other items are referenced.
*/
private void loadAllItems() {
    String[] itemNames = basketDef.getItemNames();
    for (int i = 0; i < itemNames.length; i++) {
        //print(".loadAllItems() - Loading " + itemNames[i] + " - " + BenchMark.getTimePoint(0));
        getLiveItem(itemNames[i]);
    }
    // AutoConfig feature. There are usually none.
    // We invoke the names in the sequence specified.
    String[] names = basketDef.getAutoConfigItemNames();
    for (int i = 0; i < names.length; i++ ) {
        doInvocations(names[i], false); // false for isPrimordial
    }
}
//--- Std
private static void print(String text) {
    System.out.println("Basket" + text);
}

} // End class