import { bcadd, bcdiv, bcmul } from "./bcmath";
import {
  textsubmodes,
  textlatch,
  clusters,
  rsfactors,
  TextLatchKeys
} from "./pdf417LookupTables";

/**
 * PDF417 - 2D Barcode generator (LGPLv3)
 *
 * Ported from PHP - PDF417 class, version 1.0.005, from TCPDF library (http://www.tcpdf.org/)
 */

export interface BarcodeArray {
  num_rows: number;
  num_cols: number;
  bcode: number[][];
}

const ROWHEIGHT = 4;
const QUIETH = 2;
const QUIETV = 2;
const start_pattern = "11111111010101000";
const stop_pattern = "111111101000101001";

function getInputSequences(code: string): [number, string][] {
  var sequence_array: [number, string][] = []; // array to be returned
  var numseq: [string, number][] = [];
  // get numeric sequences
  var numseqMatch = code.match(/([0-9]{13,44})/g);
  if (numseqMatch == null) {
    numseq = [];
  } else {
    // add offset to each matched line
    for (var n = 0, offset = 0; n < numseqMatch.length; n++) {
      offset = code.indexOf(numseqMatch[n], offset);
      numseq.push([numseqMatch[n], offset]);
      offset += numseqMatch[n].length;
    }
  }
  numseq.push(["", code.length]);
  var offset = 0;
  for (var i = 0; i < numseq.length; i++) {
    var seq = numseq[i];
    var seqlen = seq[0].length;
    if (seq[1] > 0) {
      // extract text sequence before the number sequence
      var prevseq = code.substr(offset, seq[1] - offset);
      var textseq: [string, number][] = [];
      // get text sequences
      var textseqMatch = prevseq.match(/([\x09\x0a\x0d\x20-\x7e]{5,})/g);
      if (textseqMatch == null) {
        textseq = [];
      } else {
        // add offset to each matched line
        for (var n = 0; n < textseqMatch.length; n++) {
          var txtOffset = prevseq.indexOf(textseqMatch[n]);
          textseq.push([textseqMatch[n], txtOffset]);
        }
      }
      textseq.push(["", prevseq.length]);
      var txtoffset = 0;
      for (var j = 0; j < textseq.length; j++) {
        var txtseq = textseq[j];
        var txtseqlen = txtseq[0].length;
        if (txtseq[1] > 0) {
          // extract byte sequence before the text sequence
          var prevtxtseq = prevseq.substr(txtoffset, txtseq[1] - txtoffset);
          if (prevtxtseq.length > 0) {
            // add BYTE sequence
            if (
              prevtxtseq.length == 1 &&
              sequence_array.length > 0 &&
              sequence_array[sequence_array.length - 1][0] == 900
            ) {
              sequence_array.push([913, prevtxtseq]);
            } else if (prevtxtseq.length % 6 == 0) {
              sequence_array.push([924, prevtxtseq]);
            } else {
              sequence_array.push([901, prevtxtseq]);
            }
          }
        }
        if (txtseqlen > 0) {
          // add numeric sequence
          sequence_array.push([900, txtseq[0]]);
        }
        txtoffset = txtseq[1] + txtseqlen;
      }
    }
    if (seqlen > 0) {
      // add numeric sequence
      sequence_array.push([902, seq[0]]);
    }
    offset = seq[1] + seqlen;
  }
  return sequence_array;
};

function getCompaction(mode: number, code: string, addmode?: boolean): number[] {
  addmode = addmode ?? true;
  var cw: number[] = []; // array of codewords to return
  switch (mode) {
    case 900: {
      // Text Compaction mode latch
      var submode = 0; // default Alpha sub-mode
      var txtarr: number[] = []; // array of characters and sub-mode switching characters
      var codelen = code.length;
      for (var i = 0; i < codelen; ++i) {
        var chval = _ord(code.charAt(i));
        var k: number | string | false;
        if (
          (k = _array_search(chval, textsubmodes[submode])) !==
          false
        ) {
          // we are on the same sub-mode
          txtarr.push(k as number);
        } else {
          // the sub-mode is changed
          for (var s = 0; s < 4; ++s) {
            // search new sub-mode
            if (
              s != submode &&
              (k = _array_search(chval, textsubmodes[s])) !==
              false
            ) {
              // s is the new submode
              if (
                (i + 1 == codelen ||
                  (i + 1 < codelen &&
                    _array_search(
                      _ord(code.charAt(i + 1)),
                      textsubmodes[submode]
                    ) !== false)) &&
                (s == 3 || (s == 0 && submode == 1))
              ) {
                // shift (temporary change only for this char)
                if (s == 3) {
                  // shift to puntuaction
                  txtarr.push(29);
                } else {
                  // shift from lower to alpha
                  txtarr.push(27);
                }
              } else {
                // latch
                txtarr = txtarr.concat(textlatch[("" + submode + s) as TextLatchKeys]);
                // set new submode
                submode = s;
              }
              // add characted code to array
              txtarr.push(k as number);
              break;
            }
          }
        }
      }
      var txtarrlen = txtarr.length;
      if (txtarrlen % 2 != 0) {
        // add padding
        txtarr.push(29);
        ++txtarrlen;
      }
      // calculate codewords
      for (var i = 0; i < txtarrlen; i += 2) {
        cw.push(30 * parseInt(String(txtarr[i])) + parseInt(String(txtarr[i + 1])));
      }
      break;
    }
    case 901:
    case 924: {
      // Byte Compaction mode latch
      var rest: string;
      var sublen: number;
      var codelen: number;
      while ((codelen = code.length) > 0) {
        if (codelen > 6) {
          rest = code.substring(6);
          code = code.substring(0, 6);
          sublen = 6;
        } else {
          rest = "";
          sublen = code.length;
        }
        if (sublen == 6) {
          var t = bcmul("" + _ord(code.charAt(0)), "1099511627776");
          t = bcadd(t, bcmul("" + _ord(code.charAt(1)), "4294967296"));
          t = bcadd(t, bcmul("" + _ord(code.charAt(2)), "16777216"));
          t = bcadd(t, bcmul("" + _ord(code.charAt(3)), "65536"));
          t = bcadd(t, bcmul("" + _ord(code.charAt(4)), "256"));
          t = bcadd(t, "" + _ord(code.charAt(5)));
          // tmp array for the 6 bytes block
          var cw6: number[] = [];
          do {
            var d = _my_bcmod(t, "900");
            t = bcdiv(t, "900");
            // prepend the value to the beginning of the array
            cw6.unshift(d);
          } while (t != "0");
          // append the result array at the end
          cw = cw.concat(cw6);
        } else {
          for (var i = 0; i < sublen; ++i) {
            cw.push(_ord(code.charAt(i)));
          }
        }
        code = rest;
      }
      break;
    }
    case 902: {
      // Numeric Compaction mode latch
      var rest: string;
      var codelen: number;
      while ((codelen = code.length) > 0) {
        if (codelen > 44) {
          rest = code.substring(44);
          code = code.substring(0, 44);
        } else {
          rest = "";
        }
        var t = "1" + code;
        do {
          var d = _my_bcmod(t, "900");
          t = bcdiv(t, "900");
          cw.unshift(d);
        } while (t != "0");
        code = rest;
      }
      break;
    }
    case 913: {
      // Byte Compaction mode shift
      cw.push(_ord(code));
      break;
    }
  }
  if (addmode) {
    // add the compaction mode codeword at the beginning
    cw.unshift(mode);
  }
  return cw;
};

function getErrorCorrectionLevel(ecl: number, numcw: number): number {
  // get maximum correction level
  var maxecl = 8; // starting error level
  var maxerrsize = 928 - numcw; // available codewords for error
  while (maxecl > 0) {
    var errsize = 2 << ecl;
    if (maxerrsize >= errsize) {
      break;
    }
    --maxecl;
  }
  // check for automatic levels
  if (ecl < 0 || ecl > 8) {
    if (numcw < 41) {
      ecl = 2;
    } else if (numcw < 161) {
      ecl = 3;
    } else if (numcw < 321) {
      ecl = 4;
    } else if (numcw < 864) {
      ecl = 5;
    } else {
      ecl = maxecl;
    }
  }
  if (ecl > maxecl) {
    ecl = maxecl;
  }
  return ecl;
};

function getErrorCorrection(cw: number[], ecl: number): number[] {
  // get error correction coefficients
  var ecc = rsfactors[ecl];
  // number of error correction factors
  var eclsize = 2 << ecl;
  // maximum index for rsfactors[ecl]
  var eclmaxid = eclsize - 1;
  // initialize array of error correction codewords
  var ecw = _array_fill(0, eclsize, 0) as number[];
  // for each data codeword
  for (var k = 0; k < cw.length; k++) {
    var t1 = (cw[k] + ecw[eclmaxid]) % 929;
    for (var j = eclmaxid; j > 0; --j) {
      var t2 = t1 * ecc[j] % 929;
      var t3 = 929 - t2;
      ecw[j] = (ecw[j - 1] + t3) % 929;
    }
    t2 = t1 * ecc[0] % 929;
    t3 = 929 - t2;
    ecw[0] = t3 % 929;
  }
  for (var j = 0; j < ecw.length; j++) {
    if (ecw[j] != 0) {
      ecw[j] = 929 - ecw[j];
    }
  }
  ecw = ecw.reverse();
  return ecw;
};

/**
 *
 * Functions from phpjs.org
 *
 */
function _array_fill(start_index: number, num: number, mixed_val: number): number[] | Record<number, number> {
  var key: number,
    tmp_arr: Record<number, number> = {};

  if (start_index == 0) {
    var tmpArray: number[] = [];
    for (var i = 0; i < num; i++) {
      tmpArray.push(mixed_val);
    }
    return tmpArray;
  }

  if (!isNaN(start_index) && !isNaN(num)) {
    for (key = 0; key < num; key++) {
      tmp_arr[key + start_index] = mixed_val;
    }
  }

  return tmp_arr;
};

function _str_repeat(input: string, multiplier: number): string {
  // http://kevin.vanzonneveld.net
  // +   original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  // +   improved by: Jonas Raoni Soares Silva (http://www.jsfromhell.com)
  // +   improved by: Ian Carter (http://euona.com/)
  // *     example 1: str_repeat('-=', 10);
  // *     returns 1: '-=-=-=-=-=-=-=-=-=-='
  var y = "";
  while (true) {
    if (multiplier & 1) {
      y += input;
    }
    multiplier >>= 1;
    if (multiplier) {
      input += input;
    } else {
      break;
    }
  }
  return y;
};

function _intval(mixed_var: any, base?: number): number {
  // http://kevin.vanzonneveld.net
  // +   original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  // +   improved by: stensi
  // +   bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  // +   input by: Matteo
  // +   bugfixed by: Brett Zamir (http://brett-zamir.me)
  // +   bugfixed by: Rafał Kukawski (http://kukawski.pl)
  // *     example 1: intval('Kevin van Zonneveld');
  // *     returns 1: 0
  // *     example 2: intval(4.2);
  // *     returns 2: 4
  // *     example 3: intval(42, 8);
  // *     returns 3: 42
  // *     example 4: intval('09');
  // *     returns 4: 9
  // *     example 5: intval('1e', 16);
  // *     returns 5: 30
  var tmp: number;

  var type = typeof mixed_var;

  if (type === "boolean") {
    return +mixed_var;
  } else if (type === "string") {
    tmp = parseInt(mixed_var, base || 10);
    return isNaN(tmp) || !isFinite(tmp) ? 0 : tmp;
  } else if (type === "number" && isFinite(mixed_var)) {
    return mixed_var | 0;
  } else {
    return 0;
  }
};

function _sprintf(format: string, ...args: any[]): string {
  // http://kevin.vanzonneveld.net
  // +   original by: Ash Searle (http://hexmen.com/blog/)
  // + namespaced by: Michael White (http://getsprink.com)
  // +    tweaked by: Jack
  // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  // +      input by: Paulo Freitas
  // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  // +      input by: Brett Zamir (http://brett-zamir.me)
  // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  // +   improved by: Dj
  // +   improved by: Allidylls
  // *     example 1: sprintf("%01.2f", 123.1);
  // *     returns 1: 123.10
  // *     example 2: sprintf("[%10s]", 'monkey');
  // *     returns 2: '[    monkey]'
  // *     example 3: sprintf("[%'#10s]", 'monkey');
  // *     returns 3: '[####monkey]'
  // *     example 4: sprintf("%d", 123456789012345);
  // *     returns 4: '123456789012345'
  var regex = /%%|%(\d+\$)?([-+\'#0 ]*)(\*\d+\$|\*|\d+)?(\.(\*\d+\$|\*|\d+))?([scboxXuideEfFgG])/g;
  var a = [format, ...args],
    i = 0;
  format = a[i++];

  // pad()
  var pad = function (str: string, len: number, chr?: string, leftJustify?: boolean): string {
    if (!chr) {
      chr = " ";
    }
    var padding = str.length >= len
      ? ""
      : Array((1 + len - str.length) >>> 0).join(chr);
    return leftJustify ? str + padding : padding + str;
  };

  // justify()
  var justify = function (
    value: string,
    prefix: string,
    leftJustify: boolean,
    minWidth: number,
    zeroPad: boolean,
    customPadChar?: string
  ): string {
    var diff = minWidth - value.length;
    if (diff > 0) {
      if (leftJustify || !zeroPad) {
        value = pad(value, minWidth, customPadChar, leftJustify);
      } else {
        value =
          value.slice(0, prefix.length) +
          pad("", diff, "0", true) +
          value.slice(prefix.length);
      }
    }
    return value;
  };

  // formatBaseX()
  var formatBaseX = function (
    value: number,
    base: number,
    prefix: string | boolean,
    leftJustify: boolean,
    minWidth: number,
    precision: number,
    zeroPad: boolean
  ): string {
    // Note: casts negative numbers to positive ones
    var number = value >>> 0;
    var prefixStr =
      (prefix &&
        number &&
        ({
          "2": "0b",
          "8": "0",
          "16": "0x"
        } as Record<string, string>)[String(base)]) ||
      "";
    value = number;
    var valueStr = prefixStr + pad(number.toString(base), precision || 0, "0", false);
    return justify(valueStr, prefixStr, leftJustify, minWidth, zeroPad);
  };

  // formatString()
  var formatString = function (
    value: string,
    leftJustify: boolean,
    minWidth: number,
    precision: number | null,
    zeroPad: boolean,
    customPadChar?: string
  ): string {
    if (precision != null) {
      value = value.slice(0, precision);
    }
    return justify(
      value,
      "",
      leftJustify,
      minWidth,
      zeroPad,
      customPadChar
    );
  };

  // doFormat()
  var doFormat = function (
    substring: string,
    valueIndex: string | undefined,
    flags: string,
    minWidth: string,
    _: any,
    precision: string,
    type: string
  ): string {
    var number: number;
    var prefix: string;
    var method: string;
    var textTransform: string;
    var value: any;

    if (substring == "%%") {
      return "%";
    }

    // parse flags
    var leftJustify = false,
      positivePrefix = "",
      zeroPad = false,
      prefixBaseX: string | boolean = false,
      customPadChar = " ";
    var flagsl = flags.length;
    for (var j = 0; flags && j < flagsl; j++) {
      switch (flags.charAt(j)) {
        case " ":
          positivePrefix = " ";
          break;
        case "+":
          positivePrefix = "+";
          break;
        case "-":
          leftJustify = true;
          break;
        case "'":
          customPadChar = flags.charAt(j + 1);
          break;
        case "0":
          zeroPad = true;
          break;
        case "#":
          prefixBaseX = true;
          break;
      }
    }

    // parameters may be null, undefined, empty-string or real valued
    // we want to ignore null, undefined and empty-string values
    var minWidthNum = 0;
    if (!minWidth) {
      minWidthNum = 0;
    } else if (minWidth == "*") {
      minWidthNum = +a[i++];
    } else if (minWidth.charAt(0) == "*") {
      minWidthNum = +a[parseInt(minWidth.slice(1, -1))];
    } else {
      minWidthNum = +minWidth;
    }

    // Note: undocumented perl feature:
    if (minWidthNum < 0) {
      minWidthNum = -minWidthNum;
      leftJustify = true;
    }

    if (!isFinite(minWidthNum)) {
      throw new Error("sprintf: (minimum-)width must be finite");
    }

    var precisionNum: number | undefined;
    if (!precision) {
      precisionNum = "fFeE".indexOf(type) > -1
        ? 6
        : type == "d" ? 0 : undefined;
    } else if (precision == "*") {
      precisionNum = +a[i++];
    } else if (precision.charAt(0) == "*") {
      precisionNum = +a[parseInt(precision.slice(1, -1))];
    } else {
      precisionNum = +precision;
    }

    // grab value using valueIndex if required?
    value = valueIndex ? a[parseInt(valueIndex.slice(0, -1))] : a[i++];

    switch (type) {
      case "s":
        return formatString(
          String(value),
          leftJustify,
          minWidthNum,
          precisionNum === undefined ? null : precisionNum,
          zeroPad,
          customPadChar
        );
      case "c":
        return formatString(
          String.fromCharCode(+value),
          leftJustify,
          minWidthNum,
          precisionNum === undefined ? null : precisionNum,
          zeroPad
        );
      case "b":
        return formatBaseX(
          value,
          2,
          prefixBaseX,
          leftJustify,
          minWidthNum,
          precisionNum || 0,
          zeroPad
        );
      case "o":
        return formatBaseX(
          value,
          8,
          prefixBaseX,
          leftJustify,
          minWidthNum,
          precisionNum || 0,
          zeroPad
        );
      case "x":
        return formatBaseX(
          value,
          16,
          prefixBaseX,
          leftJustify,
          minWidthNum,
          precisionNum || 0,
          zeroPad
        );
      case "X":
        return formatBaseX(
          value,
          16,
          prefixBaseX,
          leftJustify,
          minWidthNum,
          precisionNum || 0,
          zeroPad
        ).toUpperCase();
      case "u":
        return formatBaseX(
          value,
          10,
          prefixBaseX,
          leftJustify,
          minWidthNum,
          precisionNum || 0,
          zeroPad
        );
      case "i":
      case "d":
        number = +value || 0;
        number = Math.round(number - number % 1); // Plain Math.round doesn't just truncate
        prefix = number < 0 ? "-" : positivePrefix;
        value =
          prefix + pad(String(Math.abs(number)), precisionNum || 0, "0", false);
        return justify(value, prefix, leftJustify, minWidthNum, zeroPad);
      case "e":
      case "E":
      case "f": // Should handle locales (as per setlocale)
      case "F":
      case "g":
      case "G":
        number = +value;
        prefix = number < 0 ? "-" : positivePrefix;
        method = ["toExponential", "toFixed", "toPrecision"][
          "efg".indexOf(type.toLowerCase())
        ];
        textTransform = ["toString", "toUpperCase"][
          "eEfFgG".indexOf(type) % 2
        ];
        value = prefix + Math.abs(number)[method as 'toExponential' | 'toFixed' | 'toPrecision'](precisionNum);
        return justify(value, prefix, leftJustify, minWidthNum, zeroPad)[
          textTransform as 'toString' | 'toUpperCase'
        ]();
      default:
        return substring;
    }
  };

  return format.replace(regex, doFormat as any);
};

// function _preg_split(pattern: RegExp | string, subject: string, limit?: number, flags?: string | number): (string | [string, number])[] {
//   // http://kevin.vanzonneveld.net
//   // + original by: Marco Marchiò
//   // * example 1: preg_split(/[\s,]+/, 'hypertext language, programming');
//   // * returns 1: ['hypertext', 'language', 'programming']
//   // * example 2: preg_split('//', 'string', -1, 'PREG_SPLIT_NO_EMPTY');
//   // * returns 2: ['s', 't', 'r', 'i', 'n', 'g']
//   // * example 3: var str = 'hypertext language programming';
//   // * example 3: preg_split('/ /', str, -1, 'PREG_SPLIT_OFFSET_CAPTURE');
//   // * returns 3: [['hypertext', 0], ['language', 10], ['programming', 19]]
//   // * example 4: preg_split('/( )/', '1 2 3 4 5 6 7 8', 4, 'PREG_SPLIT_DELIM_CAPTURE');
//   // * returns 4: ['1', ' ', '2', ' ', '3', ' ', '4 5 6 7 8']
//   // * example 5: preg_split('/( )/', '1 2 3 4 5 6 7 8', 4, (2 | 4));
//   // * returns 5: [['1', 0], [' ', 1], ['2', 2], [' ', 3], ['3', 4], [' ', 5], ['4 5 6 7 8', 6]]

//   limit = limit || 0;
//   flags = flags || ""; // Limit and flags are optional

//   var result: RegExpExecArray | null,
//     ret: (string | [string, number])[] = [],
//     index = 0,
//     i = 0,
//     noEmpty = false,
//     delim = false,
//     offset = false,
//     OPTS: Record<string, number> = {},
//     optTemp = 0;

//   var patternRegex: RegExp;
//   if (typeof pattern === 'string') {
//     var regexpBody = /^\/(.*)\/\w*$/.exec(pattern)?.[1] || pattern;
//     var regexpFlags = /^\/.*\/(\w*)$/.exec(pattern)?.[1] || '';
//     patternRegex = new RegExp(regexpBody, regexpFlags + (regexpFlags.indexOf("g") !== -1 ? "" : "g"));
//   } else {
//     var regexpBody = pattern.source;
//     var regexpFlags = pattern.flags || '';
//     // Non-global regexp causes an infinite loop when executing the while,
//     // so if it's not global, copy the regexp and add the "g" modifier.
//     patternRegex = pattern.global
//       ? pattern
//       : new RegExp(
//         regexpBody,
//         regexpFlags + (regexpFlags.indexOf("g") !== -1 ? "" : "g")
//       );
//   }

//   OPTS = {
//     PREG_SPLIT_NO_EMPTY: 1,
//     PREG_SPLIT_DELIM_CAPTURE: 2,
//     PREG_SPLIT_OFFSET_CAPTURE: 4
//   };
//   if (typeof flags !== "number") {
//     // Allow for a single string or an array of string flags
//     var flagsArr = [].concat(flags as any);
//     for (i = 0; i < flagsArr.length; i++) {
//       // Resolve string input to bitwise e.g. 'PREG_SPLIT_OFFSET_CAPTURE' becomes 4
//       if (OPTS[flagsArr[i]]) {
//         optTemp = optTemp | OPTS[flagsArr[i]];
//       }
//     }
//     flags = optTemp;
//   }
//   noEmpty = !!((flags as number) & OPTS.PREG_SPLIT_NO_EMPTY);
//   delim = !!((flags as number) & OPTS.PREG_SPLIT_DELIM_CAPTURE);
//   offset = !!((flags as number) & OPTS.PREG_SPLIT_OFFSET_CAPTURE);

//   var _filter = function (str: string, strindex: number): void {
//     // If the match is empty and the PREG_SPLIT_NO_EMPTY flag is set don't add it
//     if (noEmpty && !str.length) {
//       return;
//     }
//     // If the PREG_SPLIT_OFFSET_CAPTURE flag is set
//     // transform the match into an array and add the index at position 1
//     if (offset) {
//       ret.push([str, strindex]);
//     } else {
//       ret.push(str);
//     }
//   };
//   // Special case for empty regexp
//   if (!regexpBody) {
//     var resultSplit = subject.split("");
//     for (i = 0; i < resultSplit.length; i++) {
//       _filter(resultSplit[i], i);
//     }
//     return ret;
//   }
//   // Exec the pattern and get the result
//   while ((result = patternRegex.exec(subject))) {
//     // Stop if the limit is 1
//     if (limit === 1) {
//       break;
//     }
//     // Take the correct portion of the string and filter the match
//     _filter(subject.slice(index, result.index), index);
//     index = result.index + result[0].length;
//     // If the PREG_SPLIT_DELIM_CAPTURE flag is set, every capture match must be included in the results array
//     if (delim) {
//       // Convert the regexp result into a normal array
//       var resarr = Array.prototype.slice.call(result);
//       for (i = 1; i < resarr.length; i++) {
//         if (result[i] !== undefined) {
//           _filter(result[i], result.index + result[0].indexOf(result[i]));
//         }
//       }
//     }
//     limit--;
//   }
//   // Filter last match
//   _filter(subject.slice(index, subject.length), index);
//   return ret;
// };

function _ord(string: string): number {
  return string.charCodeAt(0);
};

function _array_search(needle: any, haystack: any, argStrict?: boolean): string | number | false {
  // http://kevin.vanzonneveld.net
  // +   original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  // +      input by: Brett Zamir (http://brett-zamir.me)
  // +   bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  // *     example 1: array_search('zonneveld', {firstname: 'kevin', middle: 'van', surname: 'zonneveld'});
  // *     returns 1: 'surname'
  // *     example 2: ini_set('phpjs.return_phpjs_arrays', 'on');
  // *     example 2: var ordered_arr = array({3:'value'}, {2:'value'}, {'a':'value'}, {'b':'value'});
  // *     example 2: var key = array_search(/val/g, ordered_arr); // or var key = ordered_arr.search(/val/g);
  // *     returns 2: '3'

  var strict = !!argStrict,
    key: string | number = "";

  if (
    haystack &&
    typeof haystack === "object" &&
    (haystack as any).change_key_case
  ) {
    // Duck-type check for our own array()-created PHPJS_Array
    return (haystack as any).search(needle, argStrict);
  }
  if (typeof needle === "object" && (needle as RegExp).exec) {
    // Duck-type for RegExp
    if (!strict) {
      // Let's consider case sensitive searches as strict
      var needleRegex = needle as RegExp;
      var flags =
        "i" +
        (needleRegex.global ? "g" : "") +
        (needleRegex.multiline ? "m" : "") +
        ((needleRegex as any).sticky ? "y" : ""); // sticky is FF only
      needle = new RegExp(needleRegex.source, flags);
    }
    for (key in haystack) {
      if ((needle as RegExp).test(haystack[key])) {
        return key;
      }
    }
    return false;
  }

  for (key in haystack) {
    if (
      (strict && haystack[key] === needle) ||
      (!strict && haystack[key] == needle)
    ) {
      return key;
    }
  }

  return false;
};

function _my_bcmod(x: string, y: string): number {
  // how many numbers to take at once? carefull not to exceed (int)
  var take = 5;
  var mod = "";
  do {
    var a = parseInt(mod + "" + x.substring(0, take));
    x = x.substring(take);
    mod = String(a % parseInt(y));
  } while (x.length);

  return parseInt(mod);
}

/**
 * Creates a PDF417 object
 * @param code (string) code to represent using PDF417
 * @param ecl (int) error correction level (0-8); default -1 = automatic correction level
 * @param aspectratio (float) the width to height of the symbol (excluding quiet zones)
 */
export function generateBarcode(code: string, ecl?: number, aspectratio?: number) {

  code = unescape(encodeURIComponent(code)); // covert UTF-8 to ISO-8859-1
  ecl = ecl || -1;
  aspectratio = aspectratio || 2;
  if (code === "") {
    throw new Error("PDF417 init: empty input code");
  }
  // get the input sequence array
  let sequence = getInputSequences(code);
  let codewords: number[] = []; // array of code-words
  for (var i = 0; i < sequence.length; i++) {
    var cw = getCompaction(sequence[i][0], sequence[i][1], true);
    codewords = codewords.concat(cw);
  }
  if (codewords[0] == 900) {
    // Text Alpha is the default mode, so remove the first code
    codewords.shift();
  }
  // count number of codewords
  var numcw = codewords.length;
  if (numcw > 925) {
    // reached maximum data codeword capacity
    throw new Error("PDF417 init: maximum data codeword capacity exceeded");
  }

  // set error correction level
  ecl = getErrorCorrectionLevel(ecl, numcw);
  // number of codewords for error correction
  var errsize = 2 << ecl;
  // calculate number of columns (number of codewords per row) and rows
  var nce = numcw + errsize + 1;
  var cols = Math.round(
    (Math.sqrt(4761 + 68 * aspectratio * ROWHEIGHT * nce) - 69) / 34
  );
  // adjust cols
  if (cols < 1) {
    cols = 1;
  } else if (cols > 30) {
    cols = 30;
  }
  var rows = Math.ceil(nce / cols);
  var size = cols * rows;
  // adjust rows
  if (rows < 3 || rows > 90) {
    if (rows < 3) {
      rows = 3;
    } else if (rows > 90) {
      rows = 90;
    }
    cols = Math.ceil(size / rows);
    size = cols * rows;
  }
  if (size > 928) {
    // set dimensions to get maximum capacity
    if (
      Math.abs(aspectratio - 17 * 29 / 32) <
      Math.abs(aspectratio - 17 * 16 / 58)
    ) {
      cols = 29;
      rows = 32;
    } else {
      cols = 16;
      rows = 58;
    }
    size = 928;
  }
  // calculate padding
  var pad = size - nce;
  if (pad > 0) {
    if (size - rows == nce) {
      --rows;
      size -= rows;
    } else {
      // add pading
      codewords = codewords.concat(_array_fill(0, pad, 900) as number[]);
    }
  }

  // Symbol Length Descriptor (number of data codewords including Symbol Length Descriptor and pad codewords)
  var sld = size - errsize;
  // add symbol length description
  codewords.unshift(sld);
  // calculate error correction
  var ecw = getErrorCorrection(codewords, ecl);
  // add error correction codewords
  codewords = codewords.concat(ecw);
  // add horizontal quiet zones to start and stop patterns
  var pstart = _str_repeat("0", QUIETH) + start_pattern;
  var pstop = stop_pattern + "" + _str_repeat("0", QUIETH);

  const barcode_array: BarcodeArray = {
    num_rows: rows * ROWHEIGHT + 2 * QUIETV,
    num_cols: (cols + 2) * 17 + 35 + 2 * QUIETH,
    bcode: []
  };

  var empty_row: number[] = [];
  // build rows for vertical quiet zone
  if (QUIETV > 0) {
    empty_row = _array_fill(0, barcode_array.num_cols, 0) as number[];
    for (var i = 0; i < QUIETV; ++i) {
      // add vertical quiet rows
      barcode_array.bcode.push(empty_row);
    }
  }

  var L: number = 0;
  var k = 0; // codeword index
  var cid = 0; // initial cluster
  // for each row
  for (var r = 0; r < rows; ++r) {
    // row start code
    var row = pstart;
    switch (cid) {
      case 0: {
        L = 30 * _intval(r / 3) + _intval((rows - 1) / 3);
        break;
      }
      case 1: {
        L = 30 * _intval(r / 3) + ecl * 3 + (rows - 1) % 3;
        break;
      }
      case 2: {
        L = 30 * _intval(r / 3) + (cols - 1);
        break;
      }
    }
    // left row indicator
    row += _sprintf("%17b", clusters[cid][L]);
    // for each column
    for (var c = 0; c < cols; ++c) {
      row += _sprintf("%17b", clusters[cid][codewords[k]]);
      ++k;
    }
    switch (cid) {
      case 0: {
        L = 30 * _intval(r / 3) + (cols - 1);
        break;
      }
      case 1: {
        L = 30 * _intval(r / 3) + _intval((rows - 1) / 3);
        break;
      }
      case 2: {
        L = 30 * _intval(r / 3) + ecl * 3 + (rows - 1) % 3;
        break;
      }
    }
    // right row indicator
    row += _sprintf("%17b", clusters[cid][L]);
    // row stop code
    row += pstop;
    // convert the string to array
    const arow = row.split('');
    // duplicate row to get the desired height
    for (var h = 0; h < ROWHEIGHT; ++h) {
      barcode_array.bcode.push(arow.map(x => parseInt(x)));
    }
    ++cid;
    if (cid > 2) {
      cid = 0;
    }
  }
  if (QUIETV > 0) {
    for (var i = 0; i < QUIETV; ++i) {
      // add vertical quiet rows
      barcode_array.bcode.push(empty_row);
    }
  }

  return (barcode_array);
};


