'use strict';

var decimal = require('is-decimal');
var alphanumeric = require('is-alphanumeric');
var whitespace = require('is-whitespace-character');
var escapes = require('markdown-escapes');
var prefix = require('./util/entity-prefix-length');

module.exports = factory;

var BACKSLASH = '\\';
var BULLETS = ['*', '-', '+'];
var ALLIGNMENT = [':', '-', ' ', '|'];
var entities = {'<': '&lt;', ':': '&#x3A;', '&': '&amp;', '|': '&#x7C;', '~': '&#x7E;'};

/* Factory to escape characters. */
function factory(options) {
  return escape;

  /* Escape punctuation characters in a node's value. */
  function escape(value, node, parent) {
    var self = this;
    var gfm = options.gfm;
    var commonmark = options.commonmark;
    var pedantic = options.pedantic;
    var markers = commonmark ? ['.', ')'] : ['.'];
    var siblings = parent && parent.children;
    var index = siblings && siblings.indexOf(node);
    var prev = siblings && siblings[index - 1];
    var next = siblings && siblings[index + 1];
    var length = value.length;
    var escapable = escapes(options);
    var position = -1;
    var queue = [];
    var escaped = queue;
    var afterNewLine;
    var character;
    var wordCharBefore;
    var wordCharAfter;
    var offset;
    var replace;

    if (prev) {
      afterNewLine = text(prev) && /\n\s*$/.test(prev.value);
    } else {
      afterNewLine = !parent || parent.type === 'root' || parent.type === 'paragraph';
    }

    function one(character) {
      return escapable.indexOf(character) === -1 ?
        entities[character] : BACKSLASH + character;
    }

    while (++position < length) {
      character = value.charAt(position);
      replace = false;

      if (character === '\n') {
        afterNewLine = true;
      } else if (
        character === BACKSLASH ||
        character === '`' ||
        character === '*' ||
        character === '[' ||
        character === '<' ||
        (character === '&' && prefix(value.slice(position)) > 0) ||
        (character === ']' && self.inLink) ||
        (gfm && character === '~' && value.charAt(position + 1) === '~') ||
        (gfm && character === '|' && (self.inTable || alignment(value, position))) ||
        (
          character === '_' &&
          /* Delegate leading/trailing underscores
           * to the multinode version below. */
          position > 0 &&
          position < length - 1 &&
          (
              pedantic ||
              !alphanumeric(value.charAt(position - 1)) ||
              !alphanumeric(value.charAt(position + 1))
          )
        ) ||
        (gfm && !self.inLink && character === ':' && protocol(queue.join('')))
      ) {
        replace = true;
      } else if (afterNewLine) {
        if (
          character === '>' ||
          character === '#' ||
          BULLETS.indexOf(character) !== -1
        ) {
          replace = true;
        } else if (decimal(character)) {
          offset = position + 1;

          while (offset < length) {
            if (!decimal(value.charAt(offset))) {
              break;
            }

            offset++;
          }

          if (markers.indexOf(value.charAt(offset)) !== -1) {
            next = value.charAt(offset + 1);

            if (!next || next === ' ' || next === '\t' || next === '\n') {
              queue.push(value.slice(position, offset));
              position = offset;
              character = value.charAt(position);
              replace = true;
            }
          }
        }
      }

      if (afterNewLine && !whitespace(character)) {
        afterNewLine = false;
      }

      queue.push(replace ? one(character) : character);
    }

    /* Multi-node versions. */
    if (siblings && text(node)) {
      /* Check for an opening parentheses after a
       * link-reference (which can be joined by
       * white-space). */
      if (prev && prev.referenceType === 'shortcut') {
        position = -1;
        length = escaped.length;

        while (++position < length) {
          character = escaped[position];

          if (character === ' ' || character === '\t') {
            continue;
          }

          if (character === '(' || character === ':') {
            escaped[position] = one(character);
          }

          break;
        }

        /* If the current node is all spaces / tabs,
         * preceded by a shortcut, and followed by
         * a text starting with `(`, escape it. */
        if (
          text(next) &&
          position === length &&
          next.value.charAt(0) === '('
        ) {
          escaped.push(BACKSLASH);
        }
      }

      /* Ensure non-auto-links are not seen as links.
       * This pattern needs to check the preceding
       * nodes too. */
      if (
        gfm &&
        !self.inLink &&
        text(prev) &&
        value.charAt(0) === ':' &&
        protocol(prev.value.slice(-6))
      ) {
        escaped[0] = one(':');
      }

      /* Escape ampersand if it would otherwise
       * start an entity. */
      if (
        text(next) &&
        value.charAt(length - 1) === '&' &&
        prefix('&' + next.value) !== 0
      ) {
        escaped[escaped.length - 1] = one('&');
      }

      /* Escape double tildes in GFM. */
      if (
        gfm &&
        text(next) &&
        value.charAt(length - 1) === '~' &&
        next.value.charAt(0) === '~'
      ) {
        escaped.splice(escaped.length - 1, 0, BACKSLASH);
      }

      /* Escape underscores, but not mid-word (unless
       * in pedantic mode). */
      wordCharBefore = text(prev) && alphanumeric(prev.value.slice(-1));
      wordCharAfter = text(next) && alphanumeric(next.value.charAt(0));

      if (length === 1) {
        if (value === '_' && (pedantic || !wordCharBefore || !wordCharAfter)) {
          escaped.unshift(BACKSLASH);
        }
      } else {
        if (
          value.charAt(0) === '_' &&
          (pedantic || !wordCharBefore || !alphanumeric(value.charAt(1)))
        ) {
          escaped.unshift(BACKSLASH);
        }

        if (
          value.charAt(length - 1) === '_' &&
          (pedantic || !wordCharAfter || !alphanumeric(value.charAt(length - 2)))
        ) {
          escaped.splice(escaped.length - 1, 0, BACKSLASH);
        }
      }
    }

    return escaped.join('');
  }
}

/* Check if `index` in `value` is inside an alignment row. */
function alignment(value, index) {
  var start = value.lastIndexOf('\n', index);
  var end = value.indexOf('\n', index);

  start = start === -1 ? -1 : start;
  end = end === -1 ? value.length : end;

  while (++start < end) {
    if (ALLIGNMENT.indexOf(value.charAt(start)) === -1) {
      return false;
    }
  }

  return true;
}

/* Check if `node` is a text node. */
function text(node) {
  return node && node.type === 'text';
}

/* Check if `value` ends in a protocol. */
function protocol(value) {
  var val = value.slice(-6).toLowerCase();
  return val === 'mailto' || val.slice(-5) === 'https' || val.slice(-4) === 'http';
}
