/*
  Copyright (C) 2006 Helge Hess

  This file is part of JOPE.

  JOPE is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.

  JOPE 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 Lesser General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with JOPE; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

package org.opengroupware.jope.appserver.core;

import java.util.HashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.foundation.NSKeyValueCoding;

/*
 * WOComponent
 * 
 * TODO: document
 * - has a template (tree of WODynamicElement subclasses)
 * - has subcomponents
 * - is stateful (unless its isStateless?)
 * - is bound to a context (unless its isStateless?)
 * - different to WO:
 *   - initialization (default constructor and initWithContext)
 *   - component local resource manager
 *   - redirect support
 *   - can be used as a WODirectAction
 */
public class WOComponent extends WOElement implements WOActionResults {
  
  protected static final Log compLog = LogFactory.getLog("WOComponent");
  protected static final Log pageLog = LogFactory.getLog("WOPages");
  
  protected String    _wcName;
  protected WOContext context;
  protected WOSession session;
  
  protected Map<String,WOComponent>   subcomponents;
  protected Map<String,WOAssociation> wocBindings;
  protected WOComponent               parentComponent;
  
  public WOComponent() {
  }
  
  public WOComponent initWithContext(WOContext _ctx) {
    this.context = _ctx;
    return this;
  }
  
  /* accessors */
  
  public WOContext context() {
    return this.context;
  }
  
  public WOApplication application() {
    return this.context.application();
  }
  
  public Log log() {
    return compLog;
  }
  
  protected void _setName(String _name) {
    this._wcName = _name;
  }
  public String name() {
    if (this._wcName != null)
      return this._wcName;
    
    String s = this.getClass().getSimpleName();
    if (!"Component".equals(s))
      return s;
    
    /* a package component (class is named 'Component') */
    s = this.getClass().getPackage().getName();
    int idx = s.lastIndexOf('.');
    return s.substring(idx + 1);
  }
  
  /* notifications */
  
  protected boolean isAwake = false;
  
  public void awake() {
    compLog.debug("awake");
  }
  
  public void sleep() {
    compLog.debug("sleep");

    if (this.isStateless())
      this.reset();
    
    // TODO: Is the following necessary? I don't think so. But might be more
    //       secure.
    this.isAwake = false;
    this.context = null;
    this.session = null;
  }
  
  public void _setContext(WOContext _ctx) {
    if (compLog.isDebugEnabled())
      compLog.debug("set ctx" + _ctx);
    this.context = _ctx;
  }
  
  public void ensureAwakeInContext(WOContext _ctx) {
    if (compLog.isDebugEnabled())
      compLog.debug("ensure awake in ctx" + _ctx);
    
    if (this.isAwake) {
      if (this.context == _ctx)
        return; /* already awake */
    }
    
    if (this.context == null)
      this._setContext(_ctx);
    if (this.session == null && this.context.hasSession())
      this.session = this.context.session();
    
    this.isAwake = true;
    this.context._addAwakeComponent(this); /* ensure that sleep is called */
    
    if (this.subcomponents != null) {
      for (WOComponent child: this.subcomponents.values())
        child._awakeWithContext(_ctx);
    }
    
    this.awake();
  }
  
  public void _awakeWithContext(WOContext _ctx) {
    /* this is called by the framework to awake the component */
    if (!this.isAwake)
      this.ensureAwakeInContext(_ctx);
  }
  
  public void _sleepWithContext(WOContext _ctx) {
    /* this is called by the framework to sleep the component */
    if (compLog.isDebugEnabled())
      compLog.debug("sleep in ctx" + _ctx);
    
    if (_ctx != this.context && _ctx != null && this.context != null) {
      compLog.error("component is awake in different context!");
      return;
    }
    
    if (this.isAwake) {
      this.isAwake = false;
      
      if (this.subcomponents != null) {
        for (WOComponent child: this.subcomponents.values())
          child._sleepWithContext(_ctx);
      }
      
      this.sleep();
    }
    
    this._setContext(null);
    this.session = null;
  }
  
  /* component synchronization */
  
  public boolean synchronizesVariablesWithBindings() {
    return true;
  }
  
  public void pullValuesFromParent() { /* official WO method */
    if (this.synchronizesVariablesWithBindings())
      this.syncFromParent(this.context.parentComponent());    
  }
  public void pushValuesToParent() { /* official WO method */
    if (this.synchronizesVariablesWithBindings())
      this.syncToParent(this.context.parentComponent());
  }
  
  public void syncFromParent(WOComponent _parent) { /* SOPE method */
    boolean isDebug = compLog.isDebugEnabled();
    
    if (this.wocBindings == null) {
      if (isDebug) compLog.debug("nothing to sync from parent ...");
      return;
    }
    
    if (isDebug)
      compLog.debug("will sync from parent: " + _parent);
    
    for (String bindingName: this.wocBindings.keySet()) {
      // TODO: this is somewhat inefficient because -valueInComponent: does
      //       value=>object coercion and then takeValue:forKey: does the
      //       reverse coercion. We could improve performance for base values
      //       if we implement takeValue:forKey: on our own and just pass over
      //       the raw value (ie [self setIntA:[assoc intValueComponent:self]])
      WOAssociation binding = this.wocBindings.get(bindingName);
      this.takeValueForKey(binding.valueInComponent(_parent), bindingName);
    }
    
    if (isDebug)
      compLog.debug("did sync from parent: " + _parent);
  }
  public void syncToParent(WOComponent _parent) { /* SOPE method */
    boolean isDebug = compLog.isDebugEnabled();
    
    if (this.wocBindings == null) {
      if (isDebug) compLog.debug("nothing to sync to parent ...");
      return;
    }
    
    if (isDebug)
      compLog.debug("will sync to parent: " + _parent);

    for (String bindingName: this.wocBindings.keySet()) {
      WOAssociation binding = this.wocBindings.get(bindingName);
      
      if (binding.isValueSettableInComponent(_parent))
        binding.setValue(this.valueForKey(bindingName), _parent);
    }

    if (isDebug)
      compLog.debug("did sync to parent: " + _parent);
  }
  
  public boolean setValueForBinding(Object _value, String _name) {
    WOAssociation binding = this.wocBindings.get(_name);
    if (binding == null)
      return false;
    
    WOComponent lParent = this.parent();
    if (!binding.isValueSettableInComponent(lParent))
      return false;
    
    binding.setValue(_value, _name);
    return true;
  }
  
  public Object valueForBinding(String _name) {
    WOAssociation binding = this.wocBindings.get(_name);
    if (binding == null)
      return null;
    
    return binding.valueInComponent(this.parent());
  }
  
  public boolean hasBinding(String _name) {
    return this.wocBindings.containsKey(_name);
  }
  public boolean canGetValueForBinding(String _name) {
    // TODO: not perfectly OK?
    return this.hasBinding(_name);
  }
  public boolean canSetValueForBinding(String _name) {
    WOAssociation binding = this.wocBindings.get(_name);
    if (binding == null)
      return false;
    
    return binding.isValueSettableInComponent(this.parent());    
  }
  
  public void setBindings(Map<String,WOAssociation> _assocs) {
    this.wocBindings = _assocs;
  }
  public String[] bindingKeys() {
    String[] k = this.wocBindings.keySet().toArray(new String[0]); 
    return k;
  }
  
  public void setParent(WOComponent _parent) {
    this.parentComponent = _parent;
  }
  public WOComponent parent() {
    return this.parentComponent;
  }
  
  public void _setSubcomponents(Map<String, WOComponent> _subs) {
    this.subcomponents = _subs;
  }
  
  public Object performParentAction(String _name) {
    WOContext   ctx     = this.context();
    WOElement   content = ctx.componentContent();
    WOComponent parent  = ctx.parentComponent();
    Object      result  = null;
    
    ctx.leaveComponent(this);
    result = parent.valueForKey(_name);
    ctx.enterComponent(this, content);
    
    return result;
  }
  
  public WOComponent childComponentWithName(String _name) {
    WOComponent child;
    
    if (_name == null || this.subcomponents == null)
      return null;
    
    if ((child = this.subcomponents.get(_name)) == null) {
      compLog.debug("did not find child component: " + _name);
      return null;
    }
    
    if (child instanceof WOComponentFault) {
      child = ((WOComponentFault)child).resolveWithParent(this);
      if (child != null)
        this.subcomponents.put(_name, child);
    }
    return child;
  }
  
  /* session handling */
  
  public boolean hasSession() {
    if (this.session != null)
      return true;
    if (this.context == null)
      return false;
    return this.context.hasSession();
  }
  
  public WOSession session() {
    if (this.session != null)
      return this.session;
    if (this.context == null)
      return null;
    
    this.session = this.context.session();
    return this.session;
  }
  
  public WOSession existingSession() {
    return this.hasSession() ? this.session() : null;
  }
  
  /* resource manager */
  
  protected WOResourceManager resourceManager = null;
  
  public void setResourceManager(WOResourceManager _rm) {
    if (compLog.isDebugEnabled())
      compLog.debug("setting resource manager: "+ _rm);
    
    this.resourceManager = _rm;
  }
  public WOResourceManager resourceManager() {
    if (this.resourceManager != null)
      return this.resourceManager;
    
    return this.application().resourceManager();
  }
  
  /* labels */
  
  protected WOComponentStringTable labels = null;
  
  public WOComponentStringTable labels() {
    if (this.labels == null)
      this.labels = new WOComponentStringTable(this);
    return this.labels;
  }
  
  /* templates */
  
  protected WOElement template = null;
  
  public void setTemplate(WOElement _template) {
    this.template = _template;
  }
  
  public WOElement template() {
    if (this.template != null)
      return this.template;
    
    // TODO: somehow this fails if the component was not instantiated using
    //       pageWithName()
    return this.templateWithName(this.name());
  }
  
  public WOElement templateWithName(String _name) {
    WOResourceManager rm;
    
    if ((rm = this.resourceManager()) == null) {
      compLog.warn("missing resourcemanager to lookup template: " + _name);
      return null;
    }
    
    /* Note: this fails if the component was created by using a qualified name,
     *       eg 'account.HomePage'
     */
    return rm.templateWithName(_name, this.context().languages(), rm);
  }
  
  /* pages */
  
  public WOComponent pageWithName(String _name) {
    pageLog.debug("pageWithName: " + _name);
    
    // TODO: SOPE uses the WOResourceManager directly
    return this.application().pageWithName(_name, this.context());
  }
  
  /* WOActionResults */
  
  public WOResponse generateResponse() {
    compLog.debug("generate response ...");
    
    WOResponse r;
    
    r = new WOResponse(this.context().request());
    this.appendToResponse(r, this.context());
    return r;
  }
  
  /* WODirectAction */
  
  public Object performActionNamed(String _name) {
    compLog.debug("perform: " + _name);
    return WODirectAction.performActionNamed(this, _name, this.context());
  }
  
  public WOActionResults defaultAction() {
    return this;
  }
  
  public WOActionResults redirectToLocation(String _url) {
    if (_url          == null) return null;
    if (_url.length() == 0)    return null;
    
    WOResponse r = this.context().response();
    if (r == null) r = new WOResponse(this.context().request());
    
    // TODO: check for java.util.URL
    // TODO: support relative URLs
    
    r.setStatus(WOMessage.HTTP_STATUS_FOUND /* moved */);
    r.setHeaderForKey(_url, "location");
    return r;
  }
  
  /* responder */
  
  public boolean shouldTakeValuesFromRequest(WORequest _rq, WOContext _ctx) {
    if (_rq == null)
      return false;
    
    if ("POST".equals(_rq.method()))
      /* always process POST requests */
      return true;
    
    /* and always process requests which have form values */
    if (_rq.hasFormValues())
      return true;
    
    return false;
  }
  
  public void takeValuesFromRequest(WORequest _rq, WOContext _ctx) {
    compLog.debug("will takeValuesFromRequest ...");
    
    if (!this.isAwake)
      compLog.error("component is not awake!");
    
    WOElement lTemplate = this.template();
    if (lTemplate != null)
      lTemplate.takeValuesFromRequest(_rq, _ctx);
    else
      compLog.warn("component has no template, cannot take rq values!");

    compLog.debug("did takeValuesFromRequest.");
  }
  
  public Object invokeAction(WORequest _rq, WOContext _ctx) {
    Object result = null;
    
    compLog.debug("will invokeAction ...");

    if (!this.isAwake)
      compLog.error("component is not awake!");
    
    WOElement lTemplate = this.template();
    if (lTemplate != null)
      result = lTemplate.invokeAction(_rq, _ctx);
    else
      compLog.warn("component has no template, cannot invoke action!");

    compLog.debug("did invokeAction.");
    return result;
  }
  
  public void appendToResponse(WOResponse _r, WOContext _ctx) {
    compLog.debug("will appendToResponse ...");

    if (!this.isAwake)
      compLog.error("component is not awake!");
    
    WOElement lTemplate = this.template();
    if (lTemplate != null)
      lTemplate.appendToResponse(_r, _ctx);
    else
      compLog.warn("component has no template, cannot append: " + this);
    
    if (this.isStateless()) // not sure whether this is correct
      this.reset();

    compLog.debug("did appendToResponse.");
  }
  
  public void walkTemplate(WOElementWalker _walker, WOContext _ctx) {
    compLog.debug("will walkTemplate ...");

    if (!this.isAwake)
      compLog.error("component is not awake!");
    
    WOElement lTemplate = this.template();
    if (lTemplate != null)
      _walker.processTemplate(this, lTemplate, _ctx);
    else
      compLog.warn("component has no template, cannot walk: " + this);
    
    if (this.isStateless()) // not sure whether this is correct
      this.reset();

    compLog.debug("did walkTemplate.");
  }

  /* extra attributes */
  
  // TODO: threading?
  protected Map<String,Object> extraAttributes = null;
  
  public void setObjectForKey(Object _value, String _key) {
    if (_value == null) {
      this.removeObjectForKey(_key);
      return;
    }

    if (this.extraAttributes == null)
      this.extraAttributes = new HashMap<String,Object>(16);
    
    this.extraAttributes.put(_key, _value);
  }
  
  public void removeObjectForKey(String _key) {
    if (this.extraAttributes == null)
      return;
    
    this.extraAttributes.remove(_key);
  }
  
  public Object objectForKey(String _key) {
    if (_key == null || this.extraAttributes == null)
      return null;
    
    return this.extraAttributes.get(_key);
  }
  
  public Map<String,Object> variableDictionary() {
    return this.extraAttributes;
  }
  
  public void reset() {
    if (this.extraAttributes != null) this.extraAttributes.clear();
  }
  
  public boolean isStateless() {
    // TODO: possibly this could become a WOComponent annotation?
    return false;
  }

  /* KVC */
  
  public void takeValueForKey(Object _value, String _key) {
    if (this.extraAttributes != null) {
      if (this.extraAttributes.containsKey(_key)) {
        this.setObjectForKey(_value, _key);
        return;
      }
    }
    
    NSKeyValueCoding.DefaultImplementation.takeValueForKey(this, _value, _key);
  }
  public Object valueForKey(String _key) {
    if (this.extraAttributes != null) {
      Object v = this.extraAttributes.get(_key);
      if (v != null)
        return v;
    }
    
    return NSKeyValueCoding.DefaultImplementation.valueForKey(this, _key);
  }

  public Object handleQueryWithUnboundKey(String _key) {
    return this.objectForKey(_key);
  }
  public void handleTakeValueForUnboundKey(Object _value, String _key) {
    this.setObjectForKey(_value, _key);
  }

  /* description */
  
  public void appendAttributesToDescription(StringBuffer _d) {
    if (this.context != null)
      _d.append(" ctx=" + this.context.contextID());
    if (this.session != null)
      _d.append(" sid=" + this.session.sessionID());
    
    if (this.parentComponent != null)
      _d.append(" parent=" + this.parentComponent.name());
    
    if (this.template == null)
      _d.append(" no-template");
    
    if (this.isAwake)
      _d.append(" awake");
  }
}
