package com.thaiopensource.validate.mns;

import com.thaiopensource.util.Localizer;
import com.thaiopensource.util.PropertyMap;
import com.thaiopensource.validate.Schema;
import com.thaiopensource.validate.ValidateProperty;
import com.thaiopensource.validate.Validator;
import com.thaiopensource.xml.util.Name;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.DTDHandler;
import org.xml.sax.ErrorHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

import java.util.Hashtable;
import java.util.Stack;

class ValidatorImpl extends DefaultHandler implements Validator {
  static final Name OWNER_NAME = new Name("http://www.thaiopensource.com/ns/mns/instance", "owner");
  private SchemaImpl.Mode currentMode;
  private int laxDepth = 0;
  private final ErrorHandler eh;
  private final PropertyMap properties;
  private Locator locator;
  private Subtree subtrees = null;
  private final Hashset attributeNamespaces = new Hashset();
  private PrefixMapping prefixMapping = null;
  private final Localizer localizer = new Localizer(ValidatorImpl.class);
  private final Hashtable validatorCache = new Hashtable();

  static private class Subtree {
    final Subtree parent;
    final Validator validator;
    final Schema schema;
    final Hashset coveredNamespaces;
    final ElementsOrAttributes prune;
    final SchemaImpl.Mode parentMode;
    final int parentLaxDepth;
    final Stack context = new Stack();
    final ContextMap contextMap;

    Subtree(Hashset coveredNamespaces, ContextMap contextMap,
            ElementsOrAttributes prune, Validator validator,
            Schema schema, SchemaImpl.Mode parentMode, int parentLaxDepth, Subtree parent) {
      this.coveredNamespaces = coveredNamespaces;
      this.contextMap = contextMap;
      this.prune = prune;
      this.validator = validator;
      this.schema = schema;
      this.parentMode = parentMode;
      this.parentLaxDepth = parentLaxDepth;
      this.parent = parent;
    }
  }

  static private class PrefixMapping {
    final String prefix;
    final String uri;
    final PrefixMapping parent;

    PrefixMapping(String prefix, String uri, PrefixMapping parent) {
      this.prefix = prefix;
      this.uri = uri;
      this.parent = parent;
    }
  }

  ValidatorImpl(SchemaImpl.Mode mode, PropertyMap properties) {
    this.currentMode = mode;
    this.properties = properties;
    this.eh = properties.get(ValidateProperty.ERROR_HANDLER);
  }

  public void setDocumentLocator(Locator locator) {
    this.locator = locator;
  }

  public void characters(char ch[], int start, int length)
          throws SAXException {
    for (Subtree st = subtrees; wantsEvent(st); st = st.parent)
      st.validator.getContentHandler().characters(ch, start, length);
  }

  public void ignorableWhitespace(char ch[], int start, int length)
          throws SAXException {
    for (Subtree st = subtrees; wantsEvent(st); st = st.parent)
      st.validator.getContentHandler().ignorableWhitespace(ch, start, length);
  }

  private SchemaImpl.Mode getMode() {
    if (subtrees != null) {
      SchemaImpl.Mode mode = (SchemaImpl.Mode)subtrees.contextMap.get(subtrees.context);
      if (mode != null)
        return mode;
    }
    return currentMode;
  }

  public void startElement(String uri, String localName,
                           String qName, Attributes attributes)
          throws SAXException {
    if (namespaceCovered(uri))
      subtrees.context.push(new Name(uri, localName));
    else {
      SchemaImpl.Mode mode = getMode();
      SchemaImpl.ElementAction elementAction = mode.getElementAction(uri);
      if (elementAction == null) {
        if (laxDepth == 0 && !mode.getLax().containsElements())
          error("element_undeclared_namespace", uri);
        laxDepth++;
      }
      else {
        subtrees = new Subtree(elementAction.getCoveredNamespaces(),
                               elementAction.getContextMap(),
                               elementAction.getPrune(),
                               createValidator(elementAction.getSchema()),
                               elementAction.getSchema(),
                               currentMode,
                               laxDepth,
                               subtrees);
        subtrees.context.push(new Name(uri, localName));
        currentMode = elementAction.getMode();
        laxDepth = 0;
        startSubtree(subtrees.validator.getContentHandler());
      }
    }
    for (Subtree st = subtrees; wantsEvent(st); st = st.parent) {
      Attributes prunedAtts;
      if (st.prune.containsAttributes())
        prunedAtts = new NamespaceFilteredAttributes(uri, true, attributes);
      else
        prunedAtts = attributes;
      st.validator.getContentHandler().startElement(uri, localName, qName, prunedAtts);
    }
    for (int i = 0, len = attributes.getLength(); i < len; i++) {
      String ns = attributes.getURI(i);
      if (!ns.equals("")
          && !ns.equals(uri)
          && !namespaceCovered(ns)
          && !attributeNamespaces.contains(ns)) {
        attributeNamespaces.add(ns);
        validateAttributes(ns, attributes);
      }
    }
    attributeNamespaces.clear();
  }

  private boolean namespaceCovered(String ns) {
    return (laxDepth == 0
            && subtrees != null
            && subtrees.coveredNamespaces.contains(ns));
  }

  private boolean wantsEvent(Subtree st) {
    return st != null && (!st.prune.containsElements() || (laxDepth == 0 && st == subtrees));
  }

  private void validateAttributes(String ns, Attributes attributes) throws SAXException {
    SchemaImpl.Mode mode = getMode();
    Schema attributesSchema = mode.getAttributesSchema(ns);
    if (attributesSchema == null) {
      if (!mode.getLax().containsAttributes())
        error("attributes_undeclared_namespace", ns);
      return;
    }
    Validator validator = createValidator(attributesSchema);
    ContentHandler ch = validator.getContentHandler();
    startSubtree(ch);
    ch.startElement(OWNER_NAME.getNamespaceUri(), OWNER_NAME.getLocalName(), OWNER_NAME.getLocalName(),
                    new NamespaceFilteredAttributes(ns, false, attributes));
    ch.endElement(OWNER_NAME.getNamespaceUri(), OWNER_NAME.getLocalName(), OWNER_NAME.getLocalName());
    endSubtree(ch);
    releaseValidator(attributesSchema, validator);
  }

  private void startSubtree(ContentHandler ch) throws SAXException {
    if (locator != null)
      ch.setDocumentLocator(locator);
    ch.startDocument();
    for (PrefixMapping pm = prefixMapping; pm != null; pm = pm.parent)
      ch.startPrefixMapping(pm.prefix, pm.uri);
  }

  private void endSubtree(ContentHandler ch) throws SAXException {
    for (PrefixMapping pm = prefixMapping; pm != null; pm = pm.parent)
      ch.endPrefixMapping(pm.prefix);
    ch.endDocument();
  }

  public void endElement(String uri, String localName, String qName)
          throws SAXException {
    for (Subtree st = subtrees; wantsEvent(st); st = st.parent)
      st.validator.getContentHandler().endElement(uri, localName, qName);
    if (laxDepth > 0)
      laxDepth--;
    else if (!subtrees.context.empty()) {
      subtrees.context.pop();
      if (subtrees.context.empty()) {
        endSubtree(subtrees.validator.getContentHandler());
        releaseValidator(subtrees.schema, subtrees.validator);
        currentMode = subtrees.parentMode;
        laxDepth = subtrees.parentLaxDepth;
        subtrees = subtrees.parent;
      }
    }
  }

  private Validator createValidator(Schema schema) {
     Stack stack = (Stack)validatorCache.get(schema);
     if (stack == null) {
       stack = new Stack();
       validatorCache.put(schema, stack);
     }
     if (stack.empty())
       return schema.createValidator(properties);
     return (Validator)stack.pop();
   }

   private void releaseValidator(Schema schema, Validator validator) {
     validator.reset();
     ((Stack)validatorCache.get(schema)).push(validator);
   }

  public void endDocument()
          throws SAXException {
  }

  public void startPrefixMapping(String prefix, String uri)
          throws SAXException {
    super.startPrefixMapping(prefix, uri);
    prefixMapping = new PrefixMapping(prefix, uri, prefixMapping);
  }

  public void endPrefixMapping(String prefix)
          throws SAXException {
    super.endPrefixMapping(prefix);
    prefixMapping = prefixMapping.parent;
  }

  public void reset() {
    subtrees = null;
    locator = null;
  }

  public ContentHandler getContentHandler() {
    return this;
  }

  public DTDHandler getDTDHandler() {
    return null;
  }

  private void error(String key, String arg) throws SAXException {
    eh.error(new SAXParseException(localizer.message(key, arg), locator));
  }
}
