/*
 * Copyright (C) 2011 RenĂ© Jeschke <[email protected]>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.kaif.kmark;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;

import org.springframework.web.util.HtmlUtils;

public class KmarkProcessor {

  public static String process(final String input) {
    try {
      return new KmarkProcessor().process(new StringReader(input));
    } catch (IOException ignore) {
      //never happen, it's string reader
      return null;
    }
  }

  public static String escapeHtml(String input) {
    return HtmlUtils.htmlEscape(input);
  }

  private KmarkProcessor() {

  }

  private String process(final Reader reader) throws IOException {
    Configuration configuration = new Configuration.Builder().build();
    Emitter emitter = new Emitter(configuration);
    final HtmlEscapeStringBuilder out = new HtmlEscapeStringBuilder();
    final Block parent = this.readLines(reader, emitter);
    parent.removeSurroundingEmptyLines();
    this.recurse(parent, false);
    Block block = parent.blocks;
    while (block != null) {
      emitter.emit(out, block);
      block = block.next;
    }
    emitter.emitRefLinks(out);
    return out.toString();
  }

  private Block readLines(final Reader reader, final Emitter emitter) throws IOException {
    final Block block = new Block();
    final StringBuilder sb = new StringBuilder(200);
    boolean underCodeBlock = false;
    int c = reader.read();
    LinkRef lastLinkRef = null;
    while (c != -1) {
      sb.setLength(0);
      int pos = 0;
      boolean eol = false;
      while (!eol) {
        switch (c) {
          case -1:
            eol = true;
            break;
          case '\n':
            c = reader.read();
            if (c == '\r') {
              c = reader.read();
            }
            eol = true;
            break;
          case '\r':
            c = reader.read();
            if (c == '\n') {
              c = reader.read();
            }
            eol = true;
            break;
          case '\t': {
            final int np = pos + (4 - (pos & 3));
            while (pos < np) {
              sb.append(' ');
              pos++;
            }
            c = reader.read();
            break;
          }
          default:
            pos++;
            sb.append((char) c);
            c = reader.read();
            break;
        }
      }

      final Line line = new Line();
      line.value = sb.toString();
      line.init();

      if (line.getLineType() == LineType.FENCED_CODE) {
        underCodeBlock = !underCodeBlock;
      }

      if (underCodeBlock) {
        block.appendLine(line);
        continue;
      }

      // Check for link definitions
      boolean isLinkRef = false;
      String id = null, link = null, comment = null;
      if (!line.isEmpty && line.value.charAt(line.leading) == '[') {
        line.pos = line.leading + 1;
        // Read ID up to ']'
        id = line.readUntil(']');
        // Is ID valid and are there any more characters?
        if (id != null && line.pos + 2 < line.value.length()) {
          // Check for ':' ([...]:...)
          if (line.value.charAt(line.pos + 1) == ':') {
            line.pos += 2;
            line.skipSpaces();
            // Check for link syntax
            if (line.value.charAt(line.pos) == '<') {
              line.pos++;
              link = line.readUntil('>');
              line.pos++;
            } else {
              link = line.readUntil(' ', '\n');
            }

            // Is link valid?
            if (link != null) {
              // Any non-whitespace characters following?
              if (line.skipSpaces()) {
                final char ch = line.value.charAt(line.pos);
                // Read comment
                if (ch == '\"' || ch == '\'' || ch == '(') {
                  line.pos++;
                  comment = line.readUntil(ch == '(' ? ')' : ch);
                  // Valid linkRef only if comment is valid
                  if (comment != null) {
                    isLinkRef = true;
                  }
                }
              } else {
                isLinkRef = true;
              }
            }
          }
        }
      }

      if (isLinkRef) {
        // Store linkRef and skip line
        final LinkRef lr = emitter.addLinkRef(id, link, comment);
        if (comment == null) {
          lastLinkRef = lr;
        }
      } else {
        comment = null;
        // Check for multi-line linkRef
        if (!line.isEmpty && lastLinkRef != null) {
          line.pos = line.leading;
          final char ch = line.value.charAt(line.pos);
          if (ch == '\"' || ch == '\'' || ch == '(') {
            line.pos++;
            comment = line.readUntil(ch == '(' ? ')' : ch);
          }
          if (comment != null) {
            lastLinkRef.title = comment;
          }
          lastLinkRef = null;
        }

        // No multi-line linkRef, store line
        if (comment == null) {
          line.pos = 0;
          block.appendLine(line);
        }
      }
    }

    return block;
  }

  /**
   * Initializes a list block by separating it into list item blocks.
   *
   * @param root
   *     The Block to process.
   */
  private void initListBlock(final Block root) {
    Line line = root.lines;
    line = line.next;
    while (line != null) {
      final LineType t = line.getLineType();
      if ((t == LineType.OLIST || t == LineType.ULIST) || (!line.isEmpty && (line.prevEmpty
          && line.leading == 0))) {
        root.split(line.previous).type = BlockType.LIST_ITEM;
      }
      line = line.next;
    }
    root.split(root.lineTail).type = BlockType.LIST_ITEM;
  }

  /**
   * Recursively process the given Block.
   *
   * @param root
   *     The Block to process.
   * @param listMode
   *     Flag indicating that we're in a list item block.
   */
  private void recurse(final Block root, boolean listMode) {
    Block block, list;
    Line line = root.lines;

    if (listMode) {
      root.removeListIndent();
    }

    while (line != null && line.isEmpty) {
      line = line.next;
    }
    if (line == null) {
      return;
    }

    while (line != null) {
      final LineType type = line.getLineType();
      switch (type) {
        case OTHER: {
          final boolean wasEmpty = line.prevEmpty;
          while (line != null && !line.isEmpty) {
            final LineType t = line.getLineType();
            if ((listMode) && (t == LineType.OLIST || t == LineType.ULIST)) {
              break;
            }
            if (t == LineType.FENCED_CODE || t == LineType.BQUOTE) {
              break;
            }
            line = line.next;
          }
          final BlockType bt;
          if (line != null && !line.isEmpty) {
            bt = (!wasEmpty) ? BlockType.NONE : BlockType.PARAGRAPH;
            root.split(line.previous).type = bt;
            root.removeLeadingEmptyLines();
          } else {
            bt = (listMode && (line == null || !line.isEmpty) && !wasEmpty)
                 ? BlockType.NONE
                 : BlockType.PARAGRAPH;
            root.split(line == null ? root.lineTail : line).type = bt;
            root.removeLeadingEmptyLines();
          }
          line = root.lines;
          break;
        }
        case BQUOTE:
          while (line != null) {
            if (!line.isEmpty && (line.prevEmpty
                && line.leading == 0
                && line.getLineType() != LineType.BQUOTE)) {
              break;
            }
            line = line.next;
          }
          block = root.split(line != null ? line.previous : root.lineTail);
          block.type = BlockType.BLOCKQUOTE;
          block.removeSurroundingEmptyLines();
          block.removeBlockQuotePrefix();
          this.recurse(block, false);
          line = root.lines;
          break;
        case FENCED_CODE:
          line = line.next;
          while (line != null) {
            if (line.getLineType() == LineType.FENCED_CODE) {
              break;
            }
            // TODO ... is this really necessary? Maybe add a special
            // flag?
            line = line.next;
          }
          if (line != null) {
            line = line.next;
          }
          block = root.split(line != null ? line.previous : root.lineTail);
          block.type = BlockType.FENCED_CODE;
          block.meta = Utils.getMetaFromFence(block.lines.value);
          block.lines.setEmpty();
          if (block.lineTail.getLineType() == LineType.FENCED_CODE) {
            block.lineTail.setEmpty();
          }
          block.removeSurroundingEmptyLines();
          break;
        case OLIST:
        case ULIST:
          while (line != null) {
            final LineType t = line.getLineType();
            if (!line.isEmpty && (line.prevEmpty && line.leading == 0 && !(t == LineType.OLIST
                || t == LineType.ULIST))) {
              break;
            }
            line = line.next;
          }
          list = root.split(line != null ? line.previous : root.lineTail);
          list.type = type == LineType.OLIST ? BlockType.ORDERED_LIST : BlockType.UNORDERED_LIST;
          list.lines.prevEmpty = false;
          list.lineTail.nextEmpty = false;
          list.removeSurroundingEmptyLines();
          list.lines.prevEmpty = list.lineTail.nextEmpty = false;
          initListBlock(list);
          block = list.blocks;
          while (block != null) {
            this.recurse(block, true);
            block = block.next;
          }
          list.expandListParagraphs();
          break;
        default:
          line = line.next;
          break;
      }
    }
  }
}