/*
 * The_xmloperator_project Software License, Version 1.7
 *
 * Copyright (c) 2000 - 2003 The_xmloperator_project.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *      "This product includes or uses software developped
 *       by The_xmloperator_project (http://www.xmloperator.net/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. Products derived from this software may not be called "xmloperator",
 *    nor may "xmloperator" appear in their name, without prior written
 *    permission. For written permission, please contact
 *    the xmloperator project administrator.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE_XMLOPERATOR_PROJECT OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * Further information can be found on the project's site
 * (http://www.xmloperator.net/).
 */
package xmltorng.i2s.util;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import java.util.Stack;

import xmltorng.framework.document.relaxng.NameClass;
import xmltorng.framework.document.relaxng.Name;
import xmltorng.framework.document.relaxng.Pattern;
import xmltorng.framework.document.relaxng.NonEmptyPattern;
import xmltorng.framework.document.relaxng.ElementDefinition;
import xmltorng.framework.document.relaxng.Ref;
import xmltorng.framework.document.relaxng.Attribute;
import xmltorng.framework.document.relaxng.GroupPattern;
import xmltorng.framework.document.relaxng.Schema;

/**
 * Write a RELAX NG schema with the following restrictions :
 * Doesn't support name classes other than simple name.
 * Doesn't support list, data and value patterns.
 */
public final class SchemaWriter {
  private PrintWriter out;
  private final String tab = "  ";
  private final Stack nameStack = new Stack();
  

  private static final class SchemaAnalysis {
    private final Map fromElementDefinitionToMultipleUse = new HashMap();
    private final Map fromElementDefinitionToId = new HashMap();

    public SchemaAnalysis(Schema schema) {
      this.addPattern(schema.getStart());
      for (int k = 0; k < schema.getElementDefinitionCount(); k++)
        this.addPattern(schema.getElementDefinition(k).getPattern());
      int defineCount = 0;
      for (int k = 0; k < schema.getElementDefinitionCount(); k++) {
        ElementDefinition elementDefinition = schema.getElementDefinition(k);
        Boolean use = (Boolean)this.fromElementDefinitionToMultipleUse.
             get(elementDefinition);
        if (use != null && use.booleanValue())
          this.fromElementDefinitionToId.put(
              elementDefinition, "d" + Integer.toString(++defineCount)
              + "_" + ((Name)elementDefinition.getNameClass()).getName());
      }
    }

    private void addPattern(Pattern pattern) {
      if (pattern == null || pattern.isEmpty())
        return;
      switch (((NonEmptyPattern)pattern).getPatternType()) {
        case NonEmptyPattern.PATTERN_GROUP:
          for (int k = 0; k < ((GroupPattern)pattern).getChildCount(); k++)
            this.addPattern(((GroupPattern)pattern).getChild(k));
          break;
        case NonEmptyPattern.PATTERN_REF: {
          ElementDefinition elementDefinition =
              ((Ref)pattern).getElementDefinition();
          Boolean multipleUse =
              (Boolean)this.fromElementDefinitionToMultipleUse.
              get(elementDefinition);
          if (multipleUse == null || !multipleUse.booleanValue())
            this.fromElementDefinitionToMultipleUse.put(elementDefinition,
                multipleUse != null ? Boolean.TRUE : Boolean.FALSE);
          break;
        }
      }
    }

    public boolean isElementDefinitionUsedMoreThanOnce(
        ElementDefinition elementDefinition) {
      Boolean use = (Boolean)this.fromElementDefinitionToMultipleUse.
                    get(elementDefinition);
      return use != null && use.booleanValue();
    }

    public String getElementDefinitionId(ElementDefinition elementDefinition) {
      return (String)this.fromElementDefinitionToId.get(elementDefinition);
    }
  }

  public static final void printSchema(
      PrintWriter out, Schema schema) {
    new SchemaWriter(out).printSchema(schema);
  }

  private SchemaWriter(PrintWriter out) {
    this.out = out;
  }

  private void printSchema(Schema schema) {
    SchemaAnalysis analysis = new SchemaAnalysis(schema);
    this.startTagStart("grammar");
    this.attribute("xmlns", "http://relaxng.org/ns/structure/1.0");
    this.startTagEnd();
    String defaultNS = "";
    this.startTag("start");
    this.printPattern(analysis, defaultNS, schema.getStart());
    this.endTag();
    for (int k = 0; k < schema.getElementDefinitionCount(); k++)
      printDefine(analysis, defaultNS, schema.getElementDefinition(k));
    this.endTag();
  }

  private void printDefine(SchemaAnalysis analysis, String defaultNS,
                           ElementDefinition elementDefinition) {
    String id = analysis.getElementDefinitionId(elementDefinition);
    if (id == null)
      return;
    this.startTagStart("define");
    this.attribute("name", id);
    this.startTagEnd();
    this.printElementDefinition(analysis, defaultNS, elementDefinition);
    this.endTag();
  }

  private void printElementDefinition(
      SchemaAnalysis analysis, String defaultNS,
      ElementDefinition elementDefinition) {
    this.startTagStart("element");
    NameClass nameClass = elementDefinition.getNameClass();
    if (!(nameClass instanceof Name))
      throw new IllegalArgumentException();
    defaultNS = this.printNameAttributes(defaultNS, (Name)nameClass);
    this.startTagEnd();
    this.printPattern(analysis, defaultNS, elementDefinition.getPattern());
    this.endTag();
  }

  private void printPattern(SchemaAnalysis analysis, String defaultNS,
                            Pattern pattern) {
    if (pattern == null)
      emptyTag("notAllowed");
    else if (pattern.isEmpty())
      emptyTag("empty");
    else printNonEmptyPattern(analysis, defaultNS, (NonEmptyPattern)pattern);
  }

  private void printNonEmptyPattern(SchemaAnalysis analysis, String defaultNS,
                            NonEmptyPattern pattern) {
    printNonEmptyPattern(analysis, defaultNS, pattern, false);
  }

  private void printNonEmptyPattern(SchemaAnalysis analysis, String defaultNS,
                            NonEmptyPattern pattern, boolean isOptional) {
    boolean canBeRepeated = pattern.canBeRepeated();
    String tag = canBeRepeated ? isOptional ? "zeroOrMore" : "oneOrMore" :
                 isOptional ? "optional" : null;
    switch (pattern.getPatternType()) {
      case NonEmptyPattern.PATTERN_ATTRIBUTE: {
        if (isOptional)
          this.startTag("optional");
        canBeRepeated = false;
        this.printAttribute(defaultNS, (Attribute)pattern);
        break;
      }
      case NonEmptyPattern.PATTERN_REF: {
        if (tag != null)
          this.startTag(tag);
        this.printRef(analysis, defaultNS, (Ref)pattern);
        break;
      }
      case NonEmptyPattern.PATTERN_GROUP:
        if (tag != null)
          this.startTag(tag);
        this.printGroupPattern(analysis, defaultNS, (GroupPattern)pattern);
        break;
      default:
        if (isOptional)
          this.startTag("optional");
        canBeRepeated = false;
        this.emptyTag("text");
    }
    if (canBeRepeated | isOptional)
      this.endTag();
  }

  private void printAttribute(String defaultNS, Attribute pattern) {
    this.startTagStart("attribute");
    NameClass nameClass = pattern.getNameClass();
    if (!(nameClass instanceof Name))
      throw new IllegalArgumentException();
    defaultNS = this.printNameAttributes(defaultNS, (Name)nameClass);
    this.startTagEnd();
    this.printPattern(null, defaultNS, pattern.getPattern());
    this.endTag();
  }

  private String printNameAttributes(String defaultNS, Name name) {
    String nsURI = name.getNsURI();
    if (!nsURI.equals(defaultNS))
      this.attribute("ns", nsURI);
    this.attribute("name", name.getName());
    return nsURI;
  }

  private void printRef(SchemaAnalysis analysis, String defaultNS,
                        Ref pattern) {
    ElementDefinition elementDefinition = pattern.getElementDefinition();
    if (analysis.isElementDefinitionUsedMoreThanOnce(elementDefinition)) {
      this.startTagStart("ref");
      this.attribute("name",
                     analysis.getElementDefinitionId(elementDefinition));
      this.emptyTagEnd();
    }
    else
      this.printElementDefinition(analysis, defaultNS, elementDefinition);
  }

  private void printGroupPattern(SchemaAnalysis analysis, String defaultNS,
                            GroupPattern pattern) {
    boolean isOptional = false;
    int childCount = pattern.getChildCount();
    switch (pattern.getGroupType()) {
      case GroupPattern.GROUP_GROUP:
        if (childCount > 1)
          this.startTag("group");
        break;
      case GroupPattern.GROUP_INTERLEAVE:
        if (childCount > 1)
          this.startTag("interleave");
        break;
      case GroupPattern.GROUP_CHOICE_INCLUDING_EMPTY_PATTERN:
        isOptional = true;
      case GroupPattern.GROUP_CHOICE_EXCLUDING_EMPTY_PATTERN:
        if (childCount > 1) {
          if (isOptional)
            this.startTag("optional");
          this.startTag("choice");
        }
    }
    if (childCount == 1 && isOptional) {
      this.printNonEmptyPattern(analysis, defaultNS, pattern.getChild(0), true);
      isOptional = false;
    }
    else
      for (int k = 0; k < childCount; k++)
        this.printNonEmptyPattern(analysis, defaultNS, pattern.getChild(k));
    if (childCount > 1)
      this.endTag();
    if (isOptional)
      this.endTag();
  }

  private void startTagStart(String name) {
    this.tab();
    out.print('<');
    out.print(name);
    this.nameStack.push(name);
  }

  private void attribute(String name, String value) {
    out.print(' ');
    out.print(name);
    out.print("=\"");
    out.print(value);
    out.print('"');
  }

  private void startTagEnd() {
    out.println('>');
  }

  private void startTag(String name) {
    this.startTagStart(name);
    this.startTagEnd();
  }

  private void emptyTag(String name) {
    this.tab();
    out.print('<');
    out.print(name);
    out.println("/>");
  }

  private void endTag() {
    if (this.nameStack.isEmpty())
      throw new IllegalStateException();
    String name = (String)nameStack.pop();
    this.tab();
    out.print("</");
    out.print(name);
    out.println('>');
  }

  private void emptyTagEnd() {
    if (this.nameStack.isEmpty())
      throw new IllegalStateException();
    this.nameStack.pop();
    out.println("/>");
  }

  private void tab() {
    for (int k = 0; k < this.nameStack.size(); k++)
      out.print(this.tab);
  }
}
