const acorn = require('acorn');
const { Input } = require('./input');
const { Texture } = require('./texture');

const FUNCTION_NAME = /function ([^(]*)/;
const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
const ARGUMENT_NAMES = /([^\s,]+)/g;

/**
 *
 * @desc Various utility functions / snippets of code that GPU.JS uses internally.
 * @type {utils}
 * This covers various snippets of code that is not entirely gpu.js specific (ie. may find uses elsewhere)
 */
const utils = {
  /**
   *
   * @desc Gets the system endianness, and cache it
   * @returns {String} 'LE' or 'BE' depending on system architecture
   * Credit: https://gist.github.com/TooTallNate/4750953
   */
  systemEndianness() {
    return _systemEndianness;
  },
  getSystemEndianness() {
    const b = new ArrayBuffer(4);
    const a = new Uint32Array(b);
    const c = new Uint8Array(b);
    a[0] = 0xdeadbeef;
    if (c[0] === 0xef) return 'LE';
    if (c[0] === 0xde) return 'BE';
    throw new Error('unknown endianness');
  },

  /**
   * @descReturn TRUE, on a JS function
   * @param {Function} funcObj - Object to validate if its a function
   * @returns  {Boolean} TRUE if the object is a JS function
   */
  isFunction(funcObj) {
    return typeof(funcObj) === 'function';
  },

  /**
   * @desc Return TRUE, on a valid JS function string
   * Note: This does just a VERY simply sanity check. And may give false positives.
   *
   * @param {String} fn - String of JS function to validate
   * @returns {Boolean} TRUE if the string passes basic validation
   */
  isFunctionString(fn) {
    if (typeof fn === 'string') {
      return (fn
        .slice(0, 'function'.length)
        .toLowerCase() === 'function');
    }
    return false;
  },

  /**
   * @desc Return the function name from a JS function string
   * @param {String} funcStr - String of JS function to validate
   * @returns {String} Function name string (if found)
   */
  getFunctionNameFromString(funcStr) {
    const result = FUNCTION_NAME.exec(funcStr);
    if (!result || result.length === 0) return null;
    return result[1].trim();
  },

  getFunctionBodyFromString(funcStr) {
    return funcStr.substring(funcStr.indexOf('{') + 1, funcStr.lastIndexOf('}'));
  },

  /**
   * @desc Return list of argument names extracted from a javascript function
   * @param {String} fn - String of JS function to validate
   * @returns {String[]}  Array representing all the parameter names
   */
  getArgumentNamesFromString(fn) {
    const fnStr = fn.replace(STRIP_COMMENTS, '');
    let result = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')).match(ARGUMENT_NAMES);
    if (result === null) {
      result = [];
    }
    return result;
  },

  /**
   * @desc Returns a clone
   * @param {Object} obj - Object to clone
   * @returns {Object|Array} Cloned object
   */
  clone(obj) {
    if (obj === null || typeof obj !== 'object' || obj.hasOwnProperty('isActiveClone')) return obj;

    const temp = obj.constructor(); // changed

    for (let key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        obj.isActiveClone = null;
        temp[key] = utils.clone(obj[key]);
        delete obj.isActiveClone;
      }
    }

    return temp;
  },

  /**
   * @desc Checks if is an array or Array-like object
   * @param {Object} array - The argument object to check if is array
   * @returns {Boolean}  true if is array or Array-like object
   */
  isArray(array) {
    return !isNaN(array.length);
  },

  /**
   * @desc Evaluate the argument type, to apply respective logic for it
   * @param {*} value - The argument object to evaluate type
   * @param {boolean} [strictIntegers]
   * @returns {String}  Argument type Array/Number/Float/Texture/Unknown
   */
  getVariableType(value, strictIntegers) {
    if (utils.isArray(value)) {
      if (value.length > 0 && value[0].nodeName === 'IMG') {
        return 'HTMLImageArray';
      }
      return 'Array';
    }

    switch (value.constructor) {
      case Boolean:
        return 'Boolean';
      case Number:
        if (strictIntegers && Number.isInteger(value)) {
          return 'Integer';
        }
        return 'Float';
      case Texture:
        return value.type;
      case Input:
        return 'Input';
    }
    switch (value.nodeName) {
      case 'IMG':
        return 'HTMLImage';
      case 'CANVAS':
        return 'HTMLImage';
      case 'VIDEO':
        return 'HTMLVideo';
    }
    if (value.hasOwnProperty('type')) {
      return value.type;
    }
    return 'Unknown';
  },

  getKernelTextureSize(settings, dimensions) {
    let [w, h, d] = dimensions;
    let texelCount = (w || 1) * (h || 1) * (d || 1);

    if (settings.optimizeFloatMemory && settings.precision === 'single') {
      w = texelCount = Math.ceil(texelCount / 4);
    }
    // if given dimensions == a 2d image
    if (h > 1 && w * h === texelCount) {
      return new Int32Array([w, h]);
    }
    return utils.closestSquareDimensions(texelCount);
  },

  /**
   *
   * @param {Number} length
   * @returns {TextureDimensions}
   */
  closestSquareDimensions(length) {
    const sqrt = Math.sqrt(length);
    let high = Math.ceil(sqrt);
    let low = Math.floor(sqrt);
    while (high * low < length) {
      high--;
      low = Math.ceil(length / high);
    }
    return new Int32Array([low, Math.ceil(length / low)]);
  },

  /**
   * A texture takes up four
   * @param {OutputDimensions} dimensions
   * @param {Number} bitRatio
   * @returns {TextureDimensions}
   */
  getMemoryOptimizedFloatTextureSize(dimensions, bitRatio) {
    const totalArea = utils.roundTo((dimensions[0] || 1) * (dimensions[1] || 1) * (dimensions[2] || 1) * (dimensions[3] || 1), 4);
    const texelCount = totalArea / bitRatio;
    return utils.closestSquareDimensions(texelCount);
  },

  /**
   *
   * @param dimensions
   * @param bitRatio
   * @returns {*|TextureDimensions}
   */
  getMemoryOptimizedPackedTextureSize(dimensions, bitRatio) {
    const [w, h, d] = dimensions;
    const totalArea = utils.roundTo((w || 1) * (h || 1) * (d || 1), 4);
    const texelCount = totalArea / (4 / bitRatio);
    return utils.closestSquareDimensions(texelCount);
  },

  roundTo(n, d) {
    return Math.floor((n + d - 1) / d) * d;
  },
  /**
   * @desc Return the dimension of an array.
   * @param {Array|String|Texture|Input} x - The array
   * @param {Boolean} [pad] - To include padding in the dimension calculation
   * @returns {OutputDimensions}
   */
  getDimensions(x, pad) {
    let ret;
    if (utils.isArray(x)) {
      const dim = [];
      let temp = x;
      while (utils.isArray(temp)) {
        dim.push(temp.length);
        temp = temp[0];
      }
      ret = dim.reverse();
    } else if (x instanceof Texture) {
      ret = x.output;
    } else if (x instanceof Input) {
      ret = x.size;
    } else {
      throw new Error(`Unknown dimensions of ${x}`);
    }

    if (pad) {
      ret = Array.from(ret);
      while (ret.length < 3) {
        ret.push(1);
      }
    }

    return new Int32Array(ret);
  },

  /**
   * Puts a nested 2d array into a one-dimensional target array
   * @param {Array|*} array
   * @param {Float32Array|Float64Array} target
   */
  flatten2dArrayTo(array, target) {
    let offset = 0;
    for (let y = 0; y < array.length; y++) {
      target.set(array[y], offset);
      offset += array[y].length;
    }
  },

  /**
   * Puts a nested 3d array into a one-dimensional target array
   * @param {Array|*} array
   * @param {Float32Array|Float64Array} target
   */
  flatten3dArrayTo(array, target) {
    let offset = 0;
    for (let z = 0; z < array.length; z++) {
      for (let y = 0; y < array[z].length; y++) {
        target.set(array[z][y], offset);
        offset += array[z][y].length;
      }
    }
  },

  /**
   * Puts a nested 4d array into a one-dimensional target array
   * @param {Array|*} array
   * @param {Float32Array|Float64Array} target
   */
  flatten4dArrayTo(array, target) {
    let offset = 0;
    for (let l = 0; l < array.length; l++) {
      for (let z = 0; z < array[l].length; z++) {
        for (let y = 0; y < array[l][z].length; y++) {
          target.set(array[l][z][y], offset);
          offset += array[l][z][y].length;
        }
      }
    }
  },

  /**
   * Puts a nested 1d, 2d, or 3d array into a one-dimensional target array
   * @param {Float32Array|Uint16Array|Uint8Array} array
   * @param {Float32Array} target
   */
  flattenTo(array, target) {
    if (utils.isArray(array[0])) {
      if (utils.isArray(array[0][0])) {
        if (utils.isArray(array[0][0][0])) {
          utils.flatten4dArrayTo(array, target);
        } else {
          utils.flatten3dArrayTo(array, target);
        }
      } else {
        utils.flatten2dArrayTo(array, target);
      }
    } else {
      target.set(array);
    }
  },

  /**
   *
   * @desc Splits an array into smaller arrays.
   * Number of elements in one small chunk is given by `part`
   *
   * @param {Number[]} array - The array to split into chunks
   * @param {Number} part - elements in one chunk
   *
   * @returns {Number[]} An array of smaller chunks
   */
  splitArray(array, part) {
    const result = [];
    for (let i = 0; i < array.length; i += part) {
      result.push(new array.constructor(array.buffer, i * 4 + array.byteOffset, part));
    }
    return result;
  },

  getAstString(source, ast) {
    const lines = Array.isArray(source) ? source : source.split(/\r?\n/g);
    const start = ast.loc.start;
    const end = ast.loc.end;
    const result = [];
    if (start.line === end.line) {
      result.push(lines[start.line - 1].substring(start.column, end.column));
    } else {
      result.push(lines[start.line - 1].slice(start.column));
      for (let i = start.line; i < end.line; i++) {
        result.push(lines[i]);
      }
      result.push(lines[end.line - 1].slice(0, end.column));
    }
    return result.join('\n');
  },

  allPropertiesOf(obj) {
    const props = [];

    do {
      props.push.apply(props, Object.getOwnPropertyNames(obj));
    } while (obj = Object.getPrototypeOf(obj));

    return props;
  },

  /**
   * @param {Array} lines - An Array of strings
   * @returns {String} Single combined String, separated by *\n*
   */
  linesToString(lines) {
    if (lines.length > 0) {
      return lines.join(';\n') + ';\n';
    } else {
      return '\n';
    }
  },
  warnDeprecated(type, oldName, newName) {
    if (newName) {
      console.warn(`You are using a deprecated ${ type } "${ oldName }". It has been replaced with "${ newName }". Fixing, but please upgrade as it will soon be removed.`);
    } else {
      console.warn(`You are using a deprecated ${ type } "${ oldName }". It has been removed. Fixing, but please upgrade as it will soon be removed.`);
    }
  },
  flipPixels: (pixels, width, height) => {
    // https://stackoverflow.com/a/41973289/1324039
    const halfHeight = height / 2 | 0; // the | 0 keeps the result an int
    const bytesPerRow = width * 4;
    // make a temp buffer to hold one row
    const temp = new Uint8ClampedArray(width * 4);
    const result = pixels.slice(0);
    for (let y = 0; y < halfHeight; ++y) {
      const topOffset = y * bytesPerRow;
      const bottomOffset = (height - y - 1) * bytesPerRow;

      // make copy of a row on the top half
      temp.set(result.subarray(topOffset, topOffset + bytesPerRow));

      // copy a row from the bottom half to the top
      result.copyWithin(topOffset, bottomOffset, bottomOffset + bytesPerRow);

      // copy the copy of the top half row to the bottom half
      result.set(temp, bottomOffset);
    }
    return result;
  },
  erectPackedFloat: (array, width) => {
    return array.subarray(0, width);
  },
  erect2DPackedFloat: (array, width, height) => {
    const yResults = new Array(height);
    for (let y = 0; y < height; y++) {
      const xStart = y * width;
      const xEnd = xStart + width;
      yResults[y] = array.subarray(xStart, xEnd);
    }
    return yResults;
  },
  erect3DPackedFloat: (array, width, height, depth) => {
    const zResults = new Array(depth);
    for (let z = 0; z < depth; z++) {
      const yResults = new Array(height);
      for (let y = 0; y < height; y++) {
        const xStart = (z * height * width) + y * width;
        const xEnd = xStart + width;
        yResults[y] = array.subarray(xStart, xEnd);
      }
      zResults[z] = yResults;
    }
    return zResults;
  },
  erectMemoryOptimizedFloat: (array, width) => {
    return array.subarray(0, width);
  },
  erectMemoryOptimized2DFloat: (array, width, height) => {
    const yResults = new Array(height);
    for (let y = 0; y < height; y++) {
      const offset = y * width;
      yResults[y] = array.subarray(offset, offset + width);
    }
    return yResults;
  },
  erectMemoryOptimized3DFloat: (array, width, height, depth) => {
    const zResults = new Array(depth);
    for (let z = 0; z < depth; z++) {
      const yResults = new Array(height);
      for (let y = 0; y < height; y++) {
        const offset = (z * height * width) + (y * width);
        yResults[y] = array.subarray(offset, offset + width);
      }
      zResults[z] = yResults;
    }
    return zResults;
  },
  erectFloat: (array, width) => {
    const xResults = new Float32Array(width);
    let i = 0;
    for (let x = 0; x < width; x++) {
      xResults[x] = array[i];
      i += 4;
    }
    return xResults;
  },
  erect2DFloat: (array, width, height) => {
    const yResults = new Array(height);
    let i = 0;
    for (let y = 0; y < height; y++) {
      const xResults = new Float32Array(width);
      for (let x = 0; x < width; x++) {
        xResults[x] = array[i];
        i += 4;
      }
      yResults[y] = xResults;
    }
    return yResults;
  },
  erect3DFloat: (array, width, height, depth) => {
    const zResults = new Array(depth);
    let i = 0;
    for (let z = 0; z < depth; z++) {
      const yResults = new Array(height);
      for (let y = 0; y < height; y++) {
        const xResults = new Float32Array(width);
        for (let x = 0; x < width; x++) {
          xResults[x] = array[i];
          i += 4;
        }
        yResults[y] = xResults;
      }
      zResults[z] = yResults;
    }
    return zResults;
  },
  erectArray2: (array, width) => {
    const xResults = new Array(width);
    const xResultsMax = width * 4;
    let i = 0;
    for (let x = 0; x < xResultsMax; x += 4) {
      xResults[i++] = array.subarray(x, x + 2);
    }
    return xResults;
  },
  erect2DArray2: (array, width, height) => {
    const yResults = new Array(height);
    const XResultsMax = width * 4;
    for (let y = 0; y < height; y++) {
      const xResults = new Array(width);
      const offset = y * XResultsMax;
      let i = 0;
      for (let x = 0; x < XResultsMax; x += 4) {
        xResults[i++] = array.subarray(x + offset, x + offset + 2);
      }
      yResults[y] = xResults;
    }
    return yResults;
  },
  erect3DArray2: (array, width, height, depth) => {
    const xResultsMax = width * 4;
    const zResults = new Array(depth);
    for (let z = 0; z < depth; z++) {
      const yResults = new Array(height);
      for (let y = 0; y < height; y++) {
        const xResults = new Array(width);
        const offset = (z * xResultsMax * height) + (y * xResultsMax);
        let i = 0;
        for (let x = 0; x < xResultsMax; x += 4) {
          xResults[i++] = array.subarray(x + offset, x + offset + 2);
        }
        yResults[y] = xResults;
      }
      zResults[z] = yResults;
    }
    return zResults;
  },
  erectArray3: (array, width) => {
    const xResults = new Array(width);
    const xResultsMax = width * 4;
    let i = 0;
    for (let x = 0; x < xResultsMax; x += 4) {
      xResults[i++] = array.subarray(x, x + 3);
    }
    return xResults;
  },
  erect2DArray3: (array, width, height) => {
    const xResultsMax = width * 4;
    const yResults = new Array(height);
    for (let y = 0; y < height; y++) {
      const xResults = new Array(width);
      const offset = y * xResultsMax;
      let i = 0;
      for (let x = 0; x < xResultsMax; x += 4) {
        xResults[i++] = array.subarray(x + offset, x + offset + 3);
      }
      yResults[y] = xResults;
    }
    return yResults;
  },
  erect3DArray3: (array, width, height, depth) => {
    const xResultsMax = width * 4;
    const zResults = new Array(depth);
    for (let z = 0; z < depth; z++) {
      const yResults = new Array(height);
      for (let y = 0; y < height; y++) {
        const xResults = new Array(width);
        const offset = (z * xResultsMax * height) + (y * xResultsMax);
        let i = 0;
        for (let x = 0; x < xResultsMax; x += 4) {
          xResults[i++] = array.subarray(x + offset, x + offset + 3);
        }
        yResults[y] = xResults;
      }
      zResults[z] = yResults;
    }
    return zResults;
  },
  erectArray4: (array, width) => {
    const xResults = new Array(array);
    const xResultsMax = width * 4;
    let i = 0;
    for (let x = 0; x < xResultsMax; x += 4) {
      xResults[i++] = array.subarray(x, x + 4);
    }
    return xResults;
  },
  erect2DArray4: (array, width, height) => {
    const xResultsMax = width * 4;
    const yResults = new Array(height);
    for (let y = 0; y < height; y++) {
      const xResults = new Array(width);
      const offset = y * xResultsMax;
      let i = 0;
      for (let x = 0; x < xResultsMax; x += 4) {
        xResults[i++] = array.subarray(x + offset, x + offset + 4);
      }
      yResults[y] = xResults;
    }
    return yResults;
  },
  erect3DArray4: (array, width, height, depth) => {
    const xResultsMax = width * 4;
    const zResults = new Array(depth);
    for (let z = 0; z < depth; z++) {
      const yResults = new Array(height);
      for (let y = 0; y < height; y++) {
        const xResults = new Array(width);
        const offset = (z * xResultsMax * height) + (y * xResultsMax);
        let i = 0;
        for (let x = 0; x < xResultsMax; x += 4) {
          xResults[i++] = array.subarray(x + offset, x + offset + 4);
        }
        yResults[y] = xResults;
      }
      zResults[z] = yResults;
    }
    return zResults;
  },

  /**
   *
   * @param {String} source
   * @param {Object} settings
   * @return {String}
   */
  flattenFunctionToString: (source, settings) => {
    const { findDependency, thisLookup, doNotDefine } = settings;
    let flattened = settings.flattened;
    if (!flattened) {
      flattened = settings.flattened = {};
    }
    const ast = acorn.parse(source);
    const functionDependencies = [];
    let indent = 0;

    function flatten(ast) {
      if (Array.isArray(ast)) {
        const results = [];
        for (let i = 0; i < ast.length; i++) {
          results.push(flatten(ast[i]));
        }
        return results.join('');
      }
      switch (ast.type) {
        case 'Program':
          return flatten(ast.body) + (ast.body[0].type === 'VariableDeclaration' ? ';' : '');
        case 'FunctionDeclaration':
          return `function ${ast.id.name}(${ast.params.map(flatten).join(', ')}) ${ flatten(ast.body) }`;
        case 'BlockStatement': {
          const result = [];
          indent += 2;
          for (let i = 0; i < ast.body.length; i++) {
            const flat = flatten(ast.body[i]);
            if (flat) {
              result.push(' '.repeat(indent) + flat, ';\n');
            }
          }
          indent -= 2;
          return `{\n${result.join('')}}`;
        }
        case 'VariableDeclaration':
          const declarations = utils.normalizeDeclarations(ast)
            .map(flatten)
            .filter(r => r !== null);
          if (declarations.length < 1) {
            return '';
          } else {
            return `${ast.kind} ${declarations.join(',')}`;
          }
          case 'VariableDeclarator':
            if (ast.init.object && ast.init.object.type === 'ThisExpression') {
              const lookup = thisLookup(ast.init.property.name, true);
              if (lookup) {
                return `${ast.id.name} = ${flatten(ast.init)}`;
              } else {
                return null;
              }
            } else {
              return `${ast.id.name} = ${flatten(ast.init)}`;
            }
            case 'CallExpression': {
              if (ast.callee.property.name === 'subarray') {
                return `${flatten(ast.callee.object)}.${flatten(ast.callee.property)}(${ast.arguments.map(value => flatten(value)).join(', ')})`;
              }
              if (ast.callee.object.name === 'gl' || ast.callee.object.name === 'context') {
                return `${flatten(ast.callee.object)}.${flatten(ast.callee.property)}(${ast.arguments.map(value => flatten(value)).join(', ')})`;
              }
              if (ast.callee.object.type === 'ThisExpression') {
                functionDependencies.push(findDependency('this', ast.callee.property.name));
                return `${ast.callee.property.name}(${ast.arguments.map(value => flatten(value)).join(', ')})`;
              } else if (ast.callee.object.name) {
                const foundSource = findDependency(ast.callee.object.name, ast.callee.property.name);
                if (foundSource === null) {
                  // we're not flattening it
                  return `${ast.callee.object.name}.${ast.callee.property.name}(${ast.arguments.map(value => flatten(value)).join(', ')})`;
                } else {
                  functionDependencies.push(foundSource);
                  // we're flattening it
                  return `${ast.callee.property.name}(${ast.arguments.map(value => flatten(value)).join(', ')})`;
                }
              } else if (ast.callee.object.type === 'MemberExpression') {
                return `${flatten(ast.callee.object)}.${ast.callee.property.name}(${ast.arguments.map(value => flatten(value)).join(', ')})`;
              } else {
                throw new Error('unknown ast.callee');
              }
            }
            case 'ReturnStatement':
              return `return ${flatten(ast.argument)}`;
            case 'BinaryExpression':
              return `(${flatten(ast.left)}${ast.operator}${flatten(ast.right)})`;
            case 'UnaryExpression':
              if (ast.prefix) {
                return `${ast.operator} ${flatten(ast.argument)}`;
              } else {
                return `${flatten(ast.argument)} ${ast.operator}`;
              }
              case 'ExpressionStatement':
                return `${flatten(ast.expression)}`;
              case 'SequenceExpression':
                return `(${flatten(ast.expressions)})`;
              case 'ArrowFunctionExpression':
                return `(${ast.params.map(flatten).join(', ')}) => ${flatten(ast.body)}`;
              case 'Literal':
                return ast.raw;
              case 'Identifier':
                return ast.name;
              case 'MemberExpression':
                if (ast.object.type === 'ThisExpression') {
                  return thisLookup(ast.property.name);
                }
                if (ast.computed) {
                  return `${flatten(ast.object)}[${flatten(ast.property)}]`;
                }
                return flatten(ast.object) + '.' + flatten(ast.property);
              case 'ThisExpression':
                return 'this';
              case 'NewExpression':
                return `new ${flatten(ast.callee)}(${ast.arguments.map(value => flatten(value)).join(', ')})`;
              case 'ForStatement':
                return `for (${flatten(ast.init)};${flatten(ast.test)};${flatten(ast.update)}) ${flatten(ast.body)}`;
              case 'AssignmentExpression':
                return `${flatten(ast.left)}${ast.operator}${flatten(ast.right)}`;
              case 'UpdateExpression':
                return `${flatten(ast.argument)}${ast.operator}`;
              case 'IfStatement':
                return `if (${flatten(ast.test)}) ${flatten(ast.consequent)}`;
              case 'ThrowStatement':
                return `throw ${flatten(ast.argument)}`;
              case 'ObjectPattern':
                return ast.properties.map(flatten).join(', ');
              case 'ArrayPattern':
                return ast.elements.map(flatten).join(', ');
              case 'DebuggerStatement':
                return 'debugger;';
              case 'ConditionalExpression':
                return `${flatten(ast.test)}?${flatten(ast.consequent)}:${flatten(ast.alternate)}`;
              case 'Property':
                if (ast.kind === 'init') {
                  return flatten(ast.key);
                }
      }
      throw new Error(`unhandled ast.type of ${ ast.type }`);
    }
    const result = flatten(ast);
    if (functionDependencies.length > 0) {
      const flattenedFunctionDependencies = [];
      for (let i = 0; i < functionDependencies.length; i++) {
        const functionDependency = functionDependencies[i];
        if (!flattened[functionDependency]) {
          flattened[functionDependency] = true;
        }
        flattenedFunctionDependencies.push(utils.flattenFunctionToString(functionDependency, settings) + '\n');
      }
      return flattenedFunctionDependencies.join('') + result;
    }
    return result;
  },

  normalizeDeclarations: (ast) => {
    if (ast.type !== 'VariableDeclaration') throw new Error('Ast is not of type "VariableDeclaration"');
    const normalizedDeclarations = [];
    for (let declarationIndex = 0; declarationIndex < ast.declarations.length; declarationIndex++) {
      const declaration = ast.declarations[declarationIndex];
      if (declaration.id && declaration.id.type === 'ObjectPattern' && declaration.id.properties) {
        const { properties } = declaration.id;
        for (let propertyIndex = 0; propertyIndex < properties.length; propertyIndex++) {
          const property = properties[propertyIndex];
          if (property.value.type === 'ObjectPattern' && property.value.properties) {
            for (let subPropertyIndex = 0; subPropertyIndex < property.value.properties.length; subPropertyIndex++) {
              const subProperty = property.value.properties[subPropertyIndex];
              if (subProperty.type === 'Property') {
                normalizedDeclarations.push({
                  type: 'VariableDeclarator',
                  id: {
                    type: 'Identifier',
                    name: subProperty.key.name
                  },
                  init: {
                    type: 'MemberExpression',
                    object: {
                      type: 'MemberExpression',
                      object: declaration.init,
                      property: {
                        type: 'Identifier',
                        name: property.key.name
                      },
                      computed: false
                    },
                    property: {
                      type: 'Identifier',
                      name: subProperty.key.name
                    },
                    computed: false
                  }
                });
              } else {
                throw new Error('unexpected state');
              }
            }
          } else if (property.value.type === 'Identifier') {
            normalizedDeclarations.push({
              type: 'VariableDeclarator',
              id: {
                type: 'Identifier',
                name: property.value && property.value.name ? property.value.name : property.key.name
              },
              init: {
                type: 'MemberExpression',
                object: declaration.init,
                property: {
                  type: 'Identifier',
                  name: property.key.name
                },
                computed: false
              }
            });
          } else {
            throw new Error('unexpected state');
          }
        }
      } else if (declaration.id && declaration.id.type === 'ArrayPattern' && declaration.id.elements) {
        const { elements } = declaration.id;
        for (let elementIndex = 0; elementIndex < elements.length; elementIndex++) {
          const element = elements[elementIndex];
          if (element.type === 'Identifier') {
            normalizedDeclarations.push({
              type: 'VariableDeclarator',
              id: {
                type: 'Identifier',
                name: element.name
              },
              init: {
                type: 'MemberExpression',
                object: declaration.init,
                property: {
                  type: 'Literal',
                  value: elementIndex,
                  raw: elementIndex.toString(),
                  start: element.start,
                  end: element.end
                },
                computed: true
              }
            });
          } else {
            throw new Error('unexpected state');
          }
        }
      } else {
        normalizedDeclarations.push(declaration);
      }
    }
    return normalizedDeclarations;
  },

  /**
   *
   * @param {GPU} gpu
   * @param image
   * @return {Array}
   */
  splitHTMLImageToRGB: (gpu, image) => {
    const rKernel = gpu.createKernel(function(a) {
      const pixel = a[this.thread.y][this.thread.x];
      return pixel.r * 255;
    }, {
      output: [image.width, image.height],
      precision: 'unsigned',
      argumentTypes: { a: 'HTMLImage' },
    });
    const gKernel = gpu.createKernel(function(a) {
      const pixel = a[this.thread.y][this.thread.x];
      return pixel.g * 255;
    }, {
      output: [image.width, image.height],
      precision: 'unsigned',
      argumentTypes: { a: 'HTMLImage' },
    });
    const bKernel = gpu.createKernel(function(a) {
      const pixel = a[this.thread.y][this.thread.x];
      return pixel.b * 255;
    }, {
      output: [image.width, image.height],
      precision: 'unsigned',
      argumentTypes: { a: 'HTMLImage' },
    });
    const aKernel = gpu.createKernel(function(a) {
      const pixel = a[this.thread.y][this.thread.x];
      return pixel.a * 255;
    }, {
      output: [image.width, image.height],
      precision: 'unsigned',
      argumentTypes: { a: 'HTMLImage' },
    });
    const result = [
      rKernel(image),
      gKernel(image),
      bKernel(image),
      aKernel(image),
    ];
    result.rKernel = rKernel;
    result.gKernel = gKernel;
    result.bKernel = bKernel;
    result.aKernel = aKernel;
    result.gpu = gpu;
    return result;
  },

  /**
   * A visual debug utility
   * @param {GPU} gpu
   * @param rgba
   * @param width
   * @param height
   * @return {Object[]}
   */
  splitRGBAToCanvases: (gpu, rgba, width, height) => {
    const visualKernelR = gpu.createKernel(function(v) {
      const pixel = v[this.thread.y][this.thread.x];
      this.color(pixel.r / 255, 0, 0, 255);
    }, {
      output: [width, height],
      graphical: true,
      argumentTypes: { v: 'Array2D(4)' }
    });
    visualKernelR(rgba);

    const visualKernelG = gpu.createKernel(function(v) {
      const pixel = v[this.thread.y][this.thread.x];
      this.color(0, pixel.g / 255, 0, 255);
    }, {
      output: [width, height],
      graphical: true,
      argumentTypes: { v: 'Array2D(4)' }
    });
    visualKernelG(rgba);

    const visualKernelB = gpu.createKernel(function(v) {
      const pixel = v[this.thread.y][this.thread.x];
      this.color(0, 0, pixel.b / 255, 255);
    }, {
      output: [width, height],
      graphical: true,
      argumentTypes: { v: 'Array2D(4)' }
    });
    visualKernelB(rgba);

    const visualKernelA = gpu.createKernel(function(v) {
      const pixel = v[this.thread.y][this.thread.x];
      this.color(255, 255, 255, pixel.a / 255);
    }, {
      output: [width, height],
      graphical: true,
      argumentTypes: { v: 'Array2D(4)' }
    });
    visualKernelA(rgba);
    return [
      visualKernelR.canvas,
      visualKernelG.canvas,
      visualKernelB.canvas,
      visualKernelA.canvas,
    ];
  },

  getMinifySafeName: (fn) => {
    try {
      const ast = acorn.parse(`const value = ${fn.toString()}`);
      const { init } = ast.body[0].declarations[0];
      return init.body.name || init.body.body[0].argument.name;
    } catch (e) {
      throw new Error('Unrecognized function type.  Please use `() => yourFunctionVariableHere` or function() { return yourFunctionVariableHere; }');
    }
  },
  sanitizeName: function(name) {
    if (dollarSign.test(name)) {
      name = name.replace(dollarSign, 'S_S');
    }
    if (doubleUnderscore.test(name)) {
      name = name.replace(doubleUnderscore, 'U_U');
    } else if (singleUnderscore.test(name)) {
      name = name.replace(singleUnderscore, 'u_u');
    }
    return name;
  }
};

const dollarSign = /\$/;
const doubleUnderscore = /__/;
const singleUnderscore = /_/;

const _systemEndianness = utils.getSystemEndianness();

module.exports = {
  utils
};