/* Copyright (c) 2013-2017, Jesper Öqvist <jesper.oqvist@cs.lth.se>
 * 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. Neither the name of the copyright holder 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 HOLDER 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.
 */
package org.extendj.ragdoc;

import java.util.Collection;
import java.util.LinkedList;

public class JavaDocParser extends Object {

  static final InlineTagExpander expander = new InlineTagExpander() {
    @Override public String expand(String tag, String text) {
      if (tag.equals("code")) {
        return "<code>" + text + "</code>";
      } else if (tag.equals("link")) {
        return "<a href=\"" + classDocURL(text) + "\">" + text + "</a>";
      } else {
        return text;
      }
    }

    private String classDocURL(String klass) {
      String result = klass.replaceAll("\\.", "/");
      return "#" + result;
    }
  };

  private int i;
  private char[] doc;
  private Collection<DocTag> tags = new LinkedList<DocTag>();

  /**
   * @return normalized documentation comment. All line endings replaced by \\n.
   */
  public String parse(String comments) {
    if (comments == null) {
      return "";
    }

    int start = 0;
    int end = -2;
    while (true) {
      int newStart = comments.indexOf("/**", end + 2);
      if (newStart == -1) {
        break;
      }
      int newEnd = comments.indexOf("*/", newStart + 3);
      if (newEnd == -1) {
        break;
      }
      start = newStart;
      end = newEnd;
    }

    if (end <= start + 3) {
      return "";
    }

    doc = comments.substring(start + 3, end).toCharArray();
    StringBuilder text = new StringBuilder(doc.length);
    i = 0;

    boolean first = true;
    while (i < doc.length) {
      if (skipNewline()) {
        skipWhitespace();
        if (skipAsterisk()) {
          if (!first) {
            text.append('\n');
          }
          first = false;
          skipWhitespace();
        }
      } else {
        text.append(doc[i++]);
      }
    }
    String expanded = expandInlineTags(text.toString());
    String content = extractContent(expanded);
    return content;
  }

  public Collection<DocTag> getTags() {
    return tags;
  }

  private static boolean isKnownTag(String tag) {
    return
        /* JastAdd tags: */
        tag.equals("declaredat") || tag.equals("production") ||
            tag.equals("astdecl") ||
            tag.equals("ast") || tag.equals("aspect") ||
            tag.equals("apilevel") || tag.equals("attribute") ||
            tag.equals("relation") ||
        /* JavaDoc tags:
         * (http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#javadoctags)
         */
            tag.equals("author") || tag.equals("deprecated") ||
            tag.equals("exception") || tag.equals("param") ||
            tag.equals("return") || tag.equals("see") ||
            tag.equals("serial") || tag.equals("serialData") ||
            tag.equals("serialField") || tag.equals("since") ||
            tag.equals("throws") || tag.equals("version");
  }

  private static boolean ignoreTag(String tag) {
    return tag.equals("production");
  }

  /**
   * Separate content and doc tags.
   */
  private String extractContent(String javadoc) {
    this.doc = javadoc.toCharArray();
    StringBuilder content = new StringBuilder(doc.length);
    i = 0;
    while (i < doc.length) {
      int rewind = i;
      DocTag tag = parseTag();
      if (tag != null && isKnownTag(tag.tag)) {
        if (!ignoreTag(tag.tag)) {
          tags.add(tag);
        }
      } else {
        if (tag != null) {
          i = rewind;
        }
        String html = parseHtmlTag();
        if (!html.isEmpty()) {
          content.append(html);
        } else {
          content.append(doc[i++]);
        }
      }
    }
    return content.toString();
  }

  private String parseHtmlTag() {
    // TODO handle string literals!
    if (doc[i] != '<') {
      return "";
    }
    int j = i + 1;
    StringBuilder text = new StringBuilder();
    text.append('<');
    while (j < doc.length) {
      char c = doc[j++];
      text.append(c);
      if (c == '>') {
        i = j;
        return text.toString();
      }
    }
    return "";
  }

  private DocTag parseTag() {
    if (doc[i] != '@') {
      return null;
    }
    i += 1;
    StringBuilder text = new StringBuilder();
    while (i < doc.length) {
      char c = doc[i];
      if (c == '@') {
        return new DocTag(text.toString().trim());
      } else {
        String html = parseHtmlTag();
        if (!html.isEmpty()) {
          text.append(html);
        } else {
          text.append(c);
          i += 1;
        }
      }
    }
    return new DocTag(text.toString().trim());
  }

  private String expandInlineTags(String javadoc) {
    this.doc = javadoc.toCharArray();
    StringBuilder text = new StringBuilder(doc.length);
    i = 0;
    while (i < doc.length) {
      String inlineTag = parseInlineTag();
      if (!inlineTag.isEmpty()) {
        text.append(expandInlineTag(inlineTag, expander));
      } else {
        text.append(doc[i++]);
      }
    }
    return text.toString();
  }

  private String expandInlineTag(String inlineTag, InlineTagExpander expander) {
    String tag, text;
    int end = inlineTag.indexOf(' ');
    if (end == -1) {
      tag = inlineTag.substring(0);
      text = "";
    } else {
      tag = inlineTag.substring(0, end);
      text = inlineTag.substring(end + 1);
    }
    return expander.expand(tag, text);
  }

  private String parseInlineTag() {
    if (doc[i] != '{' || doc[i + 1] != '@') {
      // No opening bracket.
      return "";
    }
    int j = i + 2;
    StringBuilder text = new StringBuilder();
    while (j < doc.length) {
      char c = doc[j++];
      if (c == '}') {
        i = j;
        return text.toString();
      } else {
        text.append(c);
      }
    }
    // EOF before closing bracket
    return "";
  }

  private boolean skipNewline() {
    if (doc[i] == '\r') {
      i += 1;
      if (i < doc.length && doc[i] == '\n') {
        i += 1;
      }
      return true;
    } else if (doc[i] == '\n') {
      i += 1;
      return true;
    }
    return false;
  }

  private void skipWhitespace() {
    while (i < doc.length && isWhitespace(doc[i])) {
      i += 1;
    }
  }

  private boolean isWhitespace(char c) {
    return Character.isWhitespace(c) && c != '\n' && c != '\n';
  }

  private boolean skipAsterisk() {
    if (i < doc.length && doc[i] == '*') {
      i += 1;
      return true;
    }
    return false;
  }

}