"use strict";

const js2xmlparser = require('js2xmlparser');
const js2xmlOption = { format: { doubleQuotes: true } };

/**
 * Return the first valid element
 * 
 * @param {Array.<any>} array 
 */
function valueQueue(array) {
  for (var i = 0; i < array.length; i++) {
    if (typeof array[i] !== 'undefined') {
      return array[i];
    }
  }
}
exports.valueQueue = valueQueue;

/**
 * Round all number value in a javascript object at given decimal
 * 
 * @param {any} obj - Javascript object to be rounded 
 * @param {number} [decimal=0] - Round at this decimal 
 * @param {boolean} [strict=false] - set to `true` won't try to cast `string` to `number`  
 */
function roundAllValue (obj, decimal = 0, strict = false) {
  Object.keys(obj).forEach(key => {
    if (typeof(obj[key]) === "object" && obj[key] !== null) {
      roundAllValue (obj[key], decimal, strict);
    } else if(isNumeric(obj[key], strict)) {
      const num = parseFloat(obj[key]);
      obj[key] = roundNumber(num, decimal);
    }
  });
}
exports.roundAllValue = roundAllValue;

/**
 * Round a given value at desire decimal
 * 
 * @param {number} num 
 * @param {number} scale 
 * @return {number}
 */
function roundNumber(num, scale) {
  if(!("" + num).includes("e")) {
    return +(Math.round(num + "e+" + scale)  + "e-" + scale);
  } else {
    const arr = ("" + num).split("e");
    let sig = ""
    if(+arr[1] + scale > 0) {
      sig = "+";
    }
    return +(Math.round(+arr[0] + "e" + sig + (+arr[1] + scale)) + "e-" + scale);
  }
}
exports.roundNumber = roundNumber;

/**
 * Stringify javascript object to BMFont compatible json or xml
 * 
 * @param {Object} data - Java object data 
 * @param {string} outputType - Type of output "xml"(default) "json"
 * 
 */
function stringify(data, outputType) {
  if (outputType === "json") {
    return toJSON(data);
  } if (outputType === "xml")  {
    return toBMFontXML(data);
  } else {
    return toBMFontASCII(data);
  }
}
exports.stringify = stringify;

function toJSON(data) {
  return JSON.stringify(data, null, 4);
}

function addNumberParam(obj, param) {
  return ' ' + param + '=' + obj[param];
}

function toBMFontASCII(data) {
  console.log(data.info);
  let output = "";
  {
    output += "info" +
      ' face="' + data.info.face + '"' + 
      addNumberParam(data.info, "size") +
      addNumberParam(data.info, "bold") +
      addNumberParam(data.info, "italic") +
      ' charset=""' + 
      addNumberParam(data.info, "unicode") +
      addNumberParam(data.info, "stretchH") +
      addNumberParam(data.info, "smooth") +
      addNumberParam(data.info, "aa") +
      ' padding=' + stringifyArray(data.info.padding, ',') +
      ' spacing=' + stringifyArray(data.info.spacing, ',') +
      ' outline=0\n';
  }
  {
    output += "common" +
      addNumberParam(data.common, "lineHeight") +
      addNumberParam(data.common, "base") +
      addNumberParam(data.common, "scaleW") +
      addNumberParam(data.common, "scaleH") +
      addNumberParam(data.common, "pages") +
      addNumberParam(data.common, "packed") +
      addNumberParam(data.common, "alphaChnl") +
      addNumberParam(data.common, "redChnl") +
      addNumberParam(data.common, "greenChnl") +
      addNumberParam(data.common, "blueChnl") +
      '\n';
  }
  data.pages.forEach((p, i) => {
    output += 'page id=' + i + ' file="' + p + '"\n';
  });
  output += "chars count=" + data.chars.length + "\n";
  data.chars.sort((a, b) => {return a.id - b.id;});
  data.chars.forEach(c =>{
    output += "char" +
      addNumberParam(c, "id") +
      addNumberParam(c, "x") +
      addNumberParam(c, "y") +
      addNumberParam(c, "width") +
      addNumberParam(c, "height") +
      addNumberParam(c, "xoffset") +
      addNumberParam(c, "yoffset") +
      addNumberParam(c, "xadvance") +
      addNumberParam(c, "page") +
      addNumberParam(c, "chnl") +
      '\n';
  });
  output += "kernings count=" + data.kernings.length + "\n";
  data.kernings.forEach(k =>{
    output += "kerning" +
      addNumberParam(k, "first") +
      addNumberParam(k, "second") +
      addNumberParam(k, "amount") +
      '\n';
  });
  return output;
}

function toBMFontXML(data) {
  let xmlData = {};
  
  // Reorganize data structure
  // Definition: http://www.angelcode.com/products/bmfont/doc/file_format.html

  // info section
  xmlData.info = {};
  xmlData.info['@'] = data.info;
  xmlData.info['@'].padding = stringifyArray(data.info.padding, ',');
  xmlData.info['@'].spacing = stringifyArray(data.info.spacing, ',');
  // xmlData.info['@'].charset = stringifyArray(data.info.charset);
  xmlData.info['@'].charset = ""; 

  // common section
  xmlData.common = {};
  xmlData.common['@'] = data.common;

  // pages section, page shall be inserted later in module function callback
  xmlData.pages = {};
  xmlData.pages.page = []; 
  data.pages.forEach((p, i) => {
    let page = {};
    page['@'] = {id: i, file: p};
    xmlData.pages.page.push(page);
  });

  // distanceField section
  xmlData.distanceField = {};
  xmlData.distanceField['@'] = data.distanceField;

  // chars section
  xmlData.chars = {'@': {}};
  xmlData.chars['@'].count = data.chars.length;
  xmlData.chars.char = [];
  data.chars.sort((a, b) => {return a.id - b.id;});
  data.chars.forEach(c =>{
    let char = {};
    char['@'] = c;
    xmlData.chars.char.push(char);
  });

  // kernings section
  xmlData.kernings = {'@': {}};
  xmlData.kernings['@'].count = data.kernings.length;
  xmlData.kernings.kerning = [];
  data.kernings.forEach(k => {
    let kerning = {};
    kerning['@'] = k;
    xmlData.kernings.kerning.push(kerning);
  });
  
  return js2xmlparser.parse("font", xmlData, js2xmlOption);
}

/**
 * Stringify the given array, seperated by seperator
 * 
 * @param {any} array 
 * @param {string} [seperator=""] 
 * @returns {string} 
 */
function stringifyArray(array, seperator = "") {
  let result = "";
  let lastIndex = array.length - 1;
  array.forEach((element, index) => {
    result += element;
    if (index !== lastIndex){
      result += seperator;
    }
  });
  return result;
}
exports.stringifyArray = stringifyArray;

/**
 * Tell if the given object is string 
 * 
 * @param {any} n 
 * @returns {boolean} 
 */
function isString (s) {
  return (typeof s === 'string' || s instanceof String);
}
exports.isString = isString;

/**
 * Tell if the given object is numeric 
 * 
 * @param {any} n 
 * @param {boolean} [strict=false] strict casting
 * @returns {boolean} 
 */
function isNumeric (n, strict = false) {
  return !(isString(n) && strict) && !isNaN(parseFloat(n)) && isFinite(n);
}
exports.isNumeric = isNumeric;

/**
 * Tell if the given object is empty
 * 
 * @param {any} obj 
 * @returns 
 */
function isEmpty (obj) {
  if (Object.getOwnPropertyNames(obj).length > 0) return false;
  else return true;
}
exports.isEmpty = isEmpty;

function insidePath (command, contours) {
  let x = command.x, y = command.y;
  let inside = false;
  contours.forEach(contour => {
    for (let i = 0, j = contour.length - 1; i < contour.length; j = i++) {
      let xi = contour[i].x, yi = contour[i].y;
      let xj = contour[j].x, yj = contour[j].y;

      let intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
      if (intersect) inside = !inside;
    }
    if (inside) return;
  });
  return inside;
}

function isClockwise (contour) {
  let sum = 0;
  for (let i = 0; i < contour.length - 1; i++) {
      let command = contour[i], command_next = contour[i+1];
      if(command_next.type == 'Z') break;
      sum += (command_next.x - command.x) * (command_next.y + command.y);
  }
  return sum > 0
}

function reverseContour (contour) {
  let reversedContour = [];
  let tmpPoint = [];
  let hasCloseCmd = false;
  let isFirstCmd = true;

  for (let i = contour.length - 1; i > 0; i--) {
    let command = contour[i];
    let rev_comand = {};
    if (command.type === 'Z') {
      hasCloseCmd = true;
      continue;
    }
    if (isFirstCmd) {
      isFirstCmd = false;
      reversedContour.push({type: 'M', x: command.x, y: command.y});
    }
    rev_comand.type = command.type;
    rev_comand.x = contour[i - 1].x;
    rev_comand.y = contour[i - 1].y;
    if (command.type === 'C') {
      rev_comand.x1 = command.x2;
      rev_comand.y1 = command.y2;
      rev_comand.x2 = command.x1;
      rev_comand.y2 = command.y1;
    } else if (command.type === 'Q') {
      rev_comand.x1 = command.x1;
      rev_comand.y1 = command.y1;
    }
    reversedContour.push(rev_comand);
  }
  if (hasCloseCmd) {
    reversedContour.push({type: 'Z'});
  }
  return reversedContour;
}

/**
 * Align all contours' clockwiseness.
 * 
 * @param {Array} contours  Array of contour 
 * @param {boolean} direction  true for clockwise, false for counter-clockwise
 */
function alignClockwise(contours, direction) {
  let numReversed = 0;
  for (let i = 0; i < contours.length; i++) {
    let contour = contours[i];
    let restContours = contours.slice(0);
    restContours.splice(i, 1);
    if (contour.length === 0) continue;
    let isInside =  insidePath(contour[0], restContours) && 
                    insidePath(contour[Math.ceil(contour.length / 2)], restContours);
    let dir = isInside ? (!direction) : direction;
    if (isClockwise(contour) != dir) {
      contours[i] = reverseContour(contour);
      numReversed ++;
    }
  }
  return numReversed;
}
exports.alignClockwise = alignClockwise; 

/**
 * Convert contour commands to msdfgen shape description 
 * 
 * @param {Array} contours Array of font contour
 * @returns {string} 
 */
function stringifyContours(contours) {
  let shapeDesc = '';
  contours.forEach(contour => {
    shapeDesc += '{';
    const lastIndex = contour.length - 1;
    let _x, _y;
    contour.forEach((command, index) => {
      roundAllValue(command, 3);
      if (command.type === 'Z') {
        if(contour[0].x !== _x || contour[0].y !== _y) {
          shapeDesc += '# ';
        }
      } else {
        if (command.type === 'C') {
          shapeDesc += `(${command.x1}, ${command.y1}; ${command.x2}, ${command.y2}); `;
        } else if (command.type === 'Q') {
          shapeDesc += `(${command.x1}, ${command.y1}); `;
        }
        shapeDesc += `${command.x}, ${command.y}`;
        _x = command.x;
        _y = command.y;
        if (index !== lastIndex) {
          shapeDesc += '; ';
        }
      }
    });
    shapeDesc += '}';
  });
  return shapeDesc;
}
exports.stringifyContours = stringifyContours;

let pointTolerance = 0.5, areaTolerance = 2;
function setTolerance(pointValue, areaValue) {
  pointTolerance = pointValue;
  areaTolerance = areaValue;
}
exports.setTolerance = setTolerance;

function same(p1, p2) {
  return Math.abs(p1[0] - p2[0]) < pointTolerance && Math.abs(p1[1] - p2[1]) < pointTolerance;
}

class boundBox {
  constructor(left = 0, top = 0, right = 0, bottom = 0) {
    this.left = left;
    this.right = right;
    this.top = top;
    this.bottom = bottom;
    this.updateSize();
  }
 
  updateSize() {
    this.width = this.right - this.left;
    this.height = this.top - this.bottom;
  }

  update(x, y) {
    this.left = Math.min(this.left, x);
    this.right = Math.max(this.right, x);
    this.top = Math.max(this.top, y);
    this.bottom = Math.min(this.bottom, y);
    this.updateSize();
  }

  area() {
    this.updateSize();
    return this.width * this.height;
  }
}
module.exports.boundBox = boundBox;

function degenerate(p) {
  for (let i = 0; i < p.length - 1; i++) 
    if (same(p[i], p[i + 1])) p.splice(i, 1);
  return p.length;
} 

function isDegenerate(contour) {
  if (contour.length < 3) return true; // early quit with 0-area contour
  let bBox = new boundBox(contour[0].x, contour[0].y,contour[0].x, contour[0].y);
  for (let i = 0; i < contour.length - 1; i++) {
    let command_prev = contour[i];
    bBox.update(command_prev.x, command_prev.y);
    let command = contour[i + 1];
    let p = [[command_prev.x, command_prev.y]];
    if (command.type === 'C') { p.push([command.x1, command,y1]); p.push([command.x2, command.y2]); }
    else if (command.type === 'Q') p.push([command.x1, command.y1]);
    if (command.type === 'Z') p.push([contour[0].x, contour[0].y]);
    else p.push([command.x, command.y]); 
    
    let _nump = p.length;
    let nump = degenerate(p);
    if (nump < 2) {
      if (i === contour.length - 2 || command.type === 'Z') contour.splice(i, 1);
      else contour.splice(i + 1, 1);
      continue;
    } 
    // if point not degenerated, continue
    if (nump === _nump) continue;
    let newCommand = {};
    if (nump === 3) { 
      newCommand.type = 'Q';
      newCommand.x1 = p[1][0];
      newCommand.y1 = p[1][1];
      newCommand.x = p[2][0];
      newCommand.y = p[2][1];
    } else {
      newCommand.type = 'L';
      newCommand.x = p[1][0];
      newCommand.y = p[1][1];
    }
    contour[i + 1] = newCommand;
  }
  if (bBox.area() < areaTolerance || contour.length < 3) {
    return true;
  } else {
    return false;
  }
}

function filterContours(contours) {
  let filtered = 0;
  for (let i = 0; i < contours.length; i++) {
    if(isDegenerate(contours[i])) {
      contours.splice(i, 1);
      filtered ++;
    }
  }
  return filtered;
}
exports.filterContours = filterContours;

/**
 * Returns an ArrayBuffer representing the exact content of a Buffer.
 *
 * NodeJS's `fs` package may reuse a single ArrayBuffer as the underlying
 * storage for multiple readFile and readFileSync calls, so directly accessing
 * `buffer.buffer` is likely to give unexpected results unless sliced first.
 *
 * @param  {Buffer} buffer
 * @return {ArrayBuffer}
 */
function bufferToArrayBuffer(buffer) {
  const {byteOffset, byteLength} = buffer;
  return buffer.buffer.slice(byteOffset, byteOffset + byteLength);
}
exports.bufferToArrayBuffer = bufferToArrayBuffer;
