package org.opengroupware.jope.rules;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.eocontrol.EOKeyValueCoding;
import org.opengroupware.jope.eocontrol.EOQualifier;
import org.opengroupware.jope.eocontrol.EOQualifierEvaluation;
import org.opengroupware.jope.foundation.NSObject;

public class RuleContext extends NSObject
  implements EOKeyValueCoding
{
  protected static Log log = LogFactory.getLog("JoRules");

  protected RuleModel           model;
  protected Map<String, Object> storedValues;

  public RuleContext() {
  }
  
  /* accessors */
  
  public void setModel(RuleModel _model) {
    this.model = _model;
  }
  public RuleModel model() {
    return this.model;
  }
  
  /* KVC */

  @Override
  public void takeValueForKey(Object _value, String _key) {
    this.takeStoredValueForKey(_value, _key);
  }

  @Override
  public Object valueForKey(String _key) {
    if (_key == null) return null;

    Object v = this.storedValueForKey(_key);
    if (v != null) return v;
    
    return this.inferredValueForKey(_key);
  }

  public void takeStoredValueForKey(Object _value, String _key) {
    if (_key == null) return;
    if (_value == null) {
      if (this.storedValues != null)
        this.storedValues.remove(_key);
      return;
    }
    
    if (this.storedValues == null)
      this.storedValues = new HashMap<String, Object>(16);
    this.storedValues.put(_key, _value);
  }

  public Object storedValueForKey(String _key) {
    if (_key == null) return null;
    return this.storedValues != null ? this.storedValues.get(_key) : null;
  }
  
  public void reset() {
    if (this.storedValues != null)
      this.storedValues = null;
  }

  /* processing */
  
  public Object inferredValueForKey(String _key) {
    if (_key == null) {
      log.warn("got no key in inferredValueForKey.");
      return null;
    }
    if (this.model == null) {
      log.warn("cannot infer values w/o model: " + _key);
      return null;
    }
    
    boolean isDebugOn = log.isDebugEnabled();
    if (isDebugOn) log.debug("infer value for key: " + _key);
    
    /* the model returns a presorted set of candidates */
    Rule[] rules = this.model.candidateRulesForKey(_key);
    if (rules == null) {
      if (isDebugOn) log.debug("=> no candidates for key: " + _key);
      return null;
    }
    
    /* check qualifiers */
    for (int i = 0; i < rules.length; i++) {
      EOQualifier q = rules[i].qualifier();
      
      if (!(q instanceof EOQualifierEvaluation)) {
        log.warn("contained a rule which does not support eval: " + q);
        continue;
      }
      
      if (((EOQualifierEvaluation)q).evaluateWithObject(this)) {
        /* found it! */
        if (isDebugOn) log.debug("=> qualifier matched: " + rules[i]);
        
        RuleAction action = (RuleAction)rules[i].action();
        if (isDebugOn) log.debug("=> fire: " + action);
        return action.fireInContext(this);
      }
    }
    
    /* no rule matched */
    if (isDebugOn) log.debug("=> no rule qualifier matched: " + _key);
    return null;
  }
  
  public List<Object> allPossibleValuesForKey(String _key) {
    if (_key == null) {
      log.warn("got no key in allPossibleValuesForKey.");
      return null;
    }
    if (this.model == null) {
      log.warn("cannot calc values w/o model: " + _key);
      return null;
    }
    
    boolean isDebugOn = log.isDebugEnabled();
    if (isDebugOn) log.debug("infer all values for key: " + _key);
    
    List<Object> values = new ArrayList<Object>(16);
    
    /* the model returns a presorted set of candidates */
    Rule[] rules = this.model.candidateRulesForKey(_key);
    if (rules == null) {
      if (isDebugOn) log.debug("=> no candidates for key: " + _key);
      return null;
    }
    
    /* check qualifiers */
    for (int i = 0; i < rules.length; i++) {
      EOQualifier q = rules[i].qualifier();
      
      if (!(q instanceof EOQualifierEvaluation)) {
        log.warn("contained a rule which does not support eval: " + q);
        continue;
      }
      
      if (((EOQualifierEvaluation)q).evaluateWithObject(this)) {
        /* found it! */
        if (isDebugOn) log.debug("=> qualifier matched: " + rules[i]);
        
        RuleAction action = (RuleAction)rules[i].action();
        if (isDebugOn) log.debug("=> fire: " + action);
        Object v = action.fireInContext(this);
        values.add(v);
      }
    }
    
    /* no rule matched */
    if (isDebugOn) log.debug("=> " + values.size() + " rules matched: " + _key);
    return values;
  }
  
  public Object[] valuesForKeyPathWhileTakingSuccessiveValuesForKeyPath
    (String _keyPath, Object[] _values, String _valueKeyPath)
  {
    Object[] results = new Object[_values.length];
    for (int i = 0; i < _values.length; i++) {
      /* take the value */
      this.takeValueForKeyPath(_values[i], _valueKeyPath);
      
      /* calculate the rule value */
      results[i] = this.valueForKeyPath(_keyPath);
    }
    return results;
  }
  
  /* description */
  
  @Override
  public void appendAttributesToDescription(StringBuffer _d) {
    super.appendAttributesToDescription(_d);
    
    if (this.model != null)
      _d.append(" model=" + this.model);
    if (this.storedValues != null)
      _d.append(" values=" + this.storedValues);
  }
}
