//    Hikij
//    markdown.js
//    Copyright 2006 Matthew Sackman
//
//    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.

function markdownToHTML(text, pages) {
  var formatter = buildFormatter();
  formatter.knownPages = pages;
  var result = formatter.format(text);
  result = doRefLinksSubstitutions(result, formatter.linkRefs);
  result = doRefImgsSubstitutions(result, formatter.imgRefs);
  return result;
}

function doRefLinksSubstitutions(text, links) {
  var swap = function(linkObj, _idx, _ary) {
    var reString = '(?:\\[(' + linkObj.id + ')\\][ \\t]*\\[\\])|(?:\\[([^\\]]+)\\][ \\t]*\\[' + linkObj.id + '\\])';
    var re = new RegExp(reString, 'gi');
    text = text.replace(re,
                        function(_matched, id1, linkText, _str, _offset) {
                          var linkText = linkText ? linkText : id1;
                          var title = linkObj.title ? ' title="' + linkObj.title + '"' : '';
                          return '<a href="' + linkObj.url + '"' + title + '>' + linkText + '</a>';
                        }
                       );
  };
  links.forEach(swap);
  return text;
}

function doRefImgsSubstitutions(text, imgs) {
  var swap = function(linkObj, _idx, _ary) {
    var reString = '!(?:(?:\\[(' + linkObj.id + ')\\][ \\t]*\\[\\])|(?:\\[([^\\]]+)\\][ \\t]*\\[' + linkObj.id + '\\]))';
    var re = new RegExp(reString, 'gi');
    text = text.replace(re,
                        function(_matched, id1, altText, _str, _offset) {
                          var altText = altText ? altText : id1;
                          var title = linkObj.title ? ' title="' + linkObj.title + '"' : '';
                          return '<img src="' + linkObj.url + '"' + title + ' alt="' + altText + '" />';
                        }
                       );
  };
  imgs.forEach(swap);
  return text;
}

function buildFormatterClone(formatter) {
  var formatterClone = buildFormatter();
  formatterClone.wrapRemainingInParaOrig = formatter.wrapRemainingInParaOrig;
  formatterClone.knownPages = formatter.knownPages;
  formatterClone.linkRefs = formatter.linkRefs;
  formatterClone.imgRefs = formatter.imgRefs;
  return formatterClone;
}

function buildFormatter() {
  var formatter = new Object();
  formatter.wrapRemainingInParaOrig = true;

  formatter.linkRefs = new Array();
  formatter.imgRefs = new Array();

  formatter.paraSplitters = new Array();
  formatter.paraSplitters.push(getSimpleParaSplitter());

  formatter.blockFormatters = new Array();
  formatter.blockFormatters.push(getBlockHeadersUnderline());
  formatter.blockFormatters.push(getBlockHeadersHash());
  formatter.blockFormatters.push(getBlockHR());
  formatter.blockFormatters.push(getBlockCoder());
  formatter.blockFormatters.push(getBlockQuoter());
  formatter.blockFormatters.push(getBlockLister());

  formatter.spanFormatters = new Array();
  formatter.spanFormatters.push(getSpanReferenceSlurper());
  formatter.spanFormatters.push(getSpanCode());
  formatter.spanFormatters.push(getSpanWikiLinker());
  formatter.spanFormatters.push(getSpanImager());
  formatter.spanFormatters.push(getSpanLinker());
  formatter.spanFormatters.push(getSpanBR());
  formatter.spanFormatters.push(getSpanBoldItalic());
  formatter.spanFormatters.push(getSpanAutoLinks());

  formatter.invokeNextBlockFormatter = getNextBlockFun(formatter);
  formatter.invokeNextSpanFormatter = getNextSpanFun(formatter);
  formatter.format = getFormatFun(formatter);

  return formatter;
}

function encodeAmpsAngles(text) {
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

function getNextBlockFun(formatter) {
  return function() {
    if (1 + formatter.blockIdx >= formatter.blockFormatters.length) {
      formatter.invokeNextSpanFormatter();
    } else {
      formatter.blockIdx++;
      var formatterFun = formatter.blockFormatters[formatter.blockIdx];
      formatterFun(formatter);
    }
  };
}

function getNextSpanFun(formatter) {
  return function() {
    if (1 + formatter.spanIdx >= formatter.spanFormatters.length) {
      return;
    } else {
      formatter.spanIdx++;
      var formatterFun = formatter.spanFormatters[formatter.spanIdx];
      formatterFun(formatter);
    }
  };
}

function getFormatFun(formatter) {
  return function(text) {
    if (! text) {
      return '';
    }
    formatter.wrapRemainingInPara = formatter.wrapRemainingInParaOrig;
    formatter.remaining = text;
    var paraFun = formatter.paraSplitters[formatter.paraSplitters.length - 1];
    paraFun(formatter);

    formatter.unitDone = new Array();
    formatter.blockIdx = -1;
    formatter.spanIdx = -1;

    formatter.invokeNextBlockFormatter();

    if (formatter.unit) {
      if (formatter.wrapRemainingInPara) {
        formatter.unitDone.push('<p>' + formatter.unit + '</p>');
      } else {
        formatter.unitDone.push(formatter.unit);
      }
      formatter.unit = '';
    }
    var result = formatter.unitDone.join('\n');

    if (formatter.remaining) {
      result += '\n' + formatter.format(formatter.remaining);
    }
    return result;
  };
}

function getSimpleParaSplitter() {
  return function(state) {
    var text = state.remaining;
    if (undefined != text && null != text) {
      text = text.replace(/^([ \t]*\n)+/g, '');
      var matches = text.match(/^((?:.+\n)*.+)(?:$|(?:(?:\n)(?:[ \t]*\n)+))((?:.|\n)*)/);
      if (matches) {
        state.unit = matches[1];
        state.remaining = matches[2] ? matches[2] : '';
      } else {
        state.unit = text;
        state.remaining = '';
      }
    } else {
      alert("Unable to split text into paragraphs:\n'" + text + "'");
    }
    return state;
  };
}

function getBlockQuoter() {
  return function(state) {
    if (/^>[ \t]?/.test(state.unit)) {
      var inner = state.unit.replace(/^>[ \t]?/mg, '');
      var formatter = buildFormatterClone(state);
      formatter.wrapRemainingInParaOrig = true;
      state.unitDone.push('<blockquote>' + formatter.format(inner) + '</blockquote>');
      state.unit = '';
    } else {
      state.invokeNextBlockFormatter();
    }
  };
}

function getBlockCoder() {
  return function(state) {
    if (/^(?:(?:    )|\t)/.test(state.unit)) {
      var matches = state.unit.match(/^((?:(?:(?:    )|\t)(?:.*)(?:\n(?=(    )|\t))?)+)((?:.|\n)*)$/);
      if (matches) {
        var code = matches[1].replace(/^(?:(?:    )|\t)/mg, '');
        state.unitDone.push('<pre><code>' + encodeAmpsAngles(code) + '</pre></code>');
        var formatter = buildFormatterClone(state);
        state.unitDone.push(formatter.format(matches[2]));
        state.unit = '';
      } else {
        alert('Error on code block parser: ' + state.unit);
      }
    }
    state.invokeNextBlockFormatter();
  };
}

function getBlockLister() {
  return function(state) {
    var testerRe = /^(?:(?:\d+\.)|(?:[\+\*\-]))(?: |\t)+/;
    if (state.inList) {
      testerRe = /^(?:(?:\d+\.)|(?:[\+\*\-]))(?: |\t)+/m;
    }
    if (testerRe.test(state.unit)) {

      var innerFormatter = buildFormatterClone(state);
      innerFormatter.inList = false;

      state.unit.replace(testerRe,
                         function(all, offset, _str) {
                           var pre = state.unit.substr(0, offset);
                           if (pre) {
                             state.unitDone.push(innerFormatter.format(pre));
                             state.unit = state.unit.substr(offset);
                           }
                         }
                        );

      innerFormatter.inList = true;
      var listEnd = '';
      var listChar = '';
      if (/^(?:\d+\.)/.test(state.unit)) {
        state.unitDone.push('<ol>');
        listEnd = '</ol>';
        testerRe = /^(?:\d+\.)(?: |\t)+/;
        if (state.inList) {
          testerRe = /^(?:\d+\.)(?: |\t)+/m;
        }
        listChar = "\\d+\\.";
      } else {
        state.unitDone.push('<ul>');
        listEnd = '</ul>';
        var char = /^(.)/.exec(state.unit)[0];
        testerRe = new RegExp("^(?:[" + char + "])(?: |\\t)+", '');
        if (state.inList) {
          testerRe = new RegExp("^(?:[" + char + "])(?: |\\t)+", 'm');
        }
        listChar = "[" + char + "]";
      }

      var paraSplitter = getSimpleParaSplitter();
      var listElems = new Array();

      var innerParaRe = /^(?:(?:    )|\t)/;
      var nextParaObject = new Object();
      nextParaObject.unit = state.unit;
      nextParaObject.remaining = state.remaining;

      do {
        var elem = nextParaObject.unit;
        paraSplitter(nextParaObject);

        while (innerParaRe.test(nextParaObject.unit)) {
          elem += '\n\n' + nextParaObject.unit;
          paraSplitter(nextParaObject);
        }
        listElems.push(elem);
      } while (testerRe.test(nextParaObject.unit));

      state.unit = '';
      var extra = nextParaObject.unit ? nextParaObject.unit + '\n\n' : '';
      state.remaining = extra + nextParaObject.remaining;

      var formatFunction = function(e, _idx, _ary) {
        innerFormatter.wrapRemainingInParaOrig = /^$\n[ \t]*\S+/m.test(e);
        state.unitDone.push('<li>' + innerFormatter.format(e) + '</li>');
      };

      for (var idx = 0; idx < listElems.length; ++idx) {
        var allListItems = new Array();
        var leftOver = explodeListPara(listChar, listElems[idx], allListItems);

        if (idx + 1 < listElems.length) {
          // there's another para so the last list in this para is a p
          var lastItem = allListItems.pop();
          allListItems.forEach(formatFunction);
          innerFormatter.wrapRemainingInParaOrig = true;
          state.unitDone.push('<li>' + innerFormatter.format(lastItem) + '</li>');
        } else {
          // we're on the last para
          if (listElems.length == 1) {
            // if only para then all not p
            allListItems.forEach(formatFunction);

          } else {
            // if more than 1 elem then first is p, rest aren't else p
            var firstItem = allListItems.shift();
            innerFormatter.wrapRemainingInParaOrig = true;
            state.unitDone.push('<li>' + innerFormatter.format(firstItem) + '</li>');
            
            allListItems.forEach(formatFunction);
          }
        }

        if (leftOver) {
          var remaining = leftOver;
          if (idx + 1 < listElems.length) {
            remaining = remaining + "\n\n" + (listElems.slice(idx + 1).join("\n\n"));
            idx = listElems.length;
          }
          state.remaining = remaining + (state.remaining ? "\n\n" + state.remaining : '');
        }
          
      }

      state.unitDone.push(listEnd);


    } else {
      state.invokeNextBlockFormatter();
    }
  };
}

function explodeListPara(char, text, ary) {
  var re = new RegExp("^(?:[ \\t]*\\n*)(?:(?:" + char + ")[ \\t]+)((?:.+|\\n(?!((\\d+\\.)|([\\+\\*\\-]))))*)", '');
  var simpleItem = null;
  while (simpleItem = re.exec(text)) {
    var item = simpleItem[1].replace(/^(?:(?:    )|\t)/gm, '');
    ary.push(item);
    text = text.substr(simpleItem[0].length);
  }
  return text;
}

function getBlockHeadersHash() {
  return function(state) {
    if (/^#{1,6}( |\t)*(.+)/m.test(state.unit)) {
      state.unit.replace(/^(#{1,6})(?: |\t)*(.+)(?: |\t)*(?:#{0,6})(?: |\t)*$/m,
                         function(all, g1, g2, offset, _str) {
                           var pre = state.unit.substr(0, offset);
                           var post = state.unit.substr(offset + all.length);
                           var header = '<h' + g1.length + '>' + g2 + '</h' + g1.length + '>';
                           var innerFormatter = buildFormatterClone(state);
                           if (pre) {
                             state.unitDone.push(innerFormatter.format(pre));
                           }
                           innerFormatter.wrapRemainingInParaOrig = false;
                           state.unitDone.push(innerFormatter.format(header));
                           if (post) {
                             state.remaining = post + '\n\n' + state.remaining;
                           }
                           state.unit = '';
                         }
                        );

    } else {
      state.invokeNextBlockFormatter();
    }
  };
}

function getBlockHeadersUnderline() {
  return function(state) {
    if (/^(?: |\t)*(.+?)(?: |\t)*\n(=+|-+)(?: |\t)*$/m.test(state.unit)) {
      state.unit.replace(/^(?: |\t)*(.+?)(?: |\t)*\n(=+|-+)(?: |\t)*$/m,
                         function(all, g1, g2, offset, _str) {
                           var pre = state.unit.substr(0, offset);
                           var post = state.unit.substr(offset + all.length);
                           var number = g1.match(/^=/) ? 1 : 2;
                           var header = '<h' + number + '>' + g1 + '</h' + number + '>';
                           var innerFormatter = buildFormatterClone(state);
                           if (pre) {
                             state.unitDone.push(innerFormatter.format(pre));
                           }
                           innerFormatter.wrapRemainingInParaOrig = false;
                           state.unitDone.push(innerFormatter.format(header));
                           if (post) {
                             state.remaining = post + '\n\n' + state.remaining;
                           }
                           state.unit = '';
                         }
                        );
    } else {
      state.invokeNextBlockFormatter();
    }
  }
}

function getBlockHR() {
  return function(state) {
    if (/^((( |\t)*\*){3,}( |\t)*)|((( |\t)*-){3,}( |\t)*)$/m.test(state.unit)) {
       state.unit.replace(/^(?:(?:(?: |\t)*\*){3,}(?: |\t)*)|(?:(?:(?: |\t)*-){3,}(?: |\t)*)$/m,
                         function(all, offset, _str) {
                           var pre = state.unit.substr(0, offset);
                           var post = state.unit.substr(offset + all.length);
                           var innerFormatter = buildFormatterClone(state);
                           if (pre) {
                             state.unitDone.push(innerFormatter.format(pre));
                           }
                           state.unitDone.push('<hr />');
                           if (post) {
                             state.remaining = post + '\n\n' + state.remaining;
                           }
                           state.unit = '';
                         }
                        );
    } else {
      state.invokeNextBlockFormatter();
    }
  };
}

function getSpanWikiLinker() {
  return function (state) {
    state.unit = state.unit.replace(/\b[A-Z][^ ]*[a-z][A-Z][^ ]*\b/g,
                                    function(matched, _str, _offset) {
                                      return makeWikiLinkString(matched, state.knownPages);
                                    }
                                   );
    state.invokeNextSpanFormatter();
  };
}

function getSpanLinker() {
  return function(state) {
    state.unit = state.unit.replace(/\[([^\]]+)\]\(([^)]+)\)/g,
                                    function(_matched, g1, g2, _str, _offset) {
                                      var title = g2.match(/ "([^"]+)"\s*$/);
                                      if (null == title || 0 == title.length) {
                                        return '<a href="' + g2 + '">' + g1 + '</a>';
                                      } else {
                                        var href = g2.match(/^(.+) "([^"]+)"\s*$/)[1];
                                        title = title[1];
                                        return '<a href="' + href + '" title="' + title + '">' + g1 + '</a>';
                                      }
                                    }
                                   );
    state.invokeNextSpanFormatter();
  };
}

function getSpanImager() {
  return function(state) {
    state.unit = state.unit.replace(/!\[([^\]]*)\]\(([^)]+)\)/g,
                                    function(_matched, g1, g2, _str, _offset) {
                                      var title = g2.match(/ "([^"]+)"\s*$/);
                                      if (null == title || 0 == title.length) {
                                        return '<img src="' + g2 + '" alt="' + g1 + '" />';
                                      } else {
                                        var href = g2.match(/^(.+) "([^"]+)"\s*$/)[1];
                                        title = title[1];
                                        return '<img src="' + href + '" title="' + title + '" alt="' + g1 + '" />';
                                      }
                                    }
                                   );
    state.invokeNextSpanFormatter();
  };
}

function getSpanBR() {
  return function (state) {
    state.unit = state.unit.replace(/\s{2,}$/mg, '<br />');
    state.invokeNextSpanFormatter();
  };
}

function getSpanBoldItalic() {
  return function (state) {
    var text = state.unit;
    text = text.replace(/(__)(.+?)(__)/g, "<strong>$2</strong>");
    text = text.replace(/(\*\*)(.+?)(\*\*)/g, "<strong>$2</strong>");

    text = text.replace(/(_)(.+?)(_)/g, "<em>$2</em>");
    text = text.replace(/(\*)(.+?)(\*)/g, "<em>$2</em>");

    state.unit = text;
    state.invokeNextSpanFormatter();
  };
}

function getSpanCode() {
  return function (state) {
    state.unit = state.unit.replace(/(`+)((?:.|[\n\r])+?)\1/g,
                                    function(_matched, _g1, g2, _str, _offset) {
                                      return "<code>" + encodeAmpsAngles(g2) + "</code>";
                                    }
                                   );
    state.invokeNextSpanFormatter();
  };
}

function getSpanAutoLinks() {
  return function (state) {
    state.unit = state.unit.replace(/<([^ @\f\n\r\t]+?@[^ @\f\n\r\t]+?)>/g,
                                    '<a href="mailto:$1">$1</a>');
    state.unit = state.unit.replace(/<((?:http(?:s)?|ftp|gopher):\/\/[^ \f\n\r\t]+)>/g,
                                    '<a href="$1">$1</a>');
    state.invokeNextSpanFormatter();
  };
}

function getSpanReferenceSlurper() {
  return function(state) {
    state.unit = state.unit.replace(/^[ \t]*(!?)\[([^\]]+)\]:[ \t]+<?([^ \f\n\r\t]+)>?(?:[ \t]+(?:\n[ \t]*)?(?:(?:(["'])(.+)\4)|(?:\((.+)\))))?[ \t]*$\n?/mg,
                                    function(_matched, bang, id, url, _sep, title1, title2, _str, _offset) {
                                      var title = title1 ? title1 : title2;
                                      var linkObj = new Object();
                                      linkObj.title = title;
                                      linkObj.url = url;
                                      linkObj.id = id;
                                      if (bang) {
                                        state.imgRefs.push(linkObj);
                                      } else {
                                        state.linkRefs.push(linkObj);
                                      }
                                      return '';
                                    }
                                   );
    state.invokeNextSpanFormatter();
  };
}
