package org.kohsuke.rngom.binary;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;

import org.kohsuke.rngom.ast.builder.Annotations;
import org.kohsuke.rngom.ast.builder.BuildException;
import org.kohsuke.rngom.ast.builder.CommentList;
import org.kohsuke.rngom.ast.builder.DataPatternBuilder;
import org.kohsuke.rngom.ast.builder.Div;
import org.kohsuke.rngom.ast.builder.ElementAnnotationBuilder;
import org.kohsuke.rngom.ast.builder.Grammar;
import org.kohsuke.rngom.ast.builder.GrammarSection;
import org.kohsuke.rngom.ast.builder.Include;
import org.kohsuke.rngom.ast.builder.IncludedGrammar;
import org.kohsuke.rngom.ast.builder.NameClassBuilder;
import org.kohsuke.rngom.ast.builder.SchemaBuilder;
import org.kohsuke.rngom.ast.builder.Scope;
import org.kohsuke.rngom.ast.om.Location;
import org.kohsuke.rngom.ast.om.ParsedElementAnnotation;
import org.kohsuke.rngom.ast.om.ParsedNameClass;
import org.kohsuke.rngom.ast.om.ParsedPattern;
import org.kohsuke.rngom.ast.util.LocatorImpl;
import org.kohsuke.rngom.dt.builtin.BuiltinDatatypeLibraryFactory;
import org.kohsuke.rngom.nc.NameClass;
import org.kohsuke.rngom.nc.NameClassBuilderImpl;
import org.kohsuke.rngom.parse.Context;
import org.kohsuke.rngom.parse.IllegalSchemaException;
import org.kohsuke.rngom.parse.Parseable;
import org.kohsuke.rngom.util.Localizer;
import org.relaxng.datatype.Datatype;
import org.relaxng.datatype.DatatypeBuilder;
import org.relaxng.datatype.DatatypeException;
import org.relaxng.datatype.DatatypeLibrary;
import org.relaxng.datatype.DatatypeLibraryFactory;
import org.relaxng.datatype.ValidationContext;
import org.relaxng.datatype.helpers.DatatypeLibraryLoader;
import org.xml.sax.ErrorHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

public class SchemaBuilderImpl implements SchemaBuilder, ElementAnnotationBuilder, CommentList {
  private final SchemaBuilderImpl parent;
  private boolean hadError = false;
  private final SchemaPatternBuilder pb;
  private final DatatypeLibraryFactory datatypeLibraryFactory;
  private final String inheritNs;
  private final ErrorHandler eh;
  private final OpenIncludes openIncludes;
  private final NameClassBuilder ncb =new NameClassBuilderImpl();
  static final Localizer localizer = new Localizer(SchemaBuilderImpl.class);

  static class OpenIncludes {
    final String uri;
    final OpenIncludes parent;

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

  public ParsedPattern expandPattern(ParsedPattern _pattern)
        throws BuildException, IllegalSchemaException {
        Pattern pattern = (Pattern) _pattern;
        if (!hadError) {
            try {
                pattern.checkRecursion(0);
                pattern = pattern.expand(pb);
                pattern.checkRestrictions(Pattern.START_CONTEXT, null, null);
                if (!hadError) return pattern;
            } catch (SAXParseException e) {
                error(e);
            } catch (SAXException e) {
                throw new BuildException(e);
            } catch (RestrictionViolationException e) {
                if (e.getName() != null)
                    error(e.getMessageId(), e.getName().toString(), e
                        .getLocator());
                else
                    error(e.getMessageId(), e.getLocator());
            }
        }
        throw new IllegalSchemaException();
    }
  
  /**
   * 
   * @param eh
   *        Error handler to receive errors while building the schema.
   */
  public SchemaBuilderImpl(ErrorHandler eh) {
      this(eh,new BuiltinDatatypeLibraryFactory(new DatatypeLibraryLoader()),new SchemaPatternBuilder());
  }

  /**
   * 
   * @param eh
   *        Error handler to receive errors while building the schema.
   * @param datatypeLibraryFactory
   *        This is consulted to locate datatype libraries.
   * @param pb
   *        Used to build patterns.
   */
  public SchemaBuilderImpl(ErrorHandler eh,
                            DatatypeLibraryFactory datatypeLibraryFactory,
                            SchemaPatternBuilder pb) {
    this.parent = null;
    this.eh = eh;
    this.datatypeLibraryFactory = datatypeLibraryFactory;
    this.pb = pb;
    this.inheritNs = "";
    this.openIncludes = null;
  }

  private SchemaBuilderImpl(String inheritNs,
                            String uri,
                            SchemaBuilderImpl parent) {
    this.parent = parent;
    this.eh = parent.eh;
    this.datatypeLibraryFactory = parent.datatypeLibraryFactory;
    this.pb = parent.pb;
    this.inheritNs = inheritNs;
    this.openIncludes = new OpenIncludes(uri, parent.openIncludes);
  }
  
  public NameClassBuilder getNameClassBuilder() {
      return ncb;
  }

  public ParsedPattern makeChoice(List patterns, Location loc, Annotations anno)
          throws BuildException {
    if (patterns.isEmpty())
      throw new IllegalArgumentException();
    Pattern result = (Pattern)patterns.get(0);
    for (int i = 1; i < patterns.size(); i++)
      result = pb.makeChoice(result, (Pattern)patterns.get(i));
    return result;
  }

  public ParsedPattern makeInterleave(List patterns, Location loc, Annotations anno)
          throws BuildException {
    if (patterns.isEmpty())
      throw new IllegalArgumentException();
    Pattern result = (Pattern)patterns.get(0);
    for (int i = 1; i < patterns.size(); i++)
      result = pb.makeInterleave(result, (Pattern)patterns.get(i));
    return result;
  }

  public ParsedPattern makeGroup(List patterns, Location loc, Annotations anno)
          throws BuildException {
    if (patterns.isEmpty())
      throw new IllegalArgumentException();
    Pattern result = (Pattern)patterns.get(0);
    for (int i = 1; i < patterns.size(); i++)
      result = pb.makeGroup(result, (Pattern)patterns.get(i));
    return result;
  }

  public ParsedPattern makeOneOrMore(ParsedPattern p, Location loc, Annotations anno)
          throws BuildException {
    return pb.makeOneOrMore((Pattern)p);
  }

  public ParsedPattern makeZeroOrMore(ParsedPattern p, Location loc, Annotations anno)
          throws BuildException {
    return pb.makeZeroOrMore((Pattern)p);
  }

  public ParsedPattern makeOptional(ParsedPattern p, Location loc, Annotations anno)
          throws BuildException {
    return pb.makeOptional((Pattern)p);
  }

  public ParsedPattern makeList(ParsedPattern p, Location loc, Annotations anno)
          throws BuildException {
    return pb.makeList((Pattern)p, (Locator)loc);
  }

  public ParsedPattern makeMixed(ParsedPattern p, Location loc, Annotations anno)
          throws BuildException {
    return pb.makeMixed((Pattern)p);
  }

  public ParsedPattern makeEmpty(Location loc, Annotations anno) {
    return pb.makeEmpty();
  }

  public ParsedPattern makeNotAllowed(Location loc, Annotations anno) {
    return pb.makeUnexpandedNotAllowed();
  }

  public ParsedPattern makeText(Location loc, Annotations anno) {
    return pb.makeText();
  }

  public ParsedPattern makeErrorPattern() {
    return pb.makeError();
  }

//  public ParsedNameClass makeErrorNameClass() {
//    return new ErrorNameClass();
//  }

  public ParsedPattern makeAttribute(ParsedNameClass nc, ParsedPattern p, Location loc, Annotations anno)
          throws BuildException {
    return pb.makeAttribute((NameClass)nc, (Pattern)p, (Locator)loc);
  }

  public ParsedPattern makeElement(ParsedNameClass nc, ParsedPattern p, Location loc, Annotations anno)
          throws BuildException {
    return pb.makeElement((NameClass)nc, (Pattern)p, (Locator)loc);
  }

  private class DummyDataPatternBuilder implements DataPatternBuilder {
    public void addParam(String name, String value, Context context, String ns, Location loc, Annotations anno)
            throws BuildException {
    }

    public ParsedPattern makePattern(Location loc, Annotations anno)
            throws BuildException {
      return pb.makeError();
    }

    public ParsedPattern makePattern(ParsedPattern except, Location loc, Annotations anno)
            throws BuildException {
      return pb.makeError();
    }

    public void annotation(ParsedElementAnnotation ea) {
    }
  }

  private class ValidationContextImpl implements ValidationContext {
    private ValidationContext vc;
    private String ns;

    ValidationContextImpl(ValidationContext vc, String ns) {
      this.vc = vc;
      this.ns = ns.length() == 0 ? null : ns;
    }

    public String resolveNamespacePrefix(String prefix) {
      return prefix.length() == 0 ? ns : vc.resolveNamespacePrefix(prefix);
    }

    public String getBaseUri() {
      return vc.getBaseUri();
    }

    public boolean isUnparsedEntity(String entityName) {
      return vc.isUnparsedEntity(entityName);
    }

    public boolean isNotation(String notationName) {
      return vc.isNotation(notationName);
    }
  }

  private class DataPatternBuilderImpl implements DataPatternBuilder {
    private DatatypeBuilder dtb;
    DataPatternBuilderImpl(DatatypeBuilder dtb) {
      this.dtb = dtb;
    }

    public void addParam(String name, String value, Context context, String ns, Location loc, Annotations anno)
            throws BuildException {
      try {
        dtb.addParameter(name, value, new ValidationContextImpl(context, ns));
      }
      catch (DatatypeException e) {
	String detail = e.getMessage();
        int pos = e.getIndex();
        String displayedParam;
        if (pos == DatatypeException.UNKNOWN)
          displayedParam = null;
        else
          displayedParam = displayParam(value, pos);
        if (displayedParam != null) {
          if (detail != null)
            error("invalid_param_detail_display", detail, displayedParam, (Locator)loc);
          else
            error("invalid_param_display", displayedParam, (Locator)loc);
        }
	else if (detail != null)
	  error("invalid_param_detail", detail, (Locator)loc);
	else
	  error("invalid_param", (Locator)loc);
      }
    }

    String displayParam(String value, int pos) {
      if (pos < 0)
        pos = 0;
      else if (pos > value.length())
        pos = value.length();
      return localizer.message("display_param", value.substring(0, pos), value.substring(pos));
    }

    public ParsedPattern makePattern(Location loc, Annotations anno)
            throws BuildException {
      try {
        return pb.makeData(dtb.createDatatype());
      }
      catch (DatatypeException e) {
	String detail = e.getMessage();
	if (detail != null)
	  error("invalid_params_detail", detail, (Locator)loc);
	else
	  error("invalid_params", (Locator)loc);
        return pb.makeError();
      }
    }

    public ParsedPattern makePattern(ParsedPattern except, Location loc, Annotations anno)
            throws BuildException {
      try {
        return pb.makeDataExcept(dtb.createDatatype(), (Pattern)except, (Locator)loc);
      }
      catch (DatatypeException e) {
	String detail = e.getMessage();
	if (detail != null)
	  error("invalid_params_detail", detail, (Locator)loc);
	else
	  error("invalid_params", (Locator)loc);
        return pb.makeError();
      }
    }

    public void annotation(ParsedElementAnnotation ea) {
    }
  }

  public DataPatternBuilder makeDataPatternBuilder(String datatypeLibrary, String type, Location loc)
          throws BuildException {
    DatatypeLibrary dl = datatypeLibraryFactory.createDatatypeLibrary(datatypeLibrary);
    if (dl == null)
      error("unrecognized_datatype_library", datatypeLibrary, (Locator)loc);
    else {
      try {
        return new DataPatternBuilderImpl(dl.createDatatypeBuilder(type));
      }
      catch (DatatypeException e) {
	String detail = e.getMessage();
	if (detail != null)
	  error("unsupported_datatype_detail", datatypeLibrary, type, detail, (Locator)loc);
	else
	  error("unrecognized_datatype", datatypeLibrary, type, (Locator)loc);
      }
    }
    return new DummyDataPatternBuilder();
  }

  public ParsedPattern makeValue(String datatypeLibrary, String type, String value, Context context, String ns,
                                 Location loc, Annotations anno) throws BuildException {
    DatatypeLibrary dl = datatypeLibraryFactory.createDatatypeLibrary(datatypeLibrary);
    if (dl == null)
      error("unrecognized_datatype_library", datatypeLibrary, (Locator)loc);
    else {
      try {
        DatatypeBuilder dtb = dl.createDatatypeBuilder(type);
        try {
          Datatype dt = dtb.createDatatype();
          Object obj = dt.createValue(value, new ValidationContextImpl(context, ns));
          if (obj != null)
            return pb.makeValue(dt, obj);
          error("invalid_value", value, (Locator)loc);
        }
        catch (DatatypeException e) {
          String detail = e.getMessage();
          if (detail != null)
            error("datatype_requires_param_detail", detail, (Locator)loc);
          else
            error("datatype_requires_param", (Locator)loc);
        }
      }
      catch (DatatypeException e) {
        error("unrecognized_datatype", datatypeLibrary, type, (Locator)loc);
      }
    }
    return pb.makeError();
  }

  static class GrammarImpl implements Grammar, Div, IncludedGrammar {
    private final SchemaBuilderImpl sb;
    private final Hashtable defines;
    private final RefPattern startRef;
    private final Scope parent;

    private GrammarImpl(SchemaBuilderImpl sb, Scope parent) {
      this.sb = sb;
      this.parent = parent;
      this.defines = new Hashtable();
      this.startRef = new RefPattern(null);
    }

    protected GrammarImpl(SchemaBuilderImpl sb, GrammarImpl g) {
      this.sb = sb;
      parent = g.parent;
      startRef = g.startRef;
      defines = g.defines;
    }
    
    public ParsedPattern endGrammar(Location loc, Annotations anno) throws BuildException {
      for (Enumeration e = defines.keys();
           e.hasMoreElements();) {
        String name = (String)e.nextElement();
        RefPattern rp = (RefPattern)defines.get(name);
        if (rp.getPattern() == null) {
          sb.error("reference_to_undefined", name, rp.getRefLocator());
          rp.setPattern(sb.pb.makeError());
        }
      }
      Pattern start = startRef.getPattern();
      if (start == null) {
        sb.error("missing_start_element", (Locator)loc);
        start = sb.pb.makeError();
      }
      return start;
    }

    public void endDiv(Location loc, Annotations anno) throws BuildException {
      // nothing to do
    }

    public ParsedPattern endIncludedGrammar(Location loc, Annotations anno) throws BuildException {
        return null;
    }

    public void define(String name, GrammarSection.Combine combine, ParsedPattern pattern, Location loc, Annotations anno)
            throws BuildException {
      define(lookup(name), combine, pattern, loc);
    }

    private void define(RefPattern rp, GrammarSection.Combine combine, ParsedPattern pattern, Location loc)
            throws BuildException {
      switch (rp.getReplacementStatus()) {
      case RefPattern.REPLACEMENT_KEEP:
        if (combine == null) {
          if (rp.isCombineImplicit()) {
            if (rp.getName() == null)
              sb.error("duplicate_start", (Locator)loc);
            else
              sb.error("duplicate_define", rp.getName(), (Locator)loc);
          }
          else
            rp.setCombineImplicit();
        }
        else {
          byte combineType = (combine == COMBINE_CHOICE ? RefPattern.COMBINE_CHOICE : RefPattern.COMBINE_INTERLEAVE);
          if (rp.getCombineType() != RefPattern.COMBINE_NONE
              && rp.getCombineType() != combineType) {
            if (rp.getName() == null)
              sb.error("conflict_combine_start", (Locator)loc);
            else
              sb.error("conflict_combine_define", rp.getName(), (Locator)loc);
          }
          rp.setCombineType(combineType);
        }
        Pattern p = (Pattern)pattern;
        if (rp.getPattern() == null)
          rp.setPattern(p);
        else if (rp.getCombineType() == RefPattern.COMBINE_INTERLEAVE)
          rp.setPattern(sb.pb.makeInterleave(rp.getPattern(), p));
        else
          rp.setPattern(sb.pb.makeChoice(rp.getPattern(), p));
        break;
      case RefPattern.REPLACEMENT_REQUIRE:
        rp.setReplacementStatus(RefPattern.REPLACEMENT_IGNORE);
        break;
      case RefPattern.REPLACEMENT_IGNORE:
        break;
      }
    }

    public void topLevelAnnotation(ParsedElementAnnotation ea) throws BuildException {
    }

    public void topLevelComment(CommentList comments) throws BuildException {
    }

    private RefPattern lookup(String name) {
      if (name == START)
        return startRef;
      return lookup1(name);
    }

    private RefPattern lookup1(String name) {
      RefPattern p = (RefPattern)defines.get(name);
      if (p == null) {
        p = new RefPattern(name);
        defines.put(name, p);
      }
      return p;
    }

    public ParsedPattern makeRef(String name, Location loc, Annotations anno) throws BuildException {
      RefPattern p = lookup1(name);
      if (p.getRefLocator() == null && loc != null)
        p.setRefLocator((Locator)loc);
      return p;
    }

    public ParsedPattern makeParentRef(String name, Location loc, Annotations anno) throws BuildException {
        // TODO: do this check by the caller
      if (parent == null) {
        sb.error("parent_ref_outside_grammar", (Locator)loc);
        return sb.makeErrorPattern();
      }
      return parent.makeRef(name, loc, anno);
    }

    public Div makeDiv() {
      return this;
    }

    public Include makeInclude() {
      return new IncludeImpl(sb, this);
    }

  }


  static class Override {
    Override(RefPattern prp, Override next) {
      this.prp = prp;
      this.next = next;
    }

    RefPattern prp;
    Override next;
    byte replacementStatus;
  }


  private static class IncludeImpl implements Include, Div {
    private SchemaBuilderImpl sb;
    private Override overrides;
    private GrammarImpl grammar;

    private IncludeImpl(SchemaBuilderImpl sb, GrammarImpl grammar) {
      this.sb = sb;
      this.grammar = grammar;
    }

    public void define(String name, GrammarSection.Combine combine, ParsedPattern pattern, Location loc, Annotations anno)
            throws BuildException {
      RefPattern rp = grammar.lookup(name);
      overrides = new Override(rp, overrides);
      grammar.define(rp, combine, pattern, loc);
    }

    public void endDiv(Location loc, Annotations anno) throws BuildException {
      // nothing to do
    }

    public void topLevelAnnotation(ParsedElementAnnotation ea) throws BuildException {
      // nothing to do
    }

    public void topLevelComment(CommentList comments) throws BuildException {
    }

    public Div makeDiv() {
      return this;
    }

    public void endInclude(Parseable current,String uri, String ns,
                           Location loc, Annotations anno) throws BuildException {
      for (OpenIncludes inc = sb.openIncludes;
           inc != null;
           inc = inc.parent) {
        if (inc.uri.equals(uri)) {
          sb.error("recursive_include", uri, (Locator)loc);
          return;
        }
      }

      for (Override o = overrides; o != null; o = o.next) {
        o.replacementStatus = o.prp.getReplacementStatus();
        o.prp.setReplacementStatus(RefPattern.REPLACEMENT_REQUIRE);
      }
      try {
        SchemaBuilderImpl isb = new SchemaBuilderImpl(ns, uri, sb);
        current.parseInclude(uri, isb, new GrammarImpl(isb, grammar), ns);
        for (Override o = overrides; o != null; o = o.next) {
          if (o.prp.getReplacementStatus() == RefPattern.REPLACEMENT_REQUIRE) {
            if (o.prp.getName() == null)
              sb.error("missing_start_replacement", (Locator)loc);
            else
              sb.error("missing_define_replacement", o.prp.getName(), (Locator)loc);
          }
        }
      }
      catch (IllegalSchemaException e) {
        sb.noteError();
      }
      finally {
        for (Override o = overrides; o != null; o = o.next)
          o.prp.setReplacementStatus(o.replacementStatus);
      }
    }

    public Include makeInclude() {
      return null;
    }
  }

  public Grammar makeGrammar(Scope parent) {
    return new GrammarImpl(this, parent);
  }

  public ParsedPattern annotate(ParsedPattern p, Annotations anno) throws BuildException {
    return p;
  }


  public ParsedPattern annotateAfter(ParsedPattern p, ParsedElementAnnotation e) throws BuildException {
    return p;
  }

  public ParsedPattern commentAfter(ParsedPattern p, CommentList comments) throws BuildException {
    return p;
  }


  public ParsedPattern makeExternalRef(Parseable current, String uri, String ns, Scope scope,
                                       Location loc, Annotations anno)
          throws BuildException {
    for (OpenIncludes inc = openIncludes;
         inc != null;
         inc = inc.parent) {
      if (inc.uri.equals(uri)) {
        error("recursive_include", uri, (Locator)loc);
        return pb.makeError();
      }
    }
    try {
      return current.parseExternal(uri, new SchemaBuilderImpl(ns, uri, this), scope, ns );
    }
    catch (IllegalSchemaException e) {
      noteError();
      return pb.makeError();
    }
  }



  public Location makeLocation(String systemId, int lineNumber, int columnNumber) {
    return new LocatorImpl(systemId, lineNumber, columnNumber);
  }

  public Annotations makeAnnotations(CommentList comments, Context context) {
    return this;
  }

  public ElementAnnotationBuilder makeElementAnnotationBuilder(String ns, String localName, String prefix,
                                                               Location loc, CommentList comments, Context context) {
    return this;
  }

  public CommentList makeCommentList() {
    return this;
  }

  public void addComment(String value, Location loc) throws BuildException {
  }

  public void addAttribute(String ns, String localName, String prefix, String value, Location loc) {
    // nothing needed
  }

  public void addElement(ParsedElementAnnotation ea) {
    // nothing needed
  }

  public void addComment(CommentList comments) throws BuildException {
    // nothing needed
  }

  public void addLeadingComment(CommentList comments) throws BuildException {
    // nothing needed
  }

  public ParsedElementAnnotation makeElementAnnotation() {
    return null;
  }

  public void addText(String value, Location loc, CommentList comments) throws BuildException {
  }
  
  public boolean usesComments() {
    return false;
  }

  private void error(SAXParseException message) throws BuildException {
    noteError();
    try {
      if (eh != null)
        eh.error(message);
    }
    catch (SAXException e) {
      throw new BuildException(e);
    }
  }

  private void warning(SAXParseException message) throws BuildException {
    try {
      if (eh != null)
        eh.warning(message);
    }
    catch (SAXException e) {
      throw new BuildException(e);
    }
  }

  private void error(String key, Locator loc) throws BuildException {
    error(new SAXParseException(localizer.message(key), loc));
  }

  private void error(String key, String arg, Locator loc) throws BuildException {
    error(new SAXParseException(localizer.message(key, arg), loc));
  }

  private void error(String key, String arg1, String arg2, Locator loc) throws BuildException {
    error(new SAXParseException(localizer.message(key, arg1, arg2), loc));
  }

  private void error(String key, String arg1, String arg2, String arg3, Locator loc) throws BuildException {
    error(new SAXParseException(localizer.message(key, new Object[]{arg1, arg2, arg3}), loc));
  }
  private void noteError() {
    if (!hadError && parent != null)
      parent.noteError();
    hadError = true;
  }

}
