package org.opengroupware.jope.appserver.templates;

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

import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.appserver.WOAssociation;
import org.opengroupware.jope.foundation.NSPropertyListParser;

// TODO: detect 'null' and 'nil' special values in dictionaries?
//       - need to override NSPropertyListParser constant handling for this?

/*
 * WODParser
 * 
 * Note: this is a straight port of the ObjC parser and therefore somewhat
 *       clumsy.
 */

public class WODParser extends NSPropertyListParser {
  
  protected WODParserHandler   handler;
  protected Map<String,Object> entries;
  
  public WODParser() {
    super();
    
    /* overwrite logging */
    this.log = LogFactory.getLog("WOTemplates");
    this.isDebugOn = this.log.isDebugEnabled();
    
    this.entries = new HashMap<String,Object>(32);
  }
  
  /* top-level parsing */
  
  public Object parse() {
    if (!this.handler.willParseDeclarationData(this, this.buffer))
      return null;
    
    while (this._parseWodEntry() != null)
      ;
    
    /* we need to copy, otherwise we just keep a ref to the mutable type */
    Object result = new HashMap<String,Object>(this.entries);
    
    this.resetTransient();
    
    if (this.lastException != null) {
      this.handler.failedParsingDeclarationData
        (this, this.buffer, this.lastException);
      return null;
    }
    
    this.handler.finishedParsingDeclarationData
      (this, this.buffer, this.entries);
    
    return result;
  }
  
  /* setting input */
  
  public void resetTransient() {
    super.resetTransient();
    this.entries.clear();
  }
  
  public void setHandler(WODParserHandler _handler) {
    this.handler = _handler;
  }
  public WODParserHandler handler() {
    return this.handler;
  }
    
  /* parsing */
  
  protected Object _parseWodEntry() {
    /* parses: A: WOString { name = "hello"; }; */
    
    if (!this._skipComments())
      return null; /* EOF, read all entries */
    
    /* element name */
    String elementName = this._parseIdentifier();
    if (elementName == null) {
      this.addException("expected element name");
      return null;
    }
    if (this.isDebugOn) this.log.debug("parse wod entry: " + elementName);

    if (!this._skipComments()) {
      this.addException("EOF after reading element name");
      return null;
    }
    
    /* element/component separator */
    if (this.idx >= this.len || this.buffer[this.idx] != ':') {
      this.addException("expected ':' after element name (" + elementName +")");
      return null;
    }
    this.idx += 1; /* skip ':' */

    if (!this._skipComments()) {
      this.addException("EOF after reading element name and colon");
      return null;
    }

    /* component name */
    String componentName = this._parseIdentifier();
    if (componentName == null) {
      this.addException("expected component name");
      return null;
    }
    if (this.isDebugOn) this.log.debug("  component: " + componentName);
    
    /* configuration */
    
    Map<String,WOAssociation> config = this._parseWodConfig();

    /* read trailing ';' if available */
    if (this._skipComments()) {
      if (this.idx < this.len && this.buffer[this.idx] == ';')
        this.idx++; /* skip ';' */
    }
    
    /* create an entry */
    
    if (this.entries.containsKey(elementName))
      this.log.error("duplicate element: " + elementName);
    
    Object def = this.handler.makeDefinitionForComponentNamed
      (this, elementName /* LHS */, config, componentName /* RHS */);
    
    if (this.isDebugOn) this.log.debug("  element: " + def);
    
    if (def != null && elementName != null)
      this.entries.put(elementName, def);
    
    return def;
  }
  
  protected Map<String,WOAssociation> _parseWodConfig() {
    /* This is very similiar to a dictionary, but only allows identifiers for
     * keys and it does allow associations as values.
     */

    /* skip comments and spaces */
    if (!this._skipComments()) {
      /* EOF reached during comment-skipping */
      this.addException("did not find element configuration (expected '{')");
      return null;
    }
    
    if (this.buffer[this.idx] != '{') { /* it's not a dict that follows */
      this.addException("did not find element configuration (expected '{')");
      return null;
    }
    
    if (this.isDebugOn) this.log.debug("  parsing bindings ...");
    
    this.idx += 1; /* skip '{' */
    
    if (!this._skipComments()) {
      this.addException("element configuration was not closed (expected '}')");
      return null; /* EOF */
    }
    
    if (this.buffer[this.idx] == '}') { /* an empty dictionary */
      this.idx += 1; /* skip the '}' */
      return new HashMap<String, WOAssociation>(0); // TODO: add an empty-map obj?
    }
    
    Map<String, WOAssociation> result = new HashMap<String, WOAssociation>(16);
    boolean didFail = false;
    
    do {
      if (!this._skipComments()) {
        this.addException("element configuration was not closed (expected '}')");
        didFail = true;
        break; /* unexpected EOF */
      }
      
      if (this.buffer[this.idx] == '}') { /* dictionary closed */
        this.idx += 1; /* skip the '}' */
        break;
      }
      
      /* read key identifier */
      String key = this._parseIdentifier();
      if (key == null) { /* syntax error */
        if (this.lastException == null)
          this.addException("got nil-key in element configuration ..");
        didFail = true;
        break;
      }
      
      /* The following parses:  (comment|space)* '=' (comment|space)* */
      if (!this._skipComments()) {
        this.addException("expected '=' after key in element configuration");
        didFail = true;
        break; /* unexpected EOF */
      }
      /* now we need a '=' assignment */
      if (this.buffer[this.idx] != '=') {
        this.addException("expected '=' after key in element configuration");
        didFail = true;
        break;
      }
      this.idx += 1; /* skip '=' */
      if (!this._skipComments()) {
        this.addException("expected value after key '=' in element config");
        didFail = true;
        break; /* unexpected EOF */
      }
      
      /* read value property */
      WOAssociation value = this._parseAssociationProperty();
      if (this.lastException != null) {
        didFail = true;
        break;
      }
      
      if (this.isDebugOn)
        this.log.debug("    parsed binding: " + key + " <= " + value);
      result.put(key, value);
      
      /* read trailing ';' if available */
      if (!this._skipComments()) {
        this.addException("element config was not closed (expected '}')");
        didFail = true;
        break; /* unexpected EOF */
      }
      if (this.buffer[this.idx] == ';')
        this.idx += 1; /* skip ';' */
      else { /* no ';' at end of pair, only allowed at end of dictionary */
        if (this.buffer[this.idx] != '}') { /* dictionary wasn't closed */
          this.addException("key-value pair without ';' at the end");
          didFail = true;
          break;
        }
      }
    }
    while ((this.idx < this.len) && (result != null) && !didFail);
    
    if (this.isDebugOn) this.log.debug("  parsed bindings: " + result);
    return didFail ? null : result;
  }
  
  protected WOAssociation _parseAssociationProperty() {
    boolean valueProperty = true;
    Object  result = null;
    
    if (!this._skipComments())
      return null; /* EOF */
    
    char c = this.buffer[this.idx];
    switch (c) {
      case '"': /* quoted string */
        result = this._parseQString();
        break;
        
      case '{': /* dictionary */
        result = this._parseDict();
        break;
        
      case '(': /* array */
        result = this._parseArray();
        break;
        
      case '<': /* data */
        result = this._parseData();
        break;
        
      default:
        if (Character.isDigit(c) || (c == '-')) {
          String digitPath = this._parseKeyPath();
          result = _parseDigitPath(digitPath);
          valueProperty = true;
        }
        else if (_isIdChar(this.buffer[this.idx])) {
          valueProperty = false;
          
          if (c == 'Y' || c == 'N' || c == 't' || c == 'f') {
            /* parse YES and NO, true and false */
            if (_ucIsEqual(this.buffer, this.idx, "YES") && 
                _isBreakChar(this.buffer[this.idx + 3])) {
              result = Boolean.TRUE;
              valueProperty = true;
              this.idx += 3;
            }
            else if (_ucIsEqual(this.buffer, this.idx, "NO") && 
                     _isBreakChar(this.buffer[this.idx + 2])) {
              result = Boolean.FALSE;
              valueProperty = true;
              this.idx += 2;
            }
            else if (_ucIsEqual(this.buffer, this.idx, "true") && 
                     _isBreakChar(this.buffer[this.idx + 4])) {
              result = Boolean.TRUE;
              valueProperty = true;
              this.idx += 4;
            }
            else if (_ucIsEqual(this.buffer, this.idx, "false") && 
                     _isBreakChar(this.buffer[this.idx + 5])) {
              result = Boolean.FALSE;
              valueProperty = true;
              this.idx += 5;
            }
          }          
          if (!valueProperty)
            result = this._parseKeyPath();
        }
        else {
          this.addException("invalid char");
        }
        break;
    }
    
    if (this.lastException != null)
      return null;
    
    if (result == null)
      this.addException("error in property value");
    
    return valueProperty
      ? this.handler.makeAssociationWithValue(this, result)
      : this.handler.makeAssociationWithKeyPath(this, (String)result);
  }
}
