/*
  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.rules;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.eocontrol.EOAndQualifier;
import org.opengroupware.jope.eocontrol.EOBooleanQualifier;
import org.opengroupware.jope.eocontrol.EOQualifier;
import org.opengroupware.jope.foundation.NSClassLookupContext;
import org.opengroupware.jope.foundation.NSJavaRuntime;
import org.opengroupware.jope.foundation.NSObject;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/*
 * RuleModelLoader
 * 
 * Loads a RuleModel from an XML file.
 * 
 * Sample:
 *   <?xml version="1.0"?>
 *   <model version="1.0">
 *   
 *     <rule priority="high">
 *       <qualifier>*true*</qualifier>
 *       <key>color</key>
 *       <value>'green'</value>
 *     </rule>
 *     
 *     <rule priority="high">
 *       <qualifier>*true*</qualifier>
 *       <key>color</key>
 *       <var:value>backgroundColor</var:value>
 *     </rule>
 *     
 *     <rule priority="low">
 *       <qualifier>*true*</qualifier>
 *       <action>color = backgroundColor</action>
 *     </rule>
 *     
 *     <rule>*true* => color = 'green'; high</rule>
 *     
 *   </model>
 *   
 * Tag Aliases:
 *   qualifier - q
 *   action    - a
 *   priority  - p
 */
public class RuleModelLoader extends NSObject {
  protected static final Log log = LogFactory.getLog("JoRuleModelLoader");

  /* statics */

  protected static DocumentBuilderFactory dbf;
  static {
    dbf = DocumentBuilderFactory.newInstance();
    dbf.setNamespaceAware(false); /* we directly deal with prefixes */
    dbf.setCoalescing(true); /* join adjacent texts */
    dbf.setIgnoringComments(true);
  }
  
  /* ivars */
  
  protected NSClassLookupContext classLookup;
  protected RuleParser           ruleParser;
  protected Exception            lastException;
  
  public RuleModelLoader(NSClassLookupContext _clslookup) {
    this.classLookup = _clslookup;
    this.ruleParser  = new RuleParser(_clslookup);
  }
  public RuleModelLoader() {
    this(NSClassLookupContext.NSSystemClassLookupContext);
  }
  
  /* accessors */
  
  public Exception lastException() {
    return this.lastException;
  }
  
  public void clear() {
    this.lastException = null;
  }

  /* loading the model */
  
  protected RuleModel loadModelFromElement(Element _node) {
    NodeList   children  = _node.getElementsByTagName("rule");
    int        ruleCount = children != null ? children.getLength() : 0;
    List<Rule> rules     = new ArrayList<Rule>(ruleCount);

    /* load regular rules */
    
    for (int i = 0; i < ruleCount; i++) {
      Rule rule = this.loadRuleFromElement((Element)children.item(i));
      if (rule == null) {
        log.error("got no rule for element: " + children.item(i));
        continue;
      }
      
      rules.add(rule);
    }
    
    /* load multi rules */

    if ((children  = _node.getElementsByTagName("multirule")) != null) {
      ruleCount = children.getLength();
      
      for (int i = 0; i < ruleCount; i++) {
        Rule[] mrules = 
          this.loadMultiRulesFromElement((Element)children.item(i));
        
        if (mrules == null) continue;
        
        for (int j = 0; j < mrules.length; j++)
          rules.add(mrules[j]);
      }
    }
    
    return new RuleModel(rules.toArray(new Rule[0]));
  }
  
  protected RuleModel loadModelFromDocument(Document _doc) {
    return this.loadModelFromElement(_doc.getDocumentElement());
  }
  
  public Rule loadRuleFromElement(Element _node) {
    if (_node == null) return null;
    
    boolean debugOn = log.isDebugEnabled();
    
    /* try to load structured rule setup (tags for rule definition) */
    
    EOQualifier sq = this.loadQualifiersOfRule(_node);
    RuleAction  sa = this.loadActionOfRule(_node);
    Integer     sp = this.loadPriorityOfRule(_node);
    String      sk = null;
    if (sa == null) {
      sk = this.joinTrimmedTextsOfElements
        (_node.getElementsByTagName("key"),
         "." /* build keypath (useless) */);
    }
    
    /* check whether its structured */
    
    if (sq == null && sa == null && sk == null) {
      /* not structured */
      String s = _node.getTextContent();
      if (s == null || s.length() == 0) {
        log.error("found rule tag w/o recognisable content");
        return null;
      }
      
      if (sp != null) s = s + "; " + sp; // TODO: check for dups
      Rule rule = this.ruleParser.parseRule(s);
      if (rule == null)
        log.error("could not parse rule: '" + s + "'");
      return rule;
    }
    
    /* treat as a structured rule */
    
    if (debugOn)
      log.debug("load structured rule, sk: " + sk + ", sa: " + sa);
    
    if (sk != null && sa != null)
      log.warn("rule has both, 'key' and 'action' tags. Using 'action'.");
    else if (sk != null) {
      sa = this.loadSplitRuleAction(_node, sk);
      if (debugOn) log.debug("  parsed split rule: " + sa);
    }
    
    if (sq == null) /* no qualifier given, using *true* */
      sq = EOBooleanQualifier.trueQualifier;
    
    if (sp == null) /* no priority given, use normal */
      sp = new Integer(RuleParser.RULE_PRIORITY_NORMAL);
    
    return new Rule(sq, sa, sp.intValue());
  }
  
  public Rule[] loadMultiRulesFromElement(Element _node) {
    if (_node == null) return null;
    
    /* try to load structured rule setup (tags for rule definition) */
    
    RuleAction  sa = this.loadActionOfRule(_node);
    Integer     sp = this.loadPriorityOfRule(_node);
    String      sk = null;
    if (sa == null) {
      sk = this.joinTrimmedTextsOfElements
        (_node.getElementsByTagName("key"),
         "." /* build keypath (useless) */);
    }
    
    /* treat as a structured rule */
    
    if (sk != null && sa != null)
      log.warn("multirule has both, 'key' and 'action' tags. Using 'action'.");
    else if (sk != null)
      sa = this.loadSplitRuleAction(_node, sk);
    
    if (sp == null) /* no priority given, use normal */
      sp = new Integer(RuleParser.RULE_PRIORITY_NORMAL);
    
    /* walk over qualifiers, generate a rule for each */

    NodeList children  = _node.getElementsByTagName("qualifier");
    if (children == null) children  = _node.getElementsByTagName("q");
    
    if (children == null || children.getLength() == 0) {
      log.warn("multirule tag has no qualifiers?");
      return null;
    }

    Rule[] qs = new Rule[children.getLength()];
    for (int i = 0; i < qs.length; i++) {
      EOQualifier q = EOQualifier.qualifierWithQualifierFormat
        (children.item(i).getTextContent(), noArgs);
      
      qs[i] = new Rule(q, sa, sp.intValue());
    }
    return qs;
  }
  
  protected static Object[] noArgs = { };
  
  public EOQualifier loadQualifiersOfRule(Element _node) {
    if (_node == null) return null;
    
    NodeList children  = _node.getElementsByTagName("qualifier");
    if (children == null) children  = _node.getElementsByTagName("q");
    
    if (children == null || children.getLength() == 0)
      return null; /* has no qualifier subelements */
    
    EOQualifier[] qs = new EOQualifier[children.getLength()];
    for (int i = 0; i < qs.length; i++) {
      qs[i] = EOQualifier.qualifierWithQualifierFormat
        (children.item(i).getTextContent(), noArgs);
    }
    
    if (qs.length == 1)
      return qs[0];
    
    return new EOAndQualifier(qs);
  }
  
  public RuleAction loadActionOfRule(Element _node) {
    if (_node == null) return null;
    
    NodeList children = _node.getElementsByTagName("action");
    if (children == null) children  = _node.getElementsByTagName("a");

    if (children == null || children.getLength() == 0)
      return null; /* has no action subelements */
    
    RuleAction[] as = new RuleAction[children.getLength()];
    for (int i = 0; i < as.length; i++) {
      Element child = (Element)children.item(i);
      
      String actionClassName = child.getAttribute("class");
      as[i] = (RuleAction)this.ruleParser.parseAction
        (child.getTextContent(), actionClassName);
    }
    
    return CompoundRuleAction.ruleActionForActionArray(as);
  }
  
  public RuleAction loadSplitRuleAction(Element _node, String _keyPath) {
    if (_node == null) return null;
    
    /* first check for constant values */
    
    NodeList children = _node.getElementsByTagName("value");
    if (children == null || children.getLength() == 0)
      children = _node.getElementsByTagName("v");
    
    if (children != null && children.getLength() > 0) {
      String v = this.joinTrimmedTextsOfElements(children, "");
      // System.err.println("X: " + v + ": " + children);
      return new RuleAssignment(_keyPath, v);
    }
    
    /* check for variable values */

    children = _node.getElementsByTagName("var:value");
    if (children == null || children.getLength() == 0)
      children = _node.getElementsByTagName("var:v");
    
    if (children != null && children.getLength() > 0) {
      String v = this.joinTrimmedTextsOfElements(children, "." /* keypath */);
      return new RuleKeyAssignment(_keyPath, v);
    }
    
    /* didn't find a value */
    log.warn("did not find value tag in rule for keypath: " + _keyPath);
    return null;
  }
  
  public Integer loadPriorityOfRule(Element _node) {
    if (_node == null) return null;
    
    /* check for attribute */
    
    String s = _node.getAttribute("priority");
    if (s != null && s.length() > 0)
      return priorityForString(s);
    
    /* check for subtag */
    
    NodeList children  = _node.getElementsByTagName("priority");
    if (children == null) children  = _node.getElementsByTagName("p");
    
    if (children == null || children.getLength() == 0)
      return null; /* has no action subelements */
    
    if (children.getLength() > 1)
      log.error("multiple priorities given for rule!");
    
    return priorityForString(children.item(0).getTextContent());
  }
  
  public static Integer priorityForString(String _s) {
    if (_s == null)
      return null;
    
    if (Character.isDigit(_s.charAt(0)))
      return new Integer(NSJavaRuntime.intValueForObject(_s));
    
    return RuleParser.parsePriority(_s);
  }
  
  
  /* support */
  
  protected Exception newModelLoadingException(String _reason) {
    // TODO: improve error handling
    return new Exception(_reason);
  }
  
  protected void addError(String _reason) {
    log.error(_reason);
    this.lastException = this.newModelLoadingException(_reason);
  }
  protected void addError(String _reason, Exception _e) {
    log.error(_reason, _e);
    
    // TODO: wrap exception
    this.lastException = _e;
  }
  
  public RuleModel loadModelFromURL(URL _url) {
    boolean isDebugOn = log.isDebugEnabled();
    if (isDebugOn) log.debug("loading model from URL: " + _url);
    
    if (_url == null) {
      this.addError("missing URL parameter for loading model");
      return null;
    }
    
    /* instantiate document builder */
    
    DocumentBuilder db;
    try {
       db = dbf.newDocumentBuilder();
       if (isDebugOn) log.debug("  using DOM document builder:" + db);
    }
    catch (ParserConfigurationException e) {
      this.addError("failed to create docbuilder for parsing URL: " + _url, e);
      return null;
    }
    
    /* load DOM */
    
    Document doc;
    try {
      doc = db.parse(_url.openStream(), _url.toString());
      if (isDebugOn) log.debug("  parsed DOM: " + doc);
    }
    catch (SAXException e) {
      this.addError("XML error when loading model resource: " + _url, e);
      return null;
    }
    catch (IOException e) {
      this.addError("IO error when loading model resource: " + _url, e);
      return null;
    }
    
    /* transform DOM into model */

    RuleModel model = this.loadModelFromDocument(doc);
    
    if (isDebugOn && model != null) {
      log.debug("  model: " + model);
      log.debug("finished model from URL: " + _url);
    }
    if (model == null)
      log.info("failed loading model from URL: " + _url);
    
    return model;
  }

  protected String joinTrimmedTextsOfElements(NodeList _nodes, String _sep) {
    if (_nodes == null || _nodes.getLength() == 0)
      return null;
    
    StringBuffer sb = new StringBuffer(256);
    boolean isFirst = true;
    for (int i = 0; i < _nodes.getLength(); i++) {
      Element node = (Element)_nodes.item(i);
      node.normalize();
      
      String txt = node.getTextContent();
      if (txt == null) continue;
      txt = txt.trim();
      if (txt.length() == 0) continue;
      
      if (isFirst)
        isFirst = false;
      else if (_sep != null)
        sb.append(_sep);
      
      sb.append(txt);
    }
    return sb.length() > 0 ? sb.toString() : null;
  }
}
