/*
 * Copyright (c) 1998-2006 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.relaxng;

import java.io.*;
import java.util.*;
import java.util.logging.*;

import org.xml.sax.*;
import org.xml.sax.helpers.*;

import com.caucho.util.*;
import com.caucho.vfs.*;

import com.caucho.xml.QName;

import com.caucho.log.Log;

import com.caucho.relaxng.program.Item;
import com.caucho.relaxng.program.EmptyItem;

/**
 * JARV verifier implementation
 */
public class VerifierHandlerImpl extends DefaultHandler
  implements VerifierHandler {
  private static final L10N L = new L10N(VerifierHandlerImpl.class);
  protected static final Logger log = Log.open(VerifierHandlerImpl.class);
  
  private SchemaImpl _schema;
  private VerifierImpl _verifier;
  private boolean _hasProgram;

  private boolean _isValid = true;

  private LruCache<Object,Item> _programCache;

  private QName _name;
  private ArrayList<QName> _nameStack = new ArrayList<QName>();
  private String _eltLocation;
  private ArrayList<String> _eltLocationStack = new ArrayList<String>();

  private Item _item;
  private ArrayList<Item> _itemStack = new ArrayList<Item>();

  private Locator _locator;
  
  private boolean _isLogFinest;

  private CharBuffer _text = new CharBuffer();
  private boolean _hasText;
  
  private StartKey _startKey = new StartKey();
  private EndElementKey _endElementKey = new EndElementKey();

  /**
   * Creates the Verifier Handler.
   */
  public VerifierHandlerImpl(SchemaImpl schema, VerifierImpl verifier)
  {
    _schema = schema;
    _programCache = _schema.getProgramCache();
    _verifier = verifier;
  }

  /**
   * Sets the locator.
   */
  public void setDocumentLocator(Locator locator)
  {
    _locator = locator;
  }
  
  /**
   * Sets the error handler
   */
  public void setErrorHandler(ErrorHandler errorHandler)
    throws SAXException
  {
    _verifier.setErrorHandler(errorHandler);
  }

  /**
   * Called when the document starts.
   */
  public void startDocument()
    throws SAXException
  {
    try {
      _nameStack.clear();
      _itemStack.clear();
      _eltLocationStack.clear();

      _name = new QName("#top", "");
      _item = _schema.getStartItem();

      _itemStack.add(_item);

      _eltLocation = getLocation();

      _isLogFinest = log.isLoggable(Level.FINEST);
      _hasText = false;
      _text.clear();
    } catch (Exception e) {
      error(e);
    }
  }

  /**
   * Called when an element starts.
   */
  public void startElement(String uri, String localName,
                           String qName, Attributes attrs)
    throws SAXException
  {
    if (! _isValid)
      return;

    if (_hasText)
      sendText();
    
    if (_isLogFinest)
      log.finest("element start: " + qName);

    try {
      QName parent = _name;
      _nameStack.add(parent);

      String parentLocation = _eltLocation;
      _eltLocationStack.add(parentLocation);
      
      QName name = new QName(qName, uri);
      _name = name;
      _eltLocation = getLocation();

      Item newItem = getStartElement(_item, name);

      if (newItem == null) {
	Item parentItem = _itemStack.get(_itemStack.size() - 1);
	
        if (parent.getName().equals("#top"))
          throw new RelaxException(L.l("<{0}> is an unexpected top-level tag.{1}",
                                       errorNodeName(name, _item, parentItem),
				       errorMessageDetail(_item, parentItem, null, name)));
        else
          throw new RelaxException(L.l("<{0}> is an unexpected tag (parent <{1}> starts at {2}).{3}",
                                       errorNodeName(name, _item, parentItem),
				       parent.getName(), parentLocation,
                                       errorMessageDetail(_item, parentItem, parent.getName(), name)));
      }

      _item = newItem;
      _itemStack.add(newItem);

      Item parentItem = newItem;

      int len = attrs.getLength();
      for (int i = 0; i < len; i++) {
        String attrUri = attrs.getURI(i);
        String attrQName = attrs.getQName(i);
        String value = attrs.getValue(i);
        
        if (_isLogFinest)
          log.finest("attribute: " + attrQName + "=\"" + value + "\"");
        
        name = new QName(attrQName, attrUri);

        if (attrQName.startsWith("xml:")) {
        }
        else if (! _item.allowAttribute(name, value)) {
          throw new RelaxException(L.l("{0}=\"{1}\" is an unexpected attribute in <{2}>.{3}",
                                       attrQName, value, qName,
				       attributeMessageDetail(_item,
							      parentItem,
							      qName, null)));
	}
        else
          _item = _item.setAttribute(name, value);

        if (_item == null)
          _item = EmptyItem.create();
      }

      newItem = _item.attributeEnd();
      if (newItem == null)
        throw new RelaxException(L.l("<{0}> expects more attributes.{1}",
                                     qName, 
				     attributeMessageDetail(_item,
							      parentItem,
							      qName, null)));
      _item = newItem;
    } catch (Exception e) {
      error(e);
    }
  }

  private Item getStartElement(Item item, QName name)
    throws RelaxException
  {
    _startKey.init(item, name);

    Item newItem = null;//_programCache.get(_startKey);

    if (newItem != null) {
      return newItem;
    }
    
    newItem = _item.startElement(name);

    /*
    if (newItem != null)
      _programCache.put(new StartKey(item, name), newItem);
    */

    return newItem;
  }
  
  public void characters(char ch[], int start, int length)
    throws SAXException
  {
    _hasText = true;
    _text.append(ch, start, length);
  }

  public void sendText()
    throws SAXException
  {
    if (! _hasText)
      return;

    _hasText = false;
    String string = _text.toString();
    _text.clear();

    try {
      Item newItem = _item.text(string);

      if (newItem == null) {
	Item parentItem = _itemStack.get(_itemStack.size() - 1);
	
        throw new RelaxException(L.l("The following text is not allowed in this context.\n{0}\n{1}", string,
				     errorMessageDetail(_item, parentItem,
							_name.getName(), null)));
      }

      _item = newItem;                  
    } catch (Exception e) {
      error(e);
    }
  }

  /**
   * Called when an element ends.
   */
  public void endElement(String uri, String localName, String qName)
    throws SAXException
  {
    if (_hasText)
      sendText();
    
    if (! _isValid)
      return;

    if (_isLogFinest)
      log.finest("element end: " + qName);

    QName name = _name;
    QName parent = _nameStack.remove(_nameStack.size() - 1);
    _name = parent;

    Item parentItem = _itemStack.remove(_itemStack.size() - 1);

    String eltOpen = _eltLocation;
    _eltLocation = _eltLocationStack.remove(_eltLocationStack.size() - 1);
    
    try {
      Item nextItem = getEndElement(_item);
      
      if (nextItem == null)
        throw new RelaxException(L.l("<{0}> closed while expecting more elements (open at {1}).{2}",
                                     qName, eltOpen,
				     requiredMessageDetail(_item, parentItem,
							qName, null)));
      
      _item = nextItem;
    } catch (Exception e) {
      error(e);
    }
  }

  private Item getEndElement(Item item)
    throws RelaxException
  {
    _endElementKey.init(item);

    Item newItem = null;//_programCache.get(_endElementKey);

    if (newItem != null) {
      return newItem;
    }
    
    newItem = _item.endElement();

    /*
    if (newItem != null)
      _programCache.put(new EndElementKey(item), newItem);
    */

    return newItem;
  }

  /**
   * Called for errors.
   */
  private void error(SAXException e)
    throws SAXException
  {
    _isValid = false;

    _verifier.error(new SAXParseException(e.getMessage(), _locator));
  }

  /**
   * Called for errors.
   */
  private void error(Exception e)
    throws SAXException
  {
    if (e instanceof RuntimeException)
      throw (RuntimeException) e;
    else if (e instanceof SAXException)
      error((SAXException) e);
    else
      error(new SAXException(e.getMessage(), e));
  }

  /**
   * Returns a string containing the allowed values.
   */
  private String errorNodeName(QName name, Item item, Item parentItem)
  {
    Item currentItem = item;
    
    if (currentItem == null)
      currentItem = parentItem;

    if (currentItem == null)
      return name.toString();

    HashSet<QName> values = new HashSet<QName>();
    currentItem.firstSet(values);

    for (QName value : values) {
      if (! name.getLocalName().equals(value.getLocalName())) {
      }
      else if (name.getPrefix() == null || name.getPrefix().equals("")) {
	return name.getName() + " xmlns=\"" + name.getNamespaceURI() + "\"";
      }
      else {
	return name.getName() + " xmlns:" + name.getPrefix() + "=\"" + name.getNamespaceURI() + "\"";
      }
    }

    return name.getName();
  }

  /**
   * Returns a string containing the allowed values.
   */
  private String errorMessageDetail(Item item, Item parentItem,
				    String parentName, QName qName)
  {
    Item currentItem = item;
    
    if (currentItem == null)
      currentItem = parentItem;

    HashSet<QName> values = new HashSet<QName>();
    currentItem.firstSet(values);

    String expected = null;
    if (values.size() <= 5)
      expected = namesToString(values, parentName, qName,
			       currentItem.allowEmpty());
    
    return syntaxMessage(item, parentItem, parentName, qName, expected);
  }

  /**
   * Returns a string containing the allowed values.
   */
  private String requiredMessageDetail(Item item, Item parentItem,
				       String parentName, QName qName)
  {
    Item currentItem = item;

    if (currentItem == null)
      currentItem = parentItem;

    HashSet<QName> values = new HashSet<QName>();
    currentItem.requiredFirstSet(values);
      
    String expected = null;
    
    if (values.size() <= 5) {
      expected = namesToString(values, parentName, qName,
			       currentItem.allowEmpty());
    }
    
    return syntaxMessage(item, parentItem, parentName, qName, expected);
  }

  /**
   * Returns a string containing the allowed values.
   */
  private String attributeMessageDetail(Item item, Item parentItem,
					String parentName, QName qName)
  {
    Item currentItem = item;

    if (currentItem == null)
      currentItem = parentItem;
    
    String allowed = allowedAttributes(currentItem, qName);
    
    return syntaxMessage(item, parentItem, parentName, qName, allowed);
  }

  /**
   * Returns a string containing the allowed values.
   */
  private String syntaxMessage(Item item, Item parentItem,
			       String parentName, QName qName,
			       String expected)
  {
    String syntaxPrefix;

    if (parentName == null || parentName.equals("#top"))
      syntaxPrefix = "Syntax: ";
    else
      syntaxPrefix = "<" + parentName + "> syntax: ";
    
    String msg = "";

    Item topParent = null;
    for (Item parent = item;
	 parent != null;
	 parent = null) { // parent.getParent()) {
      if (qName != null && parent.allowsElement(qName)) {
	msg = " Check for duplicate and out-of-order tags.";

	if (expected != null)
	  msg += expected + "\n";
	
	msg += "\n";

	String prefix = "Syntax: ";
	if (parent == parentItem)
	  prefix = syntaxPrefix;
	
	msg += prefix + parent.toSyntaxDescription(prefix.length());
	break;
      }
      
      // topParent = parent;
    }

    if (topParent == null || topParent instanceof EmptyItem) {
      topParent = parentItem;

      if (qName != null && topParent.allowsElement(qName)) {
	msg = " Check for duplicate and out-of-order tags.";

	if (expected != null)
	  msg += expected + "\n";
	
	msg += "\n";

	String prefix = syntaxPrefix;
	msg += prefix + topParent.toSyntaxDescription(prefix.length());
      }
    }

    if (msg.equals("")) {
      msg = "";

      if (expected != null)
	msg += expected + "\n";
      
      msg += "\n";

      String prefix = syntaxPrefix;
      msg += prefix + topParent.toSyntaxDescription(prefix.length());
    }

    return msg;
  }

  /**
   * Returns a string containing the allowed values.
   */
  private String requiredValues(Item item, String parentName, QName qName)
  {
    if (item == null)
      return "";

    HashSet<QName> values = new HashSet<QName>();
    item.requiredFirstSet(values);

    return namesToString(values, parentName, qName, item.allowEmpty());
  }

  private String namesToString(HashSet<QName> values,
			       String parentName,
			       QName qName,
			       boolean allowEmpty)
  {
    CharBuffer cb = new CharBuffer();
    if (values.size() > 0) {
      ArrayList<QName> sortedValues = new ArrayList<QName>(values);
      Collections.sort(sortedValues);
      
      for (int i = 0; i < sortedValues.size(); i++) {
        QName name = sortedValues.get(i);

        if (i == 0)
          cb.append("\n\n");
        else if (i == sortedValues.size() - 1)
          cb.append(" or\n");
        else
          cb.append(",\n");

	if (name.getName().equals("#text")) {
	  cb.append("text");
	}
        else if (name.getNamespaceURI() == null || qName == null)
          cb.append("<" + name.getName() + ">");
        else if (qName.getNamespaceURI() != name.getNamespaceURI()) {
          if (name.getPrefix() != null)
            cb.append("<" + name.getName() + " xmlns:" + name.getPrefix() + "=\"" + name.getNamespaceURI() + "\">");
          else
            cb.append("<" + name.getName() + " xmlns=\"" + name.getNamespaceURI() + "\">");
        }
        else
          cb.append("<" + name.getName() + ">");
      }

      if (values.size() == 1)
        cb.append(" is expected");
      else
        cb.append(" are expected");

      if (allowEmpty) {
        if (parentName == null || parentName.equals("#top"))
          cb.append(",\nor the document may end.");
        else
          cb.append(",\nor </" + parentName + "> may close.");
      }
      else
        cb.append(".");
    }
    else if (allowEmpty) {
      if (parentName == null || parentName.equals("#top"))
        cb.append("\n\nThe document is expected to end.");
      else
        cb.append("\n\n</" + parentName + "> is expected to close.");
    }

    return cb.toString();
  }      

  /**
   * Returns a string containing the allowed values.
   */
  private String allowedAttributes(Item item, QName qName)
  {
    if (item == null)
      return "";

    HashSet<QName> values = new HashSet<QName>();
    item.attributeSet(values);

    CharBuffer cb = new CharBuffer();
    if (values.size() > 0) {
      ArrayList<QName> sortedValues = new ArrayList<QName>(values);
      Collections.sort(sortedValues);
      
      for (int i = 0; i < sortedValues.size(); i++) {
        QName name = sortedValues.get(i);

        if (i == 0)
          cb.append("\n\n");
        else if (i == sortedValues.size() - 1)
          cb.append(" or ");
        else
          cb.append(", ");

	String uri = name.getNamespaceURI();
        if (uri == null || uri.equals(""))
          cb.append("'" + name.getName() + "'");
        else if (qName == null || qName.getName().equals(name.getName()))
          cb.append("'" + name.getCanonicalName() + "'");
        else
          cb.append("'" + name.getName() + "'");
      }

      if (values.size() == 1)
        cb.append(" is expected.");
      else
        cb.append(" are expected.");
    }

    return cb.toString();
  }      

  /**
   * Returns the current location.
   */
  private String getLocation()
  {
    if (_locator == null)
      return "";
    else
      return "" + _locator.getLineNumber();
  }
  
  /**
   * Checks if the document was valid.
   * 
   * <p>
   * This method can be only called after this handler receives
   * the <code>endDocument</code> event.
   * 
   * @return
   *		<b>true</b> if the document was valid,
   *		<b>false</b> if not.
   * 
   * @exception IllegalStateException
   *		If this method is called before the endDocument event is dispatched.
   */
  public boolean isValid() throws IllegalStateException
  {
    return _isValid;
  }

  static class StartKey {
    private Item _item;
    private QName _name;
    
    public StartKey(Item item, QName name)
    {
      _item = item;
      _name = name;
    }
    
    public StartKey()
    {
    }

    public void init(Item item, QName name)
    {
      _item = item;
      _name = name;
    }

    public int hashCode()
    {
      return _name.hashCode() + 137 * System.identityHashCode(_item);
    }

    public boolean equals(Object o)
    {
      if (o == this)
        return true;

      if (o.getClass() != StartKey.class)
        return false;

      StartKey key = (StartKey) o;

      return _name.equals(key._name) && _item == key._item;
    }
  }

  static class EndElementKey {
    private Item _item;
    
    public EndElementKey(Item item)
    {
      _item = item;
    }
    
    public EndElementKey()
    {
    }

    public void init(Item item)
    {
      _item = item;
    }

    public int hashCode()
    {
      return 137 + _item.hashCode();
    }

    public boolean equals(Object o)
    {
      if (o == this)
        return true;

      if (o.getClass() != EndElementKey.class)
        return false;

      EndElementKey key = (EndElementKey) o;

      return _item.equals(key._item);
    }
  }
}
