text: const utils = require('./lib/utils');
  const reshaper = require('arabic-persian-reshaper').ArabicShaper;
  const opentype = require('opentype.js');
  const exec = require('child_process').exec;
  const mapLimit = require('map-limit');
  const MaxRectsPacker = require('maxrects-packer').MaxRectsPacker;
  const path = require('path');
  const ProgressBar = require('cli-progress');
  const fs = require('fs');
  const buffer = require('buffer').Buffer;
  const Jimp = require('jimp');
  const readline = require('readline');
  const assert = require('assert');
  
  const defaultCharset = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~".split('');
  const controlChars = ['\n', '\r', '\t'];
  
  const binaryLookup = {
    darwin: 'msdfgen.osx',
    win32: 'msdfgen.exe',
    linux: 'msdfgen.linux'
  };
  
  module.exports = generateBMFont;
  
  /**
   * Creates a BMFont compatible bitmap font of signed distance fields from a font file
   *
   * @param {string|Buffer} fontPath - Path or Buffer for the input ttf/otf/woff font
   * @param {Object} opt - Options object for generating bitmap font (Optional) :
   *            outputType : font file format Avaliable: json(default)
   *            filename : filename of both font file and font textures
   *            fontSize : font size for generated textures (default 42)
   *            charset : charset in generated font, could be array or string (default is Western)
   *            textureWidth : Width of generated textures (default 512)
   *            textureHeight : Height of generated textures (default 512)
   *            distanceRange : distance range for computing signed distance field
   *            fieldType : "msdf"(default), "sdf", "psdf"
   *            roundDecimal  : rounded digits of the output font file. (Defaut is null)
   *            smartSize : shrink atlas to the smallest possible square (Default: false)
   *            pot : atlas size shall be power of 2 (Default: false)
   *            square : atlas size shall be square (Default: false)
   *            rot : allow 90-degree rotation while packing (Default: false)
   *            rtl : use RTL charators fix (Default: false)
   * @param {function(string, Array.<Object>, Object)} callback - Callback funtion(err, textures, font)
   *
   */
  function generateBMFont (fontPath, opt, callback) {
    if (typeof opt === 'function') {
      callback = opt;
      opt = {};
    }
  
    const binName = binaryLookup[process.platform];
  
    assert.ok(binName, `No msdfgen binary for platform ${process.platform}.`);
    assert.ok(fontPath, 'must specify a font path');
    assert.ok(typeof fontPath === 'string' || fontPath instanceof Buffer, 'font must be string path or Buffer');
    assert.ok(opt.filename || !(fontPath instanceof Buffer), 'must specify filename if font is a Buffer');
    assert.ok(callback, 'missing callback')
    assert.ok(typeof callback === 'function', 'expected callback to be a function');
    assert.ok(!opt.textureSize || opt.textureSize.length === 2, 'textureSize format shall be: width,height');
  
    // Set fallback output path to font path
    let fontDir = typeof fontPath === 'string' ? path.dirname(fontPath) : '';
    const binaryPath = path.join(__dirname, 'bin', process.platform, binName);
  
    // const reuse = (typeof opt.reuse === 'boolean' || typeof opt.reuse === 'undefined') ? {} : opt.reuse.opt;
    let reuse, cfg = {};
    if (typeof opt.reuse !== 'undefined' && typeof opt.reuse !== 'boolean') {
      if (!fs.existsSync(opt.reuse)) {
        console.log('Creating cfg file : ' + opt.reuse);
        reuse = {};
      } else {
        console.log('Loading cfg file : ' + opt.reuse);
        cfg = JSON.parse(fs.readFileSync(opt.reuse, 'utf8'));
        reuse = cfg.opt;
      }
    } else reuse = {};
    const outputType = opt.outputType = utils.valueQueue([opt.outputType, reuse.outputType, "json"]);
    let filename = utils.valueQueue([opt.filename, reuse.filename]);
    const distanceRange = opt.distanceRange = utils.valueQueue([opt.distanceRange, reuse.distanceRange, 4]);
    const fontSize = opt.fontSize = utils.valueQueue([opt.fontSize, reuse.fontSize, 42]);
    const fontSpacing = opt.fontSpacing = utils.valueQueue([opt.fontSpacing, reuse.fontSpacing, [0, 0]]);
    const pad = distanceRange >> 1;
    const fontPadding = opt.fontPadding = utils.valueQueue([opt.fontPadding, reuse.fontPadding, [pad, pad, pad, pad]]);
    const textureWidth = opt.textureWidth = utils.valueQueue([opt.textureSize || reuse.textureSize, [512, 512]])[0];
    const textureHeight = opt.textureHeight = utils.valueQueue([opt.textureSize || reuse.textureSize, [512, 512]])[1];
    const texturePadding = opt.texturePadding = utils.valueQueue([opt.texturePadding, reuse.texturePadding, 1]);
    const border = opt.border = utils.valueQueue([opt.border, reuse.border, 0]);
    const fieldType = opt.fieldType = utils.valueQueue([opt.fieldType, reuse.fieldType, 'msdf']);
    const roundDecimal = opt.roundDecimal = utils.valueQueue([opt.roundDecimal, reuse.roundDecimal]); // if no roudDecimal option, left null as-is
    const smartSize = opt.smartSize = utils.valueQueue([opt.smartSize, reuse.smartSize, false]);
    const pot = opt.pot = utils.valueQueue([opt.pot, reuse.pot, false]);
    const square = opt.square = utils.valueQueue([opt.square, reuse.square, false]);
    const debug = opt.vector || false;
    const tolerance = opt.tolerance = utils.valueQueue([opt.tolerance, reuse.tolerance, 0]);
    const isRTL = opt.rtl = utils.valueQueue([opt.rtl, reuse.rtl, false]);
    const allowRotation = opt.rot = utils.valueQueue([opt.rot, reuse.rot, false]);
    opt.charset = typeof opt.charset === 'string' && (/^\[.+\]$/igm).test(opt.charset) ? JSON.parse(opt.charset) : opt.charset;
    if (isRTL) opt.charset = reshaper.convertArabic(opt.charset);
    opt.charset = (typeof opt.charset === 'string' ? Array.from(opt.charset) : opt.charset) || reuse.charset || defaultCharset;
    opt.charset = opt.charset.map(c => typeof c ==='string' ? c : String.fromCharCode(c));
    let charset = opt.charset;
    // TODO: Validate options
    if (fieldType !== 'msdf' && fieldType !== 'sdf' && fieldType !== 'psdf') {
      throw new TypeError('fieldType must be one of msdf, sdf, or psdf');
    }
  
    const font = typeof fontPath === 'string'
      ? opentype.loadSync(fontPath)
      : opentype.parse(utils.bufferToArrayBuffer(fontPath));
  
    if (font.outlinesFormat !== 'truetype' && font.outlinesFormat !== 'cff') {
      throw new TypeError('must specify a truetype font (ttf, otf, woff)');
    }
    const packer = new MaxRectsPacker(textureWidth, textureHeight, texturePadding, {
      smart: smartSize,
      pot: pot,
      square: square,
      allowRotation: allowRotation,
      tag: false,
      border: border
    });
    const charsMap = {};
  
    charset = charset.filter((e, i, self) => {
      return (i == self.indexOf(e)) && (!controlChars.includes(e));
    }); // Remove duplicate & control chars
  
    const os2 = font.tables.os2;
    const baseline = os2.sTypoAscender * (fontSize / font.unitsPerEm) + (distanceRange >> 1);
  
    const fontface = typeof fontPath === 'string' ? path.basename(fontPath, path.extname(fontPath)) : filename;
  
    if(!filename) {
      filename = fontface;
      console.log(`Use font-face as filename : ${filename}`);
    } else {
      if (opt.filename) fontDir = path.dirname(opt.filename);
      filename = opt.filename = path.basename(filename, path.extname(filename));
    }
  
    // Initialize settings
    let settings = {};
    settings.opt = JSON.parse(JSON.stringify(opt));
    delete settings.opt['reuse']; // prune previous settings
    let pages = [];
    if (cfg.packer !== undefined) {
      pages = cfg.pages;
      packer.load(cfg.packer.bins);
    }
  
    let bar;
    bar = new ProgressBar.Bar({
      format: "Generating {percentage}%|{bar}| ({value}/{total}) {duration}s",
      clearOnComplete: true
    }, ProgressBar.Presets.shades_classic); 
    bar.start(charset.length, 0);
  
    mapLimit(charset, 15, (char, cb) => {
      generateImage({
        binaryPath,
        font,
        char,
        fontSize,
        fieldType,
        distanceRange,
        roundDecimal,
        debug,
        tolerance
      }, (err, res) => {
        if (err) return cb(err);
        bar.increment();
        cb(null, res);
      });
    }, async (err, results) => {
      if (err) callback(err);
      bar.stop();
  
      packer.addArray(results);
      const textures = packer.bins.map(async (bin, index) => {
        let svg = "";
        let texname = "";
        let fillColor = fieldType === "msdf" ? 0x000000ff : 0x00000000;
        let img = new Jimp(bin.width, bin.height, fillColor);
        if (index > pages.length - 1) { 
          if (packer.bins.length > 1) texname = `${filename}.${index}`;
          else texname = filename; 
          pages.push(`${texname}.png`);
        } else {
          texname = path.basename(pages[index], path.extname(pages[index]));
          let imgPath = path.join(fontDir, `${texname}.png`);
          // let imgPath = `${texname}.png`;
          console.log('Loading previous image : ', imgPath);
          const loader = Jimp.read(imgPath);
          loader.catch(err => {
            console.warn("File read error: ", err);
          });
          const prevImg = await loader;
          img.composite(prevImg, 0, 0);
        }
        bin.rects.forEach(rect => {
          if (rect.data.imageData) {
            if (rect.rot) {
              rect.data.imageData.rotate(90);
            }
            img.composite(rect.data.imageData, rect.x, rect.y);
            if (debug) {
              const x_woffset = rect.x - rect.data.fontData.xoffset + (distanceRange >> 1);
              const y_woffset = rect.y - rect.data.fontData.yoffset + baseline + (distanceRange >> 1);
              svg += font.charToGlyph(rect.data.fontData.char).getPath(x_woffset, y_woffset, fontSize).toSVG() + "\n";
            }
          }
          const charData = rect.data.fontData;
          charData.x = rect.x;
          charData.y = rect.y;
          charData.page = index;
          charsMap[rect.data.fontData.char] = {
            id: charData.id,
            w: charData.width,
            h: charData.height,
            x:charData.x,
            y:charData.y
          };
  
          if(index > 0){
            charsMap[rect.data.fontData.char].p = index;
          }
          if(charData.xadvance != 0){
            charsMap[rect.data.fontData.char].xA = charData.xadvance;
          }
          if(charData.xoffset != 0){
            charsMap[rect.data.fontData.char]._x = charData.xoffset;
          }
          if(charData.yoffset != 0){
            charsMap[rect.data.fontData.char]._y = charData.yoffset;
          }
          // chars.push(rect.data.fontData);
        });
        const buffer = await img.getBufferAsync(Jimp.MIME_PNG);
        let tex = {
          filename: path.join(fontDir, texname),
          texture: buffer 
        }
        if (debug) tex.svg = svg;
        return tex;
      });
  
      const asyncTextures = await Promise.all(textures);
  
      const kerningsMap = {};
      charset.forEach(first => {
        charset.forEach(second => {
          const amount = font.getKerningValue(font.charToGlyph(first), font.charToGlyph(second));
          const kerning = amount * (fontSize / font.unitsPerEm);
          if (kerning !== 0 ) {
            kerningsMap[`${String(first).charCodeAt(0)}-${String(second).charCodeAt(0)}`] = kerning;
          }
        });
      });
  
      const fontData = {
        pages,
        charsMap,
        info: {
          face: fontface,
          size: fontSize,
          bold: 0,
          italic: 0,
          charset,
          unicode: 1,
          stretchH: 100,
          smooth: 1,
          aa: 1,
          padding: fontPadding,
          spacing: fontSpacing
        },
        common: {
          lineHeight: (os2.sTypoAscender - os2.sTypoDescender + os2.sTypoLineGap) * (fontSize / font.unitsPerEm),
          base: baseline,
          scaleW: packer.bins[0].width,
          scaleH: packer.bins[0].height,
          pages: packer.bins.length,
          packed: 0,
          alphaChnl: 0,
          redChnl: 0,
          greenChnl: 0,
          blueChnl: 0
        },
        distanceField: {
          fieldType: fieldType,
          distanceRange: distanceRange
        },
        kerningsMap
      };
      if(roundDecimal !== null) utils.roundAllValue(fontData, roundDecimal, true);
      let fontFile = {};
      const ext = outputType === "json" ? `.json` : `.fnt`;
      fontFile.filename = path.join(fontDir, fontface + ext);
      fontFile.data = utils.stringify(fontData, outputType);
  
      // Store pages name and available packer freeRects in settings
      settings.pages = pages;
      settings.packer = {};
      settings.packer.bins = packer.save(); 
      fontFile.settings = settings;
  
      console.log("\nGeneration complete!\n");
      callback(null, asyncTextures, fontFile);
    });
  }
  
  function generateImage (opt, callback) {
    const {binaryPath, font, char, fontSize, fieldType, distanceRange, roundDecimal, debug, tolerance} = opt;
    const glyph = font.charToGlyph(char);
    const commands = glyph.getPath(0, 0, fontSize).commands;
    let contours = [];
    let currentContour = [];
    const bBox = glyph.getPath(0, 0, fontSize).getBoundingBox();
    commands.forEach(command => {
      if (command.type === 'M') { // new contour
        if (currentContour.length > 0) {
          contours.push(currentContour);
          currentContour = [];
        }
      }
      currentContour.push(command);
    });
    contours.push(currentContour);
  
    if (tolerance != 0) {
      utils.setTolerance(tolerance, tolerance * 10);
      let numFiltered = utils.filterContours(contours);
      if (numFiltered && debug)
        console.log(`\n${char} removed ${numFiltered} small contour(s)`);
      // let numReversed = utils.alignClockwise(contours, false);
      // if (numReversed && debug)
      //   console.log(`${char} found ${numReversed}/${contours.length} reversed contour(s)`);
    }
    let shapeDesc = utils.stringifyContours(contours);
  
    if (contours.some(cont => cont.length === 1)) console.log('length is 1, failed to normalize glyph');
    const scale = fontSize / font.unitsPerEm;
    const baseline = font.tables.os2.sTypoAscender * (fontSize / font.unitsPerEm);
    const pad = distanceRange >> 1;
    let width = Math.round(bBox.x2 - bBox.x1) + pad + pad;
    let height = Math.round(bBox.y2 - bBox.y1) + pad + pad;
    let xOffset = Math.round(-bBox.x1) + pad;
    let yOffset = Math.round(-bBox.y1) + pad;
    if (roundDecimal != null) {
      xOffset = utils.roundNumber(xOffset, roundDecimal);
      yOffset = utils.roundNumber(yOffset, roundDecimal);
    }
    let command = `"${binaryPath}" ${fieldType} -format text -stdout -size ${width} ${height} -translate ${xOffset} ${yOffset} -pxrange ${distanceRange} -stdin`;
  
    let subproc = exec(command, (err, stdout, stderr) => {
      if (err) return callback(err);
      const rawImageData = stdout.match(/([0-9a-fA-F]+)/g).map(str => parseInt(str, 16)); // split on every number, parse from hex
      const pixels = [];
      const channelCount = rawImageData.length / width / height;
  
      if (!isNaN(channelCount) && channelCount % 1 !== 0) {
        console.error(command);
        console.error(stdout);
        return callback(new RangeError('msdfgen returned an image with an invalid length'));
      }
      if (fieldType === 'msdf') {
        for (let i = 0; i < rawImageData.length; i += channelCount) {
          pixels.push(...rawImageData.slice(i, i + channelCount), 255); // add 255 as alpha every 3 elements
        }
      } else {
        for (let i = 0; i < rawImageData.length; i += channelCount) {
          pixels.push(rawImageData[i], rawImageData[i], rawImageData[i], rawImageData[i]); // make monochrome w/ alpha
        }
      }
      let imageData;
      if (isNaN(channelCount) || !rawImageData.some(x => x !== 0)) { // if character is blank
        readline.clearLine(process.stdout);
        readline.cursorTo(process.stdout, 0);
        console.log(`Warning: no bitmap for character '${char}' (${char.charCodeAt(0)}), adding to font as empty`);
        width = 0;
        height = 0;
      } else {
        const buffer = new Uint8ClampedArray(pixels);
        imageData = new Jimp({data: buffer, width: width, height: height});
      }
      const container = {
        data: {
          imageData,
          fontData: {
            id: char.charCodeAt(0),
            index: glyph.index,
            char: String(char),
            width: width,
            height: height,
            xoffset: Math.round(bBox.x1) - pad,
            yoffset: Math.round(bBox.y1) + pad + baseline,
            xadvance: glyph.advanceWidth * scale,
            chnl: 15
          }
        },
        width: width,
        height: height
      };
      callback(null, container);
    });
    
    subproc.stdin.write(shapeDesc);
    subproc.stdin.write('\n');
    subproc.stdin.destroy();
  }
  
  