/* global _ */

import RSVP from 'rsvp';
import { isEmberArray } from 'ember-array/utils';
import { isAscii } from 'web-client/helpers/format-display-name';

let _isRawToken = function (input) {
  return typeof(input) === "string";
};

let _createLinkToken = function (opts) {
  return _.extend(_createBaseToken(opts), {
    type: 'link',
    isDeleted: opts.isDeleted,
    isMailTo: opts.isMailTo
  });
};

let _createEmoticonToken = function (opts) {
  return _.extend(_createBaseToken(opts), {
    type: 'emoticon'
  });
};

let _createMentionToken = function (opts) {
  return _.extend(_createBaseToken(opts), {
    type: 'mention',
    isOwnMessage: opts.isOwnMessage
  });
};

let _createTextToken = function (opts) {
  return _.extend(_createBaseToken(opts), {
    type: 'text'
  });
};

// _createBaseToken creates a new object which defines the common
// properties and methods for parsed tokens.
let _createBaseToken = function (opts) {
  return _.extend({}, opts, {
    length: opts.length,
    hidden: opts.hidden || false
  });
};

let zeroPad = function (number, length) {
  length -= number.toString().length;

  if (length > 0) {
    return new Array(length + (/\./.test(number) ? 2 : 1)).join('0') + number;
  }

  return `${number}`; // always return a string
};

let getTime = function (date) {
  let mins = zeroPad(date.getMinutes(), 2);
  let hours = date.getHours();
  if (hours > 12) {
    hours -= 12;
  } else if (hours === 0) {
    hours = 12;
  }
  return `${hours}:${mins}`;
};

let processRegex = function (options) {
  if (!_.isString(options.input) || !options.input.match(options.regex)) {
    return _.isString(options.input) ? [options.input] : options.input;
  }

  let nonMatching = _.map(options.input.split(options.regex), options.transformNoMatch),
         matching = _.map(options.input.match(options.regex), options.transformMatch),
           zipped = _.zip(nonMatching, matching);
  return zipped;
};

/** Used to linkify */
/** based on http://stackoverflow.com/questions/3809401/ */
let linkRegex = /(?:https?:\/\/)?(?:[-a-zA-Z0-9@:%_\+~#=]+\.)+[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=()]*)/g;

let linkifyMessage = function (tokens, hideChatLinks) {
  let linkify = function (token) {
    return processRegex({
      input: token,
      regex: linkRegex,
      transformNoMatch: _.identity,
      transformMatch: function (link) {
        let _isEmailLink = (emailLink) => {
          return (emailLink.indexOf('@') > -1 && (emailLink.indexOf('/') === -1 || emailLink.indexOf('@') < emailLink.indexOf('/')));
        };

        let linkOpts = {
          length: link.length,
          // NOTE: Hide long links until chrome bug fixed -- https://codereview.chromium.org/1007323003/
          isDeleted: hideChatLinks || link.length > 255,
          isMailTo: _isEmailLink(link),

          text: link,
          link: link
        };
        if (!linkOpts.isMailTo && !link.match(/^(?:https?:\/\/)/)) {
          linkOpts.link = `http://${link}`;
        }

        return _createLinkToken(linkOpts);
      }
    });
  };
  return _.chain(tokens).map(linkify).flatten().compact().value();
};

let mentionizeMessage = function (tokens, receiver, isOwnMessage) {
  /* [\b] matches backspace, hence [@\\b] is unusable. */
  let mentionedRegex = new RegExp(`@?\\b${receiver}\\b`, 'ig');
  /* Usernames are 5 - 25 characters. */
  let mentioningRegex = /@(?:[a-z0-9]|[^\u0000-\u007F])(?:\w{4,25}|[^\u003A-\u007F\u0020-\u002F]{1,12})/gi;

  if (!isAscii(receiver)) {
    // User has a CJK displayname therefore we need a new regex.
    mentionedRegex = new RegExp(`@?${receiver}`, 'ig');
  }

  let mentionRegex = isOwnMessage ? mentioningRegex : mentionedRegex;

  let mentionize = function (token) {
    return processRegex({
      input: token,
      regex: mentionRegex,
      transformNoMatch: _.identity,
      transformMatch: function (user) {
        return _createMentionToken({
          length: user.length,
          user: user,
          isOwnMessage: isOwnMessage
        });
      }
    });
  };
  return _.chain(tokens).map(mentionize).flatten().compact().value();
};

// Input:
//  emoticons: { 25: [24, 28], 499: [[5, 6], [34, 35]], 490: [31, 32] }
// Output:
//  [ { emoticonId: 499, index: [5, 6] }, { emoticonId: 25, index: [24, 28] }, { emoticonId: 490, index: [31, 32] }, { emoticonId: 499, index: [34, 35] } ]
let createOrderedEmoticonTokens = function (emoticons) {
  let flattenedEmoticons = _.reduce(emoticons, function (flattened, indicesToReplace, emoticonId) {
    if (isEmberArray(indicesToReplace[0])) {
      indicesToReplace.forEach(function (index) {
        flattened.push({emoticonId: emoticonId, index: index});
      });
    } else {
      flattened.push({emoticonId: emoticonId, index: indicesToReplace});
    }
    return flattened;
  }, []);
  return _.sortBy(flattenedEmoticons, function (emoticon) {
    return emoticon.index[0];
  });
};

// Input:
//  message: ["Haha :) this is awesome Kappa! :p :)"]
//  emoticons: { 25: [24, 28], 499: [[5, 6], [34, 35]], 490: [31, 32] }
// Output:
//  [ 'Haha ', { emoticonId: "499" }, ' this is awesome ', { emoticonId: "25" }, '! ', { emoticonId: "490" }, ' ' , { emoticonId: "499" } ]
let emoticonizeMessage = function (message, emoticons) {
  if (message && emoticons) {
    emoticons = createOrderedEmoticonTokens(emoticons);
    emoticons.reverse();

    let tokenizedMessage = _.reduce(emoticons, function (tokens, emoticon) {
      // Since the emoticons are ordered in order of last appearance to first, we can expect the first token to always contain the next emoticon
      let newTokens = [],
          token = tokens.shift(),
          counter = 0;
      // For every token, check the length of the token and if it is less than the index of the emoticon, grab the next token and add to the counter
      while (counter + token.length - 1 < emoticon.index[0]) {
        newTokens.push(token);
        counter += token.length;
        token = tokens.shift();
      }

      if (_isRawToken(token)) {
        let prefix = token.slice(0, emoticon.index[0] - counter);
        if (prefix.length > 0) {
          newTokens.push(prefix);
        }

        newTokens.push(_createEmoticonToken({
          length: emoticon.index[1] - emoticon.index[0] + 1,
          imgSrc: `//static-cdn.jtvnw.net/emoticons/v1/${emoticon.emoticonId}/1.0`,
          srcSet: `//static-cdn.jtvnw.net/emoticons/v1/${emoticon.emoticonId}/2.0 2x`,
          altText: token.slice(emoticon.index[0] - counter, emoticon.index[1] + 1 - counter)
        }));

        let suffix = token.slice(emoticon.index[1] + 1 - counter);
        if (suffix.length > 0) {
          newTokens.push(suffix);
        }

        newTokens = newTokens.concat(tokens);
      } else {
        newTokens = newTokens.concat(token, tokens);
      }

      return newTokens;
    }, message);

    if (tokenizedMessage[tokenizedMessage.length - 1] === '') {
      tokenizedMessage.pop();
    }
    return tokenizedMessage;
  }
  return message;
};

/**
 * Rich Content tokenizer.
 *
 * If content.removeOriginal is true, this function will replace the raw string
 * representing rich content with a token that does not get rendered
 * and prevents subsequent tokenizers from trying to tokenize that raw string.
 *
 * If a non-raw (string) token is already present at the indices specified by content.index,
 * then this tokenizer will give up on that content.
 *
 * If content.removeOriginal is false, this function does nothing.
 *
 * Note: A key assumption is that content is non-overlapping. If one day we support overlapping content,
 *   this whole function will require rewriting.
 *
 * Note: This function is a tokenizer, meaning it is agnostic of and can coexist with other tokenizers
 *   regardless of order.
 */
let tokenizeRichContent = function (tokens, contents, hideChatLinks) {
  if (!contents) {
    return tokens;
  }

  if (typeof(tokens) === 'string') {
    tokens = [tokens];
  }


  let indicesToReplace = [];
  Object.keys(contents).forEach(contentType => {
    contents[contentType].forEach(content => {
      if (content.removeOriginal) {
        indicesToReplace.push(content.index);
      }
    });
  });

  indicesToReplace.sort((currentContent, nextContent) => {
    return nextContent.index[0] - currentContent.index[0];
  });

  let startIndex = 0;
  let endIndex = 0;
  let indicesIndex = 0;
  let contentStartIndex = 0;
  let contentEndIndex = 0;
  let newTokens = [];
  let numIndices = indicesToReplace.length;

  // Base case
  [contentStartIndex, contentEndIndex] = indicesToReplace[indicesIndex];

  tokens.forEach(token => {
    endIndex = startIndex + token.length;

    // Nothing to do if we're dealing with a non-string token
    if (typeof(token) !== 'string') {
      newTokens.push(token);
      return;
    }

    // Guarantee content's start index is greater than the token's start index, abandoning contents as necessary
    while (contentStartIndex < startIndex && indicesIndex < numIndices - 1) {
      indicesIndex += 1;
      [contentStartIndex, contentEndIndex] = indicesToReplace[indicesIndex];
    }

    // Tokenize if content fits within the token
    if (contentEndIndex <= endIndex) {
      let preString = token.slice(startIndex, contentStartIndex);
      let postString = token.slice(contentEndIndex, endIndex);
      if (preString) {
        newTokens.push(preString);
      }

      let length = contentEndIndex - contentStartIndex;
      let newToken;

      // Need Clips to respect "Block Hyperlinks" Channel setting
      if (hideChatLinks) {
        newToken = _createLinkToken({ length, isDeleted: true, isMailTo: false });
      } else {
        newToken = _createBaseToken({ length, hidden: true, type: 'content' });
      }

      newTokens.push(newToken);

      if (postString) {
        newTokens.push(postString);
      }
    }

    // Prepare next iteration
    startIndex += endIndex;
    while (startIndex > contentStartIndex) {
      if (indicesIndex >= numIndices - 1) {
        break;
      }

      indicesIndex += 1;
      [contentStartIndex, contentEndIndex] = indicesToReplace[indicesIndex];
    }
  });

  return newTokens;
};

// finalizeTokens is expected to be run last in the token parsing pipeline.
// It converts any raw text tokens into a token with HTML escaped text.
let finalizeTokens = function (tokens) {
  return _.map(tokens, (token) => {
    if (!_isRawToken(token)) {
      return token;
    }
    return _createTextToken({
      length: token.length,
      text: token
    });
  });
};

let capitalize = function (string) {
  string = string || '';
  return string.charAt(0).toUpperCase() + string.slice(1);
};

let sleep = function(time) {
  return new RSVP.Promise((resolve) => setTimeout(resolve, time));
};

export {
  getTime,
  linkifyMessage,
  mentionizeMessage,
  emoticonizeMessage,
  finalizeTokens,
  capitalize,
  tokenizeRichContent,
  sleep
};
