/***
   KeymanWeb 2.0
   Copyright 2017 SIL International
***/

/**
 * Constructs a string from one or more Unicode character codepoint values 
 * passed as integer parameters.
 * 
 * @param  {number} cp0,...   1 or more Unicode codepoints, e.g. 0x0065, 0x10000
 * @return {string|null}      The new String object.
 */
String.kmwFromCharCode = function(cp0) {
  var chars = [], i;
  for (i = 0; i < arguments.length; i++) {
    var c = Number(arguments[i]);
    if (!isFinite(c) || c < 0 || c > 0x10FFFF || Math.floor(c) !== c) {
      throw new RangeError("Invalid code point " + c);
	  }
    if (c < 0x10000) {
      chars.push(c);
    } else {
      c -= 0x10000;
      chars.push((c >> 10) + 0xD800);
      chars.push((c % 0x400) + 0xDC00);
    }
  }
  return String.fromCharCode.apply(undefined, chars);
}

/**
 * Returns a number indicating the Unicode value of the character at the given 
 * code point index, with support for supplementary plane characters.
 * 
 * @param  {number}  codePointIndex  The code point index into the string (not 
                                     the code unit index) to return
 * @return {number}                  The Unicode character value
 */
String.prototype.kmwCharCodeAt = function(codePointIndex) {
  var str = String(this);
  var codeUnitIndex = 0;
  
  if (codePointIndex < 0 || codePointIndex  >= str.length) {
    return NaN;
  }

  for(var i = 0; i < codePointIndex; i++) {
    codeUnitIndex = str.kmwNextChar(codeUnitIndex);
    if(codeUnitIndex === null) return NaN;
  }
  
  var first = str.charCodeAt(codeUnitIndex);
  if (first >= 0xD800 && first <= 0xDBFF && str.length > codeUnitIndex + 1) {
    var second = str.charCodeAt(codeUnitIndex + 1);
    if (second >= 0xDC00 && second <= 0xDFFF) {
      return ((first - 0xD800) << 10) + (second - 0xDC00) + 0x10000;
    }
  }
  return first;  
}

/**
 * Returns the code point index within the calling String object of the first occurrence
 * of the specified value, or -1 if not found.
 * 
 * @param  {string}  searchValue    The value to search for
 * @param  {number}  [fromIndex]    Optional code point index to start searching from
 * @return {number}                 The code point index of the specified search value
 */
String.prototype.kmwIndexOf = function(searchValue, fromIndex) {
  var str = String(this);
  var codeUnitIndex = str.indexOf(searchValue, fromIndex);
  
  if(codeUnitIndex < 0) {
    return codeUnitIndex;
  }
  
  var codePointIndex = 0;
  for(var i = 0; i !== null && i < codeUnitIndex; i = str.kmwNextChar(i)) codePointIndex++;
  return codePointIndex;
}

/**
 * Returns the code point index within the calling String object of the last occurrence 
 * of the specified value, or -1 if not found.
 * 
 * @param  {string}  searchValue    The value to search for
 * @param  {number}  fromIndex      Optional code point index to start searching from
 * @return {number}                 The code point index of the specified search value
 */
String.prototype.kmwLastIndexOf = function(searchValue, fromIndex)
{
  var str = String(this);
  var codeUnitIndex = str.lastIndexOf(searchValue, fromIndex);
  
  if(codeUnitIndex < 0) {
    return codeUnitIndex;
  }
  
  var codePointIndex = 0;
  for(var i = 0; i !== null && i < codeUnitIndex; i = str.kmwNextChar(i)) codePointIndex++;
  return codePointIndex;
}

/**
 * Returns the length of the string in code points, as opposed to code units.
 * 
 * @return {number}                 The length of the string in code points
 */
String.prototype.kmwLength = function() {
  var str = String(this);
  
  if(str.length == 0) return 0;
  
  for(var i = 0, codeUnitIndex = 0; codeUnitIndex !== null; i++) 
    codeUnitIndex = str.kmwNextChar(codeUnitIndex);
  return i;
}

/**
 * Extracts a section of a string and returns a new string.
 * 
 * @param  {number}  beginSlice    The start code point index in the string to 
 *                                 extract from
 * @param  {number}  endSlice      Optional end code point index in the string
 *                                 to extract to
 * @return {string}                The substring as selected by beginSlice and
 *                                 endSlice
 */
String.prototype.kmwSlice = function(beginSlice, endSlice) {
  var str = String(this);
  var beginSliceCodeUnit = str.kmwCodePointToCodeUnit(beginSlice);
  var endSliceCodeUnit = str.kmwCodePointToCodeUnit(endSlice);
  if(beginSliceCodeUnit === null || endSliceCodeUnit === null) 
    return '';
  else
    return str.slice(beginSliceCodeUnit, endSliceCodeUnit);
}

/**
 * Returns the characters in a string beginning at the specified location through
 * the specified number of characters.
 * 
 * @param  {number}  start         The start code point index in the string to 
 *                                 extract from
 * @param  {number}  length        Optional length to extract
 * @return {string}                The substring as selected by start and length
 */
String.prototype.kmwSubstr = function(start, length)
{
  var str = String(this);
  if(start < 0) 
  {
    start = str.kmwLength() + start;
  }
	if(start < 0) start = 0;
  var startCodeUnit = str.kmwCodePointToCodeUnit(start);
  var endCodeUnit = startCodeUnit;
  
  if(startCodeUnit === null) return '';
  
  if(arguments.length < 2) {
    endCodeUnit = str.length;
  } else {
    for(var i = 0; i < length; i++) endCodeUnit = str.kmwNextChar(endCodeUnit);
  }
  if(endCodeUnit === null)
    return str.substring(startCodeUnit);
  else
    return str.substring(startCodeUnit, endCodeUnit);
}

/**
 * Returns the characters in a string between two indexes into the string.
 * 
 * @param  {number}  indexA        The start code point index in the string to 
 *                                 extract from
 * @param  {number}  indexB        The end code point index in the string to 
 *                                 extract to
 * @return {string}                The substring as selected by indexA and indexB
 */
String.prototype.kmwSubstring = function(indexA, indexB)
{
  var str = String(this),indexACodeUnit,indexBCodeUnit;
  
  if(typeof(indexB) == 'undefined') 
  {
    indexACodeUnit = str.kmwCodePointToCodeUnit(indexA);
    indexBCodeUnit =  str.length;    
  } 
  else
  {
    if(indexA > indexB) { var c = indexA; indexA = indexB; indexB = c; }
  
    indexACodeUnit = str.kmwCodePointToCodeUnit(indexA);
    indexBCodeUnit = str.kmwCodePointToCodeUnit(indexB);
  }
  if(isNaN(indexACodeUnit) || indexACodeUnit === null) indexACodeUnit = 0;
  if(isNaN(indexBCodeUnit) || indexBCodeUnit === null) indexBCodeUnit = str.length;

  return str.substring(indexACodeUnit, indexBCodeUnit);
}

/*
  Helper functions
*/

/**
 * Returns the code unit index for the next code point in the string, accounting for
 * supplementary pairs 
 *
 * @param  {number|null}  codeUnitIndex  The code unit position to increment
 * @return {number|null}                 The index of the next code point in the string,
 *                                       in code units
 */
String.prototype.kmwNextChar = function(codeUnitIndex) {
  var str = String(this);

  if(codeUnitIndex === null || codeUnitIndex < 0 || codeUnitIndex >= str.length - 1) {
    return null;
  }
  
  var first = str.charCodeAt(codeUnitIndex);
  if (first >= 0xD800 && first <= 0xDBFF && str.length > codeUnitIndex + 1) {
    var second = str.charCodeAt(codeUnitIndex + 1);
    if (second >= 0xDC00 && second <= 0xDFFF) {
      if(codeUnitIndex == str.length - 2) {
        return null;
      }
      return codeUnitIndex + 2;
    }
  }
  return codeUnitIndex + 1;
}

/**
 * Returns the code unit index for the previous code point in the string, accounting
 * for supplementary pairs 
 *
 * @param  {number|null}  codeUnitIndex  The code unit position to decrement
 * @return {number|null}                 The index of the previous code point in the
 *                                       string, in code units
*/
String.prototype.kmwPrevChar = function(codeUnitIndex) {
  var str = String(this);

  if(codeUnitIndex == null || codeUnitIndex <= 0 || codeUnitIndex > str.length) {
    return null;
  }
  
  var second = str.charCodeAt(codeUnitIndex - 1);
  if (second >= 0xDC00 && second <= 0xDFFF && codeUnitIndex > 1) {
    var first = str.charCodeAt(codeUnitIndex - 2);
    if(first >= 0xD800 && first <= 0xDBFF) {
      return codeUnitIndex - 2;
    }
  }
  return codeUnitIndex - 1;
}

/**
 * Returns the corresponding code unit index to the code point index passed
 *
 * @param  {number|null} codePointIndex  A code point index in the string
 * @return {number|null}                 The corresponding code unit index
 */
String.prototype.kmwCodePointToCodeUnit = function(codePointIndex) {
  
  if(codePointIndex === null) return null;
  
  var str = String(this);
  var codeUnitIndex = 0;
  
  if(codePointIndex < 0) {
    codeUnitIndex = str.length;
    for(var i = 0; i > codePointIndex; i--) 
      codeUnitIndex = str.kmwPrevChar(codeUnitIndex);	
    return codeUnitIndex;
  }
  
  if(codePointIndex == str.kmwLength()) return str.length;
  
  for(var i = 0; i < codePointIndex; i++)
    codeUnitIndex = str.kmwNextChar(codeUnitIndex);
  return codeUnitIndex;
}

/**
 * Returns the corresponding code point index to the code unit index passed
 *
 * @param  {number|null}  codeUnitIndex  A code unit index in the string
 * @return {number|null}                 The corresponding code point index
 */
String.prototype.kmwCodeUnitToCodePoint = function(codeUnitIndex) {
  var str = String(this);
  
  if(codeUnitIndex === null)
    return null;
  else if(codeUnitIndex == 0)
    return 0;
  else if(codeUnitIndex < 0)     
    return str.substr(codeUnitIndex).kmwLength();
  else
    return str.substr(0,codeUnitIndex).kmwLength();
}

/**
 * Returns the character at a the code point index passed
 *
 * @param  {number}  codePointIndex  A code point index in the string
 * @return {string}                  The corresponding character
 */
String.prototype.kmwCharAt = function(codePointIndex) {
  var str = String(this);
  
  if(codePointIndex >= 0) return str.kmwSubstr(codePointIndex,1); else return '';
}

/**
 * String prototype library extensions for basic plane characters, 
 * to simplify enabling or disabling supplementary plane functionality (I3319)
 */
 
/**
 * Returns the code unit index for the next code point in the string
 *
 * @param  {number}  codeUnitIndex    A code point index in the string
 * @return {number|null}                   The corresponding character
 */ 
String.prototype.kmwBMPNextChar = function(codeUnitIndex)
{
  var str = String(this);
  if(codeUnitIndex < 0 || codeUnitIndex >= str.length - 1) {
    return null;
  }
  return codeUnitIndex + 1;
}  

/**
 * Returns the code unit index for the previous code point in the string
 *
 * @param  {number}  codeUnitIndex    A code unit index in the string
 * @return {number|null}                   The corresponding character
 */ 
String.prototype.kmwBMPPrevChar = function(codeUnitIndex)
{
  var str = String(this);

  if(codeUnitIndex <= 0 || codeUnitIndex > str.length) {
    return null;
  }
  return codeUnitIndex - 1;
}  

/**
 * Returns the code unit index for a code point index
 *
 * @param  {number}  codePointIndex   A code point index in the string
 * @return {number}                   The corresponding character
 */ 
String.prototype.kmwBMPCodePointToCodeUnit = function(codePointIndex)
{
  return codePointIndex;
}

/**
 * Returns the code point index for a code unit index
 *
 * @param  {number}  codeUnitIndex    A code point index in the string
 * @return {number}                   The corresponding character
 */ 
String.prototype.kmwBMPCodeUnitToCodePoint = function(codeUnitIndex)
{
  return codeUnitIndex;
}

/**
 * Returns the length of a BMP string
 *
 * @return {number}                   The length in code points
 */ 
String.prototype.kmwBMPLength = function()
{
  var str = String(this);
  return str.length;
}


/**
 * Returns a substring 
 *
 * @param  {number}  n
 * @param  {number}  ln
 * @return {string}                   
 */ 
String.prototype.kmwBMPSubstr = function(n,ln)
{
  var str=String(this);
  if(n > -1) 
    return str.substr(n,ln);
  else
    return str.substr(str.length+n,-n);  
}

/**
 * Enable or disable supplementary plane string handling
 *
 * @param  {boolean}  bEnable
 */ 
String.kmwEnableSupplementaryPlane = function(bEnable)
{
  var p=String.prototype;
  String._kmwFromCharCode = bEnable ? String.kmwFromCharCode : String.fromCharCode;
  p._kmwCharAt = bEnable ? p.kmwCharAt : p.charAt;
  p._kmwCharCodeAt = bEnable ? p.kmwCharCodeAt : p.charCodeAt;
  p._kmwIndexOf = bEnable ? p.kmwIndexOf :p.indexOf;
  p._kmwLastIndexOf = bEnable ? p.kmwLastIndexOf : p.lastIndexOf ;
  p._kmwSlice = bEnable ? p.kmwSlice : p.slice;
  p._kmwSubstring = bEnable ? p.kmwSubstring : p.substring;
  p._kmwSubstr = bEnable ? p.kmwSubstr : p.kmwBMPSubstr;
  p._kmwLength = bEnable ? p.kmwLength : p.kmwBMPLength;
  p._kmwNextChar = bEnable ? p.kmwNextChar : p.kmwBMPNextChar;
  p._kmwPrevChar = bEnable ? p.kmwPrevChar : p.kmwBMPPrevChar;
  p._kmwCodePointToCodeUnit = bEnable ? p.kmwCodePointToCodeUnit : p.kmwBMPCodePointToCodeUnit;
  p._kmwCodeUnitToCodePoint = bEnable ? p.kmwCodeUnitToCodePoint : p.kmwBMPCodeUnitToCodePoint;
}

