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

describe('GLKernel');

test('nativeFunctionArguments() parse simple function', () => {
  const result = GLKernel.nativeFunctionArguments(`vec2 myFunction(vec2 longName) {
    return vec2(1, 1);
  }`);

  assert.deepEqual(result, {
    argumentNames: ['longName'],
    argumentTypes: ['Array(2)']
  });
});

test('nativeFunctionArguments() parse simple function with argument that has number', () => {
  const result = GLKernel.nativeFunctionArguments(`vec2 myFunction(vec2 longName123) {
    return vec2(1, 1);
  }`);

  assert.deepEqual(result, {
    argumentNames: ['longName123'],
    argumentTypes: ['Array(2)']
  });
});

test('nativeFunctionArguments() parse simple function, multiple arguments', () => {
  const result = GLKernel.nativeFunctionArguments(`vec2 myFunction(vec3 a,vec3 b,float c) {
    return vec2(1, 1);
  }`);

  assert.deepEqual(result, {
    argumentNames: ['a', 'b', 'c'],
    argumentTypes: ['Array(3)', 'Array(3)', 'Number']
  });
});

test('nativeFunctionArguments() parse simple function, multiple arguments with comments', () => {
  const result = GLKernel.nativeFunctionArguments(`vec2 myFunction(vec3 a /* vec4 b */,vec2 c, /* vec4 d */ float e) {
    return vec2(1, 1);
  }`);

  assert.deepEqual(result, {
    argumentNames: ['a', 'c', 'e'],
    argumentTypes: ['Array(3)', 'Array(2)', 'Number']
  });
});

test('nativeFunctionArguments() parse simple function, multiple arguments on multi line with spaces', () => {
  const result = GLKernel.nativeFunctionArguments(`vec2 myFunction(
    vec4  a,
    vec3  b,
    float  c
  ) {
    vec3 delta = a - b;
  }`);

  assert.deepEqual(result, {
    argumentNames: ['a', 'b', 'c'],
    argumentTypes: ['Array(4)', 'Array(3)', 'Number']
  });
});

test('nativeFunctionArguments() parse simple function, multiple arguments on multi line with spaces and multi-line-comments', () => {
  const result = GLKernel.nativeFunctionArguments(`vec2 myFunction(
    vec2  a,
    /* test 1 */
    vec3  b,
    /* test 2 */
    float  c
    /* test 3 */
  ) {
    vec3 delta = a - b;
  }`);

  assert.deepEqual(result, {
    argumentNames: ['a', 'b', 'c'],
    argumentTypes: ['Array(2)', 'Array(3)', 'Number']
  });
});

test('nativeFunctionArguments() parse simple function, multiple arguments on multi line with spaces and in-line-comments', () => {
  const result = GLKernel.nativeFunctionArguments(`vec2 myFunction(
    vec2  a, // test 1
    vec4  b, // test 2
    int  c // test 3
  ) {
    vec3 delta = a - b;
  }`);

  assert.deepEqual(result, {
    argumentNames: ['a', 'b', 'c'],
    argumentTypes: ['Array(2)', 'Array(4)', 'Integer']
  });
});

test('nativeFunctionArguments() parse simple function that is cut short', () => {
  const result = GLKernel.nativeFunctionArguments(`vec2 myFunction(
    vec2  a,
    vec3  b,
    float  c
  )`);

  assert.deepEqual(result, {
    argumentNames: ['a', 'b', 'c'],
    argumentTypes: ['Array(2)', 'Array(3)', 'Number']
  });
});

test('getVariablePrecisionString() when tactic is set to "speed" returns "lowp"', () => {
  assert.equal(GLKernel.prototype.getVariablePrecisionString.call({ tactic: 'speed' }), 'lowp');
});

test('getVariablePrecisionString() when tactic is set to "balanced" returns "mediump"', () => {
  assert.equal(GLKernel.prototype.getVariablePrecisionString.call({ tactic: 'balanced' }), 'mediump');
});

test('getVariablePrecisionString() when tactic is set to "precision" returns "highp"', () => {
  assert.equal(GLKernel.prototype.getVariablePrecisionString.call({ tactic: 'precision' }), 'highp');
});

test('getVariablePrecisionString() when tactic is not set and texSize is within lowFloatPrecision', () => {
  const mockInstance = {
    tactic: null,
    constructor: {
      features: {
        lowFloatPrecision: { rangeMax: Math.log2(3 * 3) },
        mediumFloatPrecision: { rangeMax: Math.log2(4 * 4) },
        highFloatPrecision: { rangeMax: Math.log2(5 * 5) },
        isSpeedTacticSupported: true,
      }
    }
  };
  const textureSize = [2, 2];
  assert.equal(GLKernel.prototype.getVariablePrecisionString.call(mockInstance, textureSize), 'lowp');
});

test('getVariablePrecisionString() when tactic is not set and texSize is within mediumFloatPrecision', () => {
  const mockInstance = {
    tactic: null,
    constructor: {
      features: {
        lowFloatPrecision: { rangeMax: Math.log2(3 * 3) },
        mediumFloatPrecision: { rangeMax: Math.log2(4 * 4) },
        highFloatPrecision: { rangeMax: Math.log2(5 * 5) },
        isSpeedTacticSupported: true,
      }
    }
  };
  const textureSize = [4, 4];
  assert.equal(GLKernel.prototype.getVariablePrecisionString.call(mockInstance, textureSize), 'mediump');
});

test('getVariablePrecisionString() when tactic is not set and texSize is within highFloatPrecision', () => {
  const mockInstance = {
    tactic: null,
    constructor: {
      features: {
        lowFloatPrecision: { rangeMax: Math.log2(3 * 3) },
        mediumFloatPrecision: { rangeMax: Math.log2(4 * 4) },
        highFloatPrecision: { rangeMax: Math.log2(5 * 5) },
        isSpeedTacticSupported: true,
      }
    }
  };
  const textureSize = [5, 5];
  assert.equal(GLKernel.prototype.getVariablePrecisionString.call(mockInstance, textureSize), 'highp');
});

test('getVariablePrecisionString() when tactic is not set and texSize is outside highFloatPrecision', () => {
  const mockInstance = {
    tactic: null,
    constructor: {
      features: {
        lowFloatPrecision: { rangeMax: Math.log2(3 * 3) },
        mediumFloatPrecision: { rangeMax: Math.log2(4 * 4) },
        highFloatPrecision: { rangeMax: Math.log2(5 * 5) },
        isSpeedTacticSupported: true,
      }
    }
  };
  const textureSize = [6, 6];
  assert.throws(() => GLKernel.prototype.getVariablePrecisionString.call(mockInstance, textureSize));
});

test('getVariablePrecisionString() when tactic is not set and texSize is within lowIntPrecision', () => {
  const mockInstance = {
    tactic: null,
    constructor: {
      features: {
        lowIntPrecision: { rangeMax: Math.log2(3 * 3) },
        mediumIntPrecision: { rangeMax: Math.log2(4 * 4) },
        highIntPrecision: { rangeMax: Math.log2(5 * 5) },
        isSpeedTacticSupported: true,
      }
    }
  };
  const textureSize = [2, 2];
  assert.equal(GLKernel.prototype.getVariablePrecisionString.call(mockInstance, textureSize, null, true), 'lowp');
});

test('getVariablePrecisionString() when tactic is not set and texSize is within mediumIntPrecision', () => {
  const mockInstance = {
    tactic: null,
    constructor: {
      features: {
        lowIntPrecision: { rangeMax: Math.log2(3 * 3) },
        mediumIntPrecision: { rangeMax: Math.log2(4 * 4) },
        highIntPrecision: { rangeMax: Math.log2(5 * 5) },
        isSpeedTacticSupported: true,
      }
    }
  };
  const textureSize = [4, 4];
  assert.equal(GLKernel.prototype.getVariablePrecisionString.call(mockInstance, textureSize, null, true), 'mediump');
});

test('getVariablePrecisionString() when tactic is not set and texSize is within highIntPrecision', () => {
  const mockInstance = {
    tactic: null,
    constructor: {
      features: {
        lowIntPrecision: { rangeMax: Math.log2(3 * 3) },
        mediumIntPrecision: { rangeMax: Math.log2(4 * 4) },
        highIntPrecision: { rangeMax: Math.log2(5 * 5) },
        isSpeedTacticSupported: true,
      }
    }
  };
  const textureSize = [5, 5];
  assert.equal(GLKernel.prototype.getVariablePrecisionString.call(mockInstance, textureSize, null, true), 'highp');
});

test('getVariablePrecisionString() when tactic is not set and texSize is outside highIntPrecision', () => {
  const mockInstance = {
    tactic: null,
    constructor: {
      features: {
        lowIntPrecision: { rangeMax: Math.log2(3 * 3) },
        mediumIntPrecision: { rangeMax: Math.log2(4 * 4) },
        highIntPrecision: { rangeMax: Math.log2(5 * 5) },
        isSpeedTacticSupported: true,
      }
    }
  };
  const textureSize = [6, 6];
  assert.throws(() => GLKernel.prototype.getVariablePrecisionString.call(mockInstance, textureSize, null, true));
});

test('getVariablePrecisionString() when features.isSpeedTacticSupported is false returns "highp"', () => {
  const mockInstance = {
    tactic: null,
    constructor: {
      features: {
        isSpeedTacticSupported: false,
      }
    }
  };
  const textureSize = [1, 1];
  assert.equal(GLKernel.prototype.getVariablePrecisionString.call(mockInstance, textureSize, null, true), 'highp');
});

function testGetFeatures(canvas, context) {
  const gpu = new GPU({ canvas, context });
  const { Kernel } = gpu;
  Kernel.setupFeatureChecks();
  const features = Kernel.getFeatures();
  assert.ok(typeof features.isFloatRead === 'boolean');
  assert.ok(typeof features.isIntegerDivisionAccurate === 'boolean');
  assert.ok(typeof features.isSpeedTacticSupported === 'boolean');
  assert.ok(typeof features.isTextureFloat === 'boolean');
  assert.ok(typeof features.isDrawBuffers === 'boolean');
  assert.ok(typeof features.kernelMap === 'boolean');
  assert.ok(typeof features.channelCount === 'number');
  assert.ok(typeof features.maxTextureSize === 'number');

  assert.ok(typeof features.lowIntPrecision.rangeMax === 'number');
  assert.ok(typeof features.mediumIntPrecision.rangeMax === 'number');
  assert.ok(typeof features.highIntPrecision.rangeMax === 'number');

  assert.ok(typeof features.lowFloatPrecision.rangeMax === 'number');
  assert.ok(typeof features.mediumFloatPrecision.rangeMax === 'number');
  assert.ok(typeof features.highFloatPrecision.rangeMax === 'number');

  gpu.destroy();
}

(GPU.isWebGLSupported ? test : skip)('getFeatures() webgl', () => {
  const canvas = document.createElement('canvas');
  const context = canvas.getContext('webgl');
  testGetFeatures(canvas, context);
});

(GPU.isWebGL2Supported ? test : skip)('getFeatures() webgl2', () => {
  const canvas = document.createElement('canvas');
  const context = canvas.getContext('webgl2');
  testGetFeatures(canvas, context);
});

(GPU.isHeadlessGLSupported ? test : skip)('getFeatures() headlessgl', () => {
  const canvas = null;
  const context = require('gl')(1, 1);
  testGetFeatures(canvas, context);
});

test('setOutput() throws when not dynamicOutput and already compiled', () => {
  assert.throws(() => {
    GLKernel.prototype.setOutput.call({
      program: {},
      toKernelOutput: () => {},
      dynamicOutput: false
    });
  }, new Error('Resizing a kernel with dynamicOutput: false is not possible'));
});

test('setOutput() when not dynamicOutput and not already compiled', () => {
  const mockInstance = {
    toKernelOutput: () => [100, 100],
    dynamicOutput: false,
    output: null,
  };
  GLKernel.prototype.setOutput.call(mockInstance, [100, 100]);
  assert.deepEqual(mockInstance.output, [100, 100]);
});

test('setOutput() when does not need to trigger recompile', () => {
  const mockContext = {
    bindFramebuffer: sinon.spy(),
    FRAMEBUFFER: 'FRAMEBUFFER',
    viewport: sinon.spy()
  };
  const mockTexture = {
    delete: sinon.spy(),
  };
  const mockMappedTexture = {
    delete: sinon.spy()
  };
  const mock_setupOutputTexture = sinon.spy();
  const mock_setupSubOutputTextures = sinon.spy();
  const mockInstance = {
    context: mockContext,
    program: {},
    texSize: [1, 1],
    framebuffer: {
      width: 0,
      height: 0,
    },
    toKernelOutput: GLKernel.prototype.toKernelOutput,
    dynamicOutput: true,
    getVariablePrecisionString: () => {
      return 'lowp';
    },
    switchKernels: sinon.spy(),
    updateMaxTexSize: sinon.spy(),
    maxTexSize: [123, 321],
    canvas: {
      width: 0,
      height: 0,
    },
    texture: mockTexture,
    mappedTextures: [
      mockMappedTexture
    ],
    _setupOutputTexture: mock_setupOutputTexture,
    _setupSubOutputTextures: mock_setupSubOutputTextures,
  };
  GLKernel.prototype.setOutput.call(mockInstance, [100, 100]);
  assert.equal(mockContext.bindFramebuffer.callCount, 1);
  assert.equal(mockContext.bindFramebuffer.args[0][0], 'FRAMEBUFFER');
  assert.equal(mockContext.bindFramebuffer.args[0][1], mockInstance.framebuffer);
  assert.equal(mockInstance.updateMaxTexSize.callCount, 1);
  assert.equal(mockInstance.framebuffer.width, 100);
  assert.equal(mockInstance.framebuffer.height, 100);
  assert.equal(mockContext.viewport.callCount, 1);
  assert.equal(mockContext.viewport.args[0][0], 0);
  assert.equal(mockContext.viewport.args[0][1], 0);
  assert.equal(mockContext.viewport.args[0][2], 123);
  assert.equal(mockContext.viewport.args[0][3], 321);
  assert.equal(mockInstance.canvas.width, 123);
  assert.equal(mockInstance.canvas.height, 321);
  assert.equal(mockInstance.texture, null);
  assert.equal(mockInstance.mappedTextures, null);
  assert.ok(mockTexture.delete.called);
  assert.ok(mockMappedTexture.delete.called);
  assert.ok(mock_setupOutputTexture.called);
  assert.ok(mock_setupSubOutputTextures.called);
});

test('setOutput() when needs to trigger recompile', () => {
  const mockInstance = {
    program: {},
    texSize: [1, 1],
    toKernelOutput: GLKernel.prototype.toKernelOutput,
    dynamicOutput: true,
    getVariablePrecisionString: (textureSize) => {
      if (textureSize[0] === 1) return 'lowp';
      return 'highp';
    },
    switchKernels: sinon.spy()
  };
  GLKernel.prototype.setOutput.call(mockInstance, [100, 100]);
  assert.ok(mockInstance.switchKernels.callCount, 1);
});