/*
 * Copyright (c) 2003, Henri Yandell
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the
 * following conditions are met:
 *
 * + Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 *
 * + 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.
 *
 * + Neither the name of XmlWriter nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS 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 COPYRIGHT OWNER OR 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.
 */

/*
 * Copyright (c) 2003, Henri Yandell
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the
 * following conditions are met:
 *
 * + Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 *
 * + 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.
 *
 * + Neither the name of XmlWriter nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS 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 COPYRIGHT OWNER OR 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.
 */

/*
 * Copyright (c) 2010, Menny Even Danan
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the
 * following conditions are met:
 *
 * + Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 *
 * + 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.
 *
 * + Neither the name of XmlWriter nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS 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 COPYRIGHT OWNER OR 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.
 */
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.Writer;
import java.util.Stack;

/**
 * Makes writing XML much much easier.
 *
 * @author <a href="mailto:bayard@generationjava.com">Henri Yandell</a>
 * @author <a href="mailto:menny|AT| evendanan{dot} net">Menny Even Danan - just added some features
 *     on Henri's initial version</a>
 * @version 0.2
 */
class XmlWriter {

  private static final String INDENT_STRING = "    ";
  private final boolean thisIsWriterOwner; // is this instance the owner?
  private final Writer writer; // underlying writer
  private final int indentingOffset;
  private final Stack<String> stack; // of xml entity names
  private final StringBuffer attrs; // current attribute string
  private boolean empty; // is the current node empty
  private boolean justWroteText;
  private boolean closed; // is the current node closed...

  /**
   * Create an XmlWriter on top of an existing java.io.Writer.
   *
   * @throws IOException
   */
  XmlWriter(Writer writer, boolean takeOwnership, int indentingOffset, boolean addXmlPrefix)
      throws IOException {
    thisIsWriterOwner = takeOwnership;
    this.indentingOffset = indentingOffset;
    this.writer = writer;
    this.closed = true;
    this.stack = new Stack<>();
    this.attrs = new StringBuffer();
    if (addXmlPrefix) this.writer.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
  }

  XmlWriter(File outputFile) throws IOException {
    this(new FileWriter(outputFile), true, 0, true);
  }

  /**
   * Begin to output an entity.
   *
   * @param name name of entity.
   */
  XmlWriter writeEntity(String name) throws IOException {
    closeOpeningTag(true);
    this.closed = false;
    for (int tabIndex = 0; tabIndex < stack.size() + indentingOffset; tabIndex++)
      this.writer.write(INDENT_STRING);
    this.writer.write("<");
    this.writer.write(name);
    stack.add(name);
    this.empty = true;
    this.justWroteText = false;
    return this;
  }

  // close off the opening tag
  private void closeOpeningTag(final boolean newLine) throws IOException {
    if (!this.closed) {
      writeAttributes();
      this.closed = true;
      this.writer.write(">");
      if (newLine) this.writer.write("\n");
    }
  }

  // write out all current attributes
  private void writeAttributes() throws IOException {
    this.writer.write(this.attrs.toString());
    this.attrs.setLength(0);
    this.empty = false;
  }

  /**
   * Write an attribute out for the current entity. Any xml characters in the value are escaped.
   * Currently it does not actually throw the exception, but the api is set that way for future
   * changes.
   *
   * @param attr name of attribute.
   * @param value value of attribute.
   */
  public XmlWriter writeAttribute(String attr, String value) {
    this.attrs.append(" ");
    this.attrs.append(attr);
    this.attrs.append("=\"");
    this.attrs.append(escapeXml(value));
    this.attrs.append("\"");
    return this;
  }

  /**
   * End the current entity. This will throw an exception if it is called when there is not a
   * currently open entity.
   *
   * @throws IOException
   */
  public XmlWriter endEntity() throws IOException {
    if (this.stack.empty()) {
      throw new InvalidObjectException("Called endEntity too many times. ");
    }
    String name = this.stack.pop();
    if (name != null) {
      if (this.empty) {
        writeAttributes();
        this.writer.write("/>\n");
      } else {
        if (!this.justWroteText) {
          for (int tabIndex = 0; tabIndex < stack.size() + indentingOffset; tabIndex++)
            this.writer.write(INDENT_STRING);
        }
        this.writer.write("</");
        this.writer.write(name);
        this.writer.write(">\n");
      }
      this.empty = false;
      this.closed = true;
      this.justWroteText = false;
    }
    return this;
  }

  /**
   * Close this writer. It does not close the underlying writer, but does throw an exception if
   * there are as yet unclosed tags.
   *
   * @throws IOException
   */
  public void close() throws IOException {
    if (!this.stack.empty()) {
      throw new InvalidObjectException(
          "Tags are not all closed. " + "Possibly, " + this.stack.pop() + " is unclosed. ");
    }
    if (thisIsWriterOwner) {
      this.writer.flush();
      this.writer.close();
    }
  }

  /** Output body text. Any xml characters are escaped. */
  public XmlWriter writeText(String text) throws IOException {
    closeOpeningTag(false);
    this.empty = false;
    this.justWroteText = true;
    this.writer.write(escapeXml(text));
    return this;
  }

  // Static functions lifted from generationjava helper classes
  // to make the jar smaller.

  // from XmlW
  public static String escapeXml(String str) {
    str = replaceString(str, "&", "&amp;");
    str = replaceString(str, "<", "&lt;");
    str = replaceString(str, ">", "&gt;");
    str = replaceString(str, "\"", "&quot;");
    str = replaceString(str, "'", "&apos;");
    return str;
  }

  // from StringW
  public static String replaceString(String text, String repl, String with) {
    return replaceString(text, repl, with, -1);
  }

  /**
   * Replace a string with another string inside a larger string, for the first n values of the
   * search string.
   *
   * @param text String to do search and replace in
   * @param repl String to search for
   * @param with String to replace with
   * @param max int values to replace
   * @return String with n values replacEd
   */
  static String replaceString(String text, String repl, String with, int max) {
    if (text == null) {
      return null;
    }

    StringBuffer buffer = new StringBuffer(text.length());
    int start = 0;
    int end = 0;
    while ((end = text.indexOf(repl, start)) != -1) {
      buffer.append(text.substring(start, end)).append(with);
      start = end + repl.length();

      if (--max == 0) {
        break;
      }
    }
    buffer.append(text.substring(start));

    return buffer.toString();
  }
  //
  // static public void test1() throws WritingException {
  // Writer writer = new java.io.StringWriter();
  // XmlWriter xmlwriter = new XmlWriter(writer);
  // xmlwriter.writeEntity("person").writeAttribute("name",
  // "fred").writeAttribute("age",
  // "12").writeEntity("phone").writeText("4254343").endEntity().writeEntity("bob").endEntity().endEntity();
  // xmlwriter.close();
  // System.err.println(writer.toString());
  // }
  // static public void test2() throws WritingException {
  // Writer writer = new java.io.StringWriter();
  // XmlWriter xmlwriter = new XmlWriter(writer);
  // xmlwriter.writeEntity("person");
  // xmlwriter.writeAttribute("name", "fred");
  // xmlwriter.writeAttribute("age", "12");
  // xmlwriter.writeEntity("phone");
  // xmlwriter.writeText("4254343");
  // xmlwriter.endEntity();
  // xmlwriter.writeEntity("bob");
  // xmlwriter.endEntity();
  // xmlwriter.endEntity();
  // xmlwriter.close();
  // System.err.println(writer.toString());
  // }

}
