package com.thaiopensource.validate.nrl;

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.Enumeration;
import java.util.Hashtable;
import java.util.Stack;
import java.util.Vector;

class ValidatorImpl extends DefaultHandler implements Validator {
  static final Name OWNER_NAME = new Name("http://www.thaiopensource.com/validate/nrl/instance", "owner");
  private static final String NO_NS = "\0";
  private final ErrorHandler eh;
  private final PropertyMap properties;
  private Locator locator;
  private Section currentSection;
  private PrefixMapping prefixMapping = null;
  private final Hashtable validatorHandlerCache = new Hashtable();
  private final Localizer localizer = new Localizer(ValidatorImpl.class);
  private final Hashset noResultActions = new Hashset();
  private final Hashtable attributeNamespaceIndexSets = new Hashtable();
  private final Vector activeHandlersAttributeIndexSets = new Vector();
  private final Hashset attributeSchemas = new Hashset();
  private boolean attributeNamespaceRejected;
  private Attributes filteredAttributes;
  private final Mode startMode;

  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;
    }
  }

  private class Section implements SectionState {
    final Section parent;
    /**
     * Namespace of this section.  Empty string for absent.
     */
    final String ns;
    /**
     * Number of open elements in this section.
     */
    int depth = 0;
    /**
     * List of the Validators rooted in this section
     */
    final Vector validators = new Vector();
    final Vector schemas = new Vector();
    /**
     * List of the ContentHandlers that want to see the elements in this section
     */
    final Vector activeHandlers = new Vector();
    final Vector activeHandlersAttributeModeUsage = new Vector();
    final Vector attributeValidationModeUsages = new Vector();
    /**
     * List of Programs saying what to do with child sections
     */
    final Vector childPrograms = new Vector();
    final Stack context = new Stack();
    boolean contextDependent = false;
    int attributeProcessing = Mode.ATTRIBUTE_PROCESSING_NONE;

    Section(String ns, Section parent) {
      this.ns = ns;
      this.parent = parent;
    }

    public void addChildMode(ModeUsage modeUsage, ContentHandler handler) {
      childPrograms.addElement(new Program(modeUsage, handler));
      if (modeUsage.isContextDependent())
        contextDependent = true;
    }

    public void addValidator(Schema schema, ModeUsage modeUsage) {
      schemas.addElement(schema);
      Validator validator = createValidator(schema);
      validators.addElement(validator);
      activeHandlers.addElement(validator.getContentHandler());
      activeHandlersAttributeModeUsage.addElement(modeUsage);
      attributeProcessing = Math.max(attributeProcessing,
                                     modeUsage.getAttributeProcessing());
      childPrograms.addElement(new Program(modeUsage, validator.getContentHandler()));
      if (modeUsage.isContextDependent())
        contextDependent = true;
    }

    public void addActiveHandler(ContentHandler handler, ModeUsage attributeModeUsage) {
      activeHandlers.addElement(handler);
      activeHandlersAttributeModeUsage.addElement(attributeModeUsage);
      attributeProcessing = Math.max(attributeProcessing,
                                     attributeModeUsage.getAttributeProcessing());
      if (attributeModeUsage.isContextDependent())
        contextDependent = true;
    }

    public void addAttributeValidationModeUsage(ModeUsage modeUsage) {
      int ap = modeUsage.getAttributeProcessing();
      if (ap != Mode.ATTRIBUTE_PROCESSING_NONE) {
        attributeValidationModeUsages.addElement(modeUsage);
        attributeProcessing = Math.max(ap, attributeProcessing);
        if (modeUsage.isContextDependent())
          contextDependent = true;
      }
    }

    public void reject() throws SAXException {
      if (eh != null)
        eh.error(new SAXParseException(localizer.message("reject_element", ns),
                                       locator));
    }

  }

  static private class Program {
    final ModeUsage modeUsage;
    final ContentHandler handler;

    Program(ModeUsage modeUsage, ContentHandler handler) {
      this.modeUsage = modeUsage;
      this.handler = handler;
    }
  }

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

  private void initCurrentSection() {
    currentSection = new Section(NO_NS, null);
    currentSection.addChildMode(new ModeUsage(startMode, startMode), null);
  }

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

  public void characters(char ch[], int start, int length)
          throws SAXException {
    for (int i = 0, len = currentSection.activeHandlers.size(); i < len; i++)
      ((ContentHandler)(currentSection.activeHandlers.elementAt(i))).characters(ch, start, length);

  }

  public void ignorableWhitespace(char ch[], int start, int length)
          throws SAXException {
    for (int i = 0, len = currentSection.activeHandlers.size(); i < len; i++)
      ((ContentHandler)(currentSection.activeHandlers.elementAt(i))).ignorableWhitespace(ch, start, length);
  }

  public void startElement(String uri, String localName,
                           String qName, Attributes attributes)
          throws SAXException {
    if (!uri.equals(currentSection.ns))
      startSection(uri);
    currentSection.depth++;
    if (currentSection.contextDependent)
      currentSection.context.push(localName);
    boolean transformAttributes = processAttributes(attributes);
    for (int i = 0, len = currentSection.activeHandlers.size(); i < len; i++) {
      ContentHandler handler = (ContentHandler)(currentSection.activeHandlers.elementAt(i));
      handler.startElement(uri, localName, qName,
                           transformAttributes
                           ? filterAttributes((IntSet)activeHandlersAttributeIndexSets.elementAt(i),
                                              attributes)
                           : attributes);
    }
  }

  private static Attributes filterAttributes(IntSet indexSet, Attributes attributes) {
    if (indexSet.size() == attributes.getLength())
      return attributes;
    return new FilteredAttributes(indexSet, attributes);
  }

  private boolean processAttributes(Attributes attributes) throws SAXException {
    if (currentSection.attributeProcessing == Mode.ATTRIBUTE_PROCESSING_NONE
        || attributes.getLength() == 0)
      return false;
    attributeNamespaceIndexSets.clear();
    for (int i = 0, len = attributes.getLength(); i < len; i++) {
      String ns = attributes.getURI(i);
      IntSet indexSet = (IntSet)attributeNamespaceIndexSets.get(ns);
      if (indexSet == null) {
        indexSet = new IntSet();
        attributeNamespaceIndexSets.put(ns, indexSet);
      }
      indexSet.add(i);
    }
    if (currentSection.attributeProcessing == Mode.ATTRIBUTE_PROCESSING_QUALIFIED
        && attributeNamespaceIndexSets.size() == 1
        && attributeNamespaceIndexSets.get("") != null)
      return false;
    Vector handlerModes = currentSection.activeHandlersAttributeModeUsage;
    activeHandlersAttributeIndexSets.setSize(handlerModes.size());
    for (int i = 0, len = handlerModes.size(); i < len; i++)
      activeHandlersAttributeIndexSets.setElementAt(new IntSet(), i);
    boolean transform = false;
    Vector validationModes = currentSection.attributeValidationModeUsages;
    for (Enumeration e = attributeNamespaceIndexSets.keys(); e.hasMoreElements();) {
      String ns = (String)e.nextElement();
      IntSet indexSet = (IntSet)attributeNamespaceIndexSets.get(ns);
      attributeSchemas.clear();
      filteredAttributes = null;
      attributeNamespaceRejected = false;
      for (int i = 0, len = handlerModes.size(); i < len; i++) {
        ModeUsage modeUsage = (ModeUsage)handlerModes.elementAt(i);
        AttributeActionSet actions = processAttributeSection(modeUsage, ns, indexSet, attributes);
        if (actions.getAttach())
          ((IntSet)activeHandlersAttributeIndexSets.get(i)).addAll(indexSet);
        else
          transform = true;
      }
      for (int i = 0, len = validationModes.size(); i < len; i++) {
        ModeUsage modeUsage = (ModeUsage)validationModes.elementAt(i);
        processAttributeSection(modeUsage, ns, indexSet, attributes);
      }
    }
    return transform;
  }

  private AttributeActionSet processAttributeSection(ModeUsage modeUsage,
                                                     String ns,
                                                     IntSet indexSet,
                                                     Attributes attributes)
          throws SAXException {
    Mode mode = modeUsage.getMode(currentSection.context);
    AttributeActionSet actions = mode.getAttributeActions(ns);
    if (actions.getReject() && !attributeNamespaceRejected) {
      attributeNamespaceRejected = true;
      if (eh != null)
        eh.error(new SAXParseException(localizer.message("reject_attribute", ns),
                                       locator));
    }
    Schema[] schemas = actions.getSchemas();
    for (int j = 0; j < schemas.length; j++) {
      if (attributeSchemas.contains(schemas[j]))
        continue;
      attributeSchemas.add(schemas[j]);
      if (filteredAttributes == null)
        filteredAttributes = filterAttributes(indexSet, attributes);
      validateAttributes(schemas[j], filteredAttributes);
    }
    return actions;
  }

  private void validateAttributes(Schema schema, Attributes attributes) throws SAXException {
    Validator validator = createValidator(schema);
    ContentHandler ch = validator.getContentHandler();
    initHandler(ch);
    ch.startElement(OWNER_NAME.getNamespaceUri(), OWNER_NAME.getLocalName(), OWNER_NAME.getLocalName(), attributes);
    ch.endElement(OWNER_NAME.getNamespaceUri(), OWNER_NAME.getLocalName(), OWNER_NAME.getLocalName());
    cleanupHandler(ch);
    releaseValidator(schema, validator);
  }

  private void startSection(String uri) throws SAXException {
    Section section = new Section(uri, currentSection);
    Vector childPrograms = currentSection.childPrograms;
    noResultActions.clear();
    for (int i = 0, len = childPrograms.size(); i < len; i++) {
      Program program = (Program)childPrograms.elementAt(i);
      ActionSet actions = program.modeUsage.getMode(currentSection.context).getElementActions(uri);
      ResultAction resultAction = actions.getResultAction();
      if (resultAction != null)
        resultAction.perform(program.handler, section);
      NoResultAction[] nra = actions.getNoResultActions();
      for (int j = 0; j < nra.length; j++) {
        NoResultAction tem = nra[j];
        if (!noResultActions.contains(tem)) {
          nra[j].perform(section);
          noResultActions.add(tem);
        }
      }
    }
    for (int i = 0, len = section.validators.size(); i < len; i++)
      initHandler(((Validator)section.validators.elementAt(i)).getContentHandler());
    currentSection = section;
  }

  private void initHandler(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);
  }

  public void endElement(String uri, String localName, String qName)
          throws SAXException {
    for (int i = 0, len = currentSection.activeHandlers.size(); i < len; i++)
      ((ContentHandler)(currentSection.activeHandlers.elementAt(i))).endElement(uri, localName, qName);
    currentSection.depth--;
    if (currentSection.contextDependent)
      currentSection.context.pop();
    if (currentSection.depth == 0)
      endSection();
  }

  private void endSection() throws SAXException {
    for (int i = 0, len = currentSection.validators.size(); i < len; i++) {
      Validator validator = (Validator)currentSection.validators.elementAt(i);
      cleanupHandler(validator.getContentHandler());
      releaseValidator((Schema)currentSection.schemas.elementAt(i), validator);
      // endDocument() on one of the validators may throw an exception
      // in this case we don't want to release the validator twice
      currentSection.validators.setElementAt(null, i);
    }
    currentSection = currentSection.parent;
  }

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

  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;
  }

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

  private void releaseValidator(Schema schema, Validator vh) {
    if (vh == null)
      return;
    vh.reset();
    ((Stack)validatorHandlerCache.get(schema)).push(vh);
  }

  public void reset() {
    for (; currentSection != null; currentSection = currentSection.parent) {
      for (int i = 0, len = currentSection.validators.size(); i < len; i++)
        releaseValidator((Schema)currentSection.schemas.elementAt(i),
                         (Validator)currentSection.validators.elementAt(i));
    }
    initCurrentSection();
  }

  public ContentHandler getContentHandler() {
    return this;
  }

  public DTDHandler getDTDHandler() {
    return this;
  }
}
