const sinon = require('sinon');
const { assert, test, module: describe, only, skip } = require('qunit');
const { GPU, FunctionBuilder } = require('../../src');

describe('internal: deep types');

function oneLayerDeepFloat(mode) {
  const gpu = new GPU({ mode });
  function childFunction(childFunctionArgument1) {
    return childFunctionArgument1 + 1;
  }
  gpu.addFunction(childFunction);

  const kernel = gpu.createKernel(function(kernelArgument1) {
    return childFunction(kernelArgument1);
  }, { output: [1] });
  sinon.spy(FunctionBuilder.prototype, 'lookupReturnType');
  try {
    const result = kernel(1.5);
    assert.equal(result[0], 2.5);
    assert.equal(FunctionBuilder.prototype.lookupReturnType.callCount, 1);
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[0][0], 'childFunction');
  } finally {
    FunctionBuilder.prototype.lookupReturnType.restore();
  }
}

(GPU.isWebGLSupported ? test : skip)('one layer deep float WebGL', () => {
  oneLayerDeepFloat('webgl');
});
(GPU.isWebGL2Supported ? test : skip)('one layer deep float WebGL2', () => {
  oneLayerDeepFloat('webgl2');
});
(GPU.isHeadlessGLSupported ? test : skip)('one layer deep float HeadlessGL', () => {
  oneLayerDeepFloat('headlessgl');
});

function twoLayerDeepFloat(mode) {
  const gpu = new GPU({ mode });
  function child1Function(child1FunctionArgument1) {
    return child2Function(child1FunctionArgument1);
  }
  function child2Function(child2FunctionArgument1) {
    return child2FunctionArgument1 + 1;
  }
  gpu
    .addFunction(child1Function)
    .addFunction(child2Function);
  const kernel = gpu.createKernel(function(kernelArgument1) {
    return child1Function(kernelArgument1);
  }, { output: [1] });
  sinon.spy(FunctionBuilder.prototype, 'lookupReturnType');
  try {
    const result = kernel(1.5);
    assert.equal(result[0], 2.5);
    assert.equal(FunctionBuilder.prototype.lookupReturnType.callCount, 3);
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[0][0], 'child1Function');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[1][0], 'child2Function');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[2][0], 'child2Function');
  } finally {
    FunctionBuilder.prototype.lookupReturnType.restore();
  }
}

(GPU.isWebGLSupported ? test : skip)('two layer deep float WebGL', () => {
  twoLayerDeepFloat('webgl');
});
(GPU.isWebGL2Supported ? test : skip)('two layer deep float WebGL2', () => {
  twoLayerDeepFloat('webgl2');
});
(GPU.isHeadlessGLSupported ? test : skip)('two layer deep float HeadlessGL', () => {
  twoLayerDeepFloat('headlessgl');
});

function twoArgumentLayerDeepFloat(mode) {
  const gpu = new GPU({ mode });
  function child1Function(child1FunctionArgument1) {
    return child1FunctionArgument1 + 1;
  }
  function child2Function(child2FunctionArgument1) {
    return child2FunctionArgument1 + 1;
  }
  gpu
    .addFunction(child1Function)
    .addFunction(child2Function);
  const kernel = gpu.createKernel(function(kernelArgument1) {
    return child1Function(child2Function(kernelArgument1));
  }, { output: [1] });
  sinon.spy(FunctionBuilder.prototype, 'lookupReturnType');
  try {
    const result = kernel(1.5);
    assert.equal(kernel.returnType, 'Float');
    assert.equal(result[0], 3.5);
    assert.equal(FunctionBuilder.prototype.lookupReturnType.callCount, 3);
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[0][0], 'child2Function');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[1][0], 'child1Function');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[2][0], 'child2Function');
  } finally {
    FunctionBuilder.prototype.lookupReturnType.restore();
  }
}

(GPU.isWebGLSupported ? test : skip)('two argument layer deep float WebGL', () => {
  twoArgumentLayerDeepFloat('webgl');
});
(GPU.isWebGL2Supported ? test : skip)('two argument layer deep float WebGL2', () => {
  twoArgumentLayerDeepFloat('webgl2');
});
(GPU.isHeadlessGLSupported ? test : skip)('two argument layer deep float HeadlessGL', () => {
  twoArgumentLayerDeepFloat('headlessgl');
});


function threeLayerDeepFloat(mode) {
  const gpu = new GPU({ mode });
  function child1Function(child1FunctionArgument1) {
    return child2Function(child1FunctionArgument1);
  }
  function child2Function(child2FunctionArgument1) {
    return child3Function(child2FunctionArgument1 + 1);
  }
  function child3Function(child3FunctionArgument1) {
    return child3FunctionArgument1 + 1;
  }
  gpu
    .addFunction(child1Function)
    .addFunction(child2Function)
    .addFunction(child3Function);
  const kernel = gpu.createKernel(function(kernelArgument1) {
    return child1Function(kernelArgument1);
  }, { output: [1] });
  sinon.spy(FunctionBuilder.prototype, 'lookupReturnType');
  try {
    const result = kernel(1.5);
    assert.equal(result[0], 3.5);
    assert.equal(FunctionBuilder.prototype.lookupReturnType.callCount, 5);
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[0][0], 'child1Function');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[1][0], 'child2Function');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[2][0], 'child3Function');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[3][0], 'child2Function');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[4][0], 'child3Function');
  } finally {
    FunctionBuilder.prototype.lookupReturnType.restore();
  }
}

(GPU.isWebGLSupported ? test : skip)('three layer deep float WebGL', () => {
  threeLayerDeepFloat('webgl');
});
(GPU.isWebGL2Supported ? test : skip)('three layer deep float WebGL2', () => {
  threeLayerDeepFloat('webgl2');
});
(GPU.isHeadlessGLSupported ? test : skip)('three layer deep float HeadlessGL', () => {
  threeLayerDeepFloat('headlessgl');
});

function threeArgumentLayerDeepFloat(mode) {
  const gpu = new GPU({ mode });
  function child1Function(child1FunctionArgument1) {
    return child1FunctionArgument1 + 1;
  }
  function child2Function(child2FunctionArgument1) {
    return child2FunctionArgument1 + 1;
  }
  function child3Function(child3FunctionArgument1) {
    return child3FunctionArgument1 + 1;
  }
  gpu
    .addFunction(child1Function)
    .addFunction(child2Function)
    .addFunction(child3Function);
  const kernel = gpu.createKernel(function(kernelArgument1) {
    return child1Function(child2Function(child3Function(kernelArgument1)));
  }, { output: [1] });
  sinon.spy(FunctionBuilder.prototype, 'lookupReturnType');
  try {
    const result = kernel(1.5);
    assert.equal(result[0], 4.5);
    assert.equal(FunctionBuilder.prototype.lookupReturnType.callCount, 5);
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[0][0], 'child3Function');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[1][0], 'child2Function');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[2][0], 'child1Function');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[3][0], 'child2Function');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[4][0], 'child3Function');
  } finally {
    FunctionBuilder.prototype.lookupReturnType.restore();
  }
}

(GPU.isWebGLSupported ? test : skip)('three argument layer deep float WebGL', () => {
  threeArgumentLayerDeepFloat('webgl');
});
(GPU.isWebGL2Supported ? test : skip)('three argument layer deep float WebGL2', () => {
  threeArgumentLayerDeepFloat('webgl2');
});
(GPU.isHeadlessGLSupported ? test : skip)('three argument layer deep float HeadlessGL', () => {
  threeArgumentLayerDeepFloat('headlessgl');
});

function threeArgumentLayerDeepNumberTexture1(mode) {
  const gpu = new GPU({ mode });
  const texture = gpu.createKernel(function() {
    return 1.5;
  }, { output: [1], pipeline: true, precision: 'single' })();
  function child1Function(child1FunctionArgument1) {
    return child1FunctionArgument1 + 1;
  }
  function child2Function(child2FunctionArgument1) {
    return child2FunctionArgument1 + 1;
  }
  function child3Function(child3FunctionArgument1) {
    return child3FunctionArgument1[this.thread.x] + 1;
  }
  gpu
    .addFunction(child1Function)
    .addFunction(child2Function)
    .addFunction(child3Function);
  const kernel = gpu.createKernel(function(kernelArgument1) {
    return child1Function(child2Function(child3Function(kernelArgument1)));
  }, { output: [1] });
  sinon.spy(FunctionBuilder.prototype, 'lookupReturnType');
  try {
    const result = kernel(texture);
    assert.equal(result[0], 4.5);
    assert.equal(FunctionBuilder.prototype.lookupReturnType.callCount, 5);
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[0][0], 'child3Function');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[1][0], 'child2Function');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[2][0], 'child1Function');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[3][0], 'child2Function');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[4][0], 'child3Function');
  } finally {
    FunctionBuilder.prototype.lookupReturnType.restore();
  }
}

(GPU.isSinglePrecisionSupported && GPU.isWebGLSupported ? test : skip)('three argument layer deep NumberTexture(1) WebGL', () => {
  threeArgumentLayerDeepNumberTexture1('webgl');
});
(GPU.isSinglePrecisionSupported && GPU.isWebGL2Supported ? test : skip)('three argument layer deep NumberTexture(1) WebGL2', () => {
  threeArgumentLayerDeepNumberTexture1('webgl2');
});
(GPU.isSinglePrecisionSupported && GPU.isHeadlessGLSupported ? test : skip)('three argument layer deep NumberTexture(1) HeadlessGL', () => {
  threeArgumentLayerDeepNumberTexture1('headlessgl');
});

function circlicalLogic(mode) {
  const gpu = new GPU({ mode });
  function child1Function(child1FunctionArgument1) {
    return child1Function(child1FunctionArgument1);
  }
  gpu
    .addFunction(child1Function);
  const kernel = gpu.createKernel(function(kernelArgument1) {
    return child1Function(kernelArgument1);
  }, { output: [1] });
  assert.throws(() => {
    kernel(1.5);
  });
}

(GPU.isWebGLSupported ? test : skip)('circlical logic webgl', () => {
  circlicalLogic('webgl');
});
(GPU.isWebGL2Supported ? test : skip)('circlical logic webgl2', () => {
  circlicalLogic('webgl2');
});
(GPU.isHeadlessGLSupported ? test : skip)('circlical logic headlessgl', () => {
  circlicalLogic('headlessgl');
});

function arrayTexture1(mode) {
  const gpu = new GPU({ mode });
  function addOne(functionValue) {
    return functionValue[this.thread.x] + 1;
  }
  gpu.addFunction(addOne);
  const texture1 = gpu.createKernel(function() {
    return 1;
  }, {
    output: [1],
    precision: 'single',
    pipeline: true,
  })();
  if (mode !== 'cpu') {
    assert.equal(texture1.type, 'ArrayTexture(1)');
  }

  const kernel = gpu.createKernel(function(kernelValue) {
    return addOne(kernelValue);
  }, { output: [1] });
  const result = kernel(texture1);
  assert.equal(result[0], 2);
  gpu.destroy();
}

(GPU.isSinglePrecisionSupported ? test : skip)('ArrayTexture(1) auto', ()=> {
  arrayTexture1();
});
(GPU.isSinglePrecisionSupported ? test : skip)('ArrayTexture(1) gpu', ()=> {
  arrayTexture1('gpu');
});
(GPU.isSinglePrecisionSupported && GPU.isWebGLSupported ? test : skip)('ArrayTexture(1) webgl', ()=> {
  arrayTexture1('webgl');
});
(GPU.isSinglePrecisionSupported && GPU.isWebGL2Supported ? test : skip)('ArrayTexture(1) webgl2', ()=> {
  arrayTexture1('webgl2');
});
(GPU.isSinglePrecisionSupported && GPU.isHeadlessGLSupported ? test : skip)('ArrayTexture(1) headlessgl', ()=> {
  arrayTexture1('headlessgl');
});
(GPU.isSinglePrecisionSupported ? test : skip)('ArrayTexture(1) cpu', ()=> {
  arrayTexture1('cpu');
});

function arrayTexture2(mode) {
  const gpu = new GPU({ mode });
  function addOne(functionValue) {
    const declaredValue = functionValue[this.thread.x];
    return declaredValue[0] + 1 + declaredValue[1] + 1;
  }
  gpu.addFunction(addOne);
  const texture1 = gpu.createKernel(function() {
    return [1,2];
  }, {
    output: [1],
    precision: 'single',
    pipeline: true,
  })();
  if (mode !== 'cpu') {
    assert.equal(texture1.type, 'ArrayTexture(2)');
  }

  const kernel = gpu.createKernel(function(kernelValue) {
    return addOne(kernelValue);
  }, { output: [1] });
  const result = kernel(texture1);
  assert.equal(result[0], 5);
  gpu.destroy();
}

(GPU.isSinglePrecisionSupported ? test : skip)('ArrayTexture(2) auto', ()=> {
  arrayTexture2();
});
(GPU.isSinglePrecisionSupported ? test : skip)('ArrayTexture(2) gpu', ()=> {
  arrayTexture2('gpu');
});
(GPU.isSinglePrecisionSupported && GPU.isWebGLSupported ? test : skip)('ArrayTexture(2) webgl', ()=> {
  arrayTexture2('webgl');
});
(GPU.isSinglePrecisionSupported && GPU.isWebGL2Supported ? test : skip)('ArrayTexture(2) webgl2', ()=> {
  arrayTexture2('webgl2');
});
(GPU.isSinglePrecisionSupported && GPU.isHeadlessGLSupported ? test : skip)('ArrayTexture(2) headlessgl', ()=> {
  arrayTexture2('headlessgl');
});
(GPU.isSinglePrecisionSupported ? test : skip)('ArrayTexture(2) cpu', ()=> {
  arrayTexture2('cpu');
});

function arrayTexture3(mode) {
  const gpu = new GPU({ mode });
  function addOne(functionValue) {
    const declaredValue = functionValue[this.thread.x];
    return declaredValue[0] + 1
      + declaredValue[1] + 1
      + declaredValue[2] + 1;
  }
  gpu.addFunction(addOne);
  const texture1 = gpu.createKernel(function() {
    return [1,2,3];
  }, {
    output: [1],
    precision: 'single',
    pipeline: true,
  })();
  if (mode !== 'cpu') {
    assert.equal(texture1.type, 'ArrayTexture(3)');
  }

  const kernel = gpu.createKernel(function(kernelValue) {
    return addOne(kernelValue);
  }, { output: [1] });
  const result = kernel(texture1);
  assert.equal(result[0], 9);
  gpu.destroy();
}

(GPU.isSinglePrecisionSupported ? test : skip)('ArrayTexture(3) auto', ()=> {
  arrayTexture3();
});
(GPU.isSinglePrecisionSupported ? test : skip)('ArrayTexture(3) gpu', ()=> {
  arrayTexture3('gpu');
});
(GPU.isSinglePrecisionSupported && GPU.isWebGLSupported ? test : skip)('ArrayTexture(3) webgl', ()=> {
  arrayTexture3('webgl');
});
(GPU.isSinglePrecisionSupported && GPU.isWebGL2Supported ? test : skip)('ArrayTexture(3) webgl2', ()=> {
  arrayTexture3('webgl2');
});
(GPU.isSinglePrecisionSupported && GPU.isHeadlessGLSupported ? test : skip)('ArrayTexture(3) headlessgl', ()=> {
  arrayTexture3('headlessgl');
});
(GPU.isSinglePrecisionSupported ? test : skip)('ArrayTexture(3) cpu', ()=> {
  arrayTexture3('cpu');
});

function arrayTexture4(mode) {
  const gpu = new GPU({ mode });
  function addOne(functionValue) {
    const declaredValue = functionValue[this.thread.x];
    return declaredValue[0] + 1
     + declaredValue[1] + 1
     + declaredValue[2] + 1
     + declaredValue[3] + 1;
  }
  gpu.addFunction(addOne);
  const texture1 = gpu.createKernel(function() {
    return [1,2,3,4];
  }, {
    output: [1],
    precision: 'single',
    pipeline: true,
  })();
  if (mode !== 'cpu') {
    assert.equal(texture1.type, 'ArrayTexture(4)');
  }

  const kernel = gpu.createKernel(function(kernelValue) {
    return addOne(kernelValue);
  }, { output: [1] });
  const result = kernel(texture1);
  assert.equal(result[0], 14);
  gpu.destroy();
}

(GPU.isSinglePrecisionSupported ? test : skip)('ArrayTexture(4) auto', ()=> {
  arrayTexture4();
});
(GPU.isSinglePrecisionSupported ? test : skip)('ArrayTexture(4) gpu', ()=> {
  arrayTexture4('gpu');
});
(GPU.isSinglePrecisionSupported && GPU.isWebGLSupported ? test : skip)('ArrayTexture(4) webgl', ()=> {
  arrayTexture4('webgl');
});
(GPU.isSinglePrecisionSupported && GPU.isWebGL2Supported ? test : skip)('ArrayTexture(4) webgl2', ()=> {
  arrayTexture4('webgl2');
});
(GPU.isSinglePrecisionSupported && GPU.isHeadlessGLSupported ? test : skip)('ArrayTexture(4) headlessgl', ()=> {
  arrayTexture4('headlessgl');
});
(GPU.isSinglePrecisionSupported ? test : skip)('ArrayTexture(4) cpu', ()=> {
  arrayTexture4('cpu');
});

function testTortureTest(mode) {
  const gpu = new GPU({ mode });
  function addFloatArray(addFloatArrayArgument1, addFloatArrayArgument2) {
    return addFloatArrayArgument1 + addFloatArrayArgument2[this.thread.x];
  }
  function addArrayFloat(addArrayFloatArgument1, addArrayFloatArgument2) {
    return addArrayFloatArgument1[this.thread.x] + addArrayFloatArgument2;
  }
  function addArrayArray(addArrayArrayArgument1, addArrayArrayArgument2) {
    return addArrayArrayArgument1[this.thread.x] + addArrayArrayArgument2[this.thread.x];
  }
  function addFloatFloat(addFloatFloatArgument1, addFloatFloatArgument2) {
    return addFloatFloatArgument1 + addFloatFloatArgument2;
  }
  gpu
    .addFunction(addFloatArray)
    .addFunction(addArrayFloat)
    .addFunction(addArrayArray)
    .addFunction(addFloatFloat);

  const texture = gpu.createKernel(function() { return 2; }, { output: [1], precision: 'single' })();
  // sinon.spy(FunctionBuilder.prototype, 'lookupArgumentType');
  sinon.spy(FunctionBuilder.prototype, 'lookupReturnType');

  try {
    const kernel = gpu.createKernel(function (v1, v2, v3, v4, v5) {
      return addFloatFloat(v4, addArrayFloat(v3, addFloatArray(addArrayArray(v1, v5), v2)));
    }, {output: [1]});

    const result = kernel([1], texture, [3], 4, new Float32Array([5]));
    assert.equal(result[0], 1 + 2 + 3 + 4 + 5);
    assert.equal(FunctionBuilder.prototype.lookupReturnType.callCount, 7);
    assert.equal(FunctionBuilder.prototype.lookupReturnType.returnValues.length, 7);
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[0][0], 'addArrayArray');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.returnValues[0], 'Number');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[1][0], 'addFloatArray');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.returnValues[1], 'Number');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[2][0], 'addArrayFloat');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.returnValues[2], 'Number');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[3][0], 'addFloatFloat');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.returnValues[3], 'Float');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[4][0], 'addArrayFloat');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.returnValues[4], 'Number');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[5][0], 'addFloatArray');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.returnValues[5], 'Number');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.args[6][0], 'addArrayArray');
    assert.equal(FunctionBuilder.prototype.lookupReturnType.returnValues[6], 'Number');
  } finally {
    FunctionBuilder.prototype.lookupReturnType.restore();
  }
}

(GPU.isSinglePrecisionSupported ? test : skip)('torture test auto', () => {
  testTortureTest();
});

(GPU.isSinglePrecisionSupported ? test : skip)('torture test gpu', () => {
  testTortureTest('gpu');
});

(GPU.isSinglePrecisionSupported && GPU.isWebGLSupported ? test : skip)('torture test webgl', () => {
  testTortureTest('webgl');
});

(GPU.isSinglePrecisionSupported && GPU.isWebGL2Supported ? test : skip)('torture test webgl2', () => {
  testTortureTest('webgl2');
});

(GPU.isSinglePrecisionSupported && GPU.isHeadlessGLSupported ? test : skip)('torture test headlessgl', () => {
  testTortureTest('headlessgl');
});

test('torture test cpu', () => {
  testTortureTest('cpu');
});

function testKernelMap(mode) {
  const gpu = new GPU({ mode });
  function calc1(v1, v2) {
    return v2[this.thread.x] - v1;
  }
  function calc2(v1, v2) {
    return v1 * v2;
  }
  const kernelMap = gpu.createKernelMap({
    calc1,
    calc2,
  }, function (outputs, targets) {
    const output = outputs[this.thread.x];
    return calc2(calc1(output, targets), output);
  }, { output: [1], pipeline: true });
  try {
    const result = kernelMap([1], [3]);
    assert.equal(result.calc1.toArray()[0], 2);
    assert.equal(result.calc2.toArray()[0], 2);
    assert.equal(result.result.toArray()[0], 2);
  } finally {
    gpu.destroy();
  }
}


(GPU.isWebGLSupported ? test : skip)('kernel map webgl', () => {
  testKernelMap('webgl');
});

(GPU.isWebGL2Supported ? test : skip)('kernel map webgl2', () => {
  testKernelMap('webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('kernel map headlessgl', () => {
  testKernelMap('headlessgl');
});
