/* globals jasmine, expect, it, describe, beforeEach, isArray, StringStream,
           PostScriptParser, PostScriptLexer, PostScriptEvaluator,
           PostScriptCompiler*/

'use strict';

describe('function', function() {
  beforeEach(function() {
    jasmine.addMatchers({
      toMatchArray: function(util, customEqualityTesters) {
        return {
          compare: function (actual, expected) {
            var result = {};
            if (actual.length !== expected.length) {
              result.pass = false;
              result.message = 'Array length: ' + actual.length +
                               ', expected: ' + expected.length;
              return result;
            }
            result.pass = true;
            for (var i = 0; i < expected.length; i++) {
              var a = actual[i], b = expected[i];
              if (isArray(b)) {
                if (a.length !== b.length) {
                  result.pass = false;
                  break;
                }
                for (var j = 0; j < a.length; j++) {
                  var suba = a[j], subb = b[j];
                  if (suba !== subb) {
                    result.pass = false;
                    break;
                  }
                }
              } else {
                if (a !== b) {
                  result.pass = false;
                  break;
                }
              }
            }
            return result;
          }
        };
      }
    });
  });

  describe('PostScriptParser', function() {
    function parse(program) {
      var stream = new StringStream(program);
      var parser = new PostScriptParser(new PostScriptLexer(stream));
      return parser.parse();
    }
    it('parses empty programs', function() {
      var output = parse('{}');
      expect(output.length).toEqual(0);
    });
    it('parses positive numbers', function() {
      var number = 999;
      var program = parse('{ ' + number + ' }');
      var expectedProgram = [number];
      expect(program).toMatchArray(expectedProgram);
    });
    it('parses negative numbers', function() {
      var number = -999;
      var program = parse('{ ' + number + ' }');
      var expectedProgram = [number];
      expect(program).toMatchArray(expectedProgram);
    });
    it('parses negative floats', function() {
      var number = 3.3;
      var program = parse('{ ' + number + ' }');
      var expectedProgram = [number];
      expect(program).toMatchArray(expectedProgram);
    });
    it('parses operators', function() {
      var program = parse('{ sub }');
      var expectedProgram = ['sub'];
      expect(program).toMatchArray(expectedProgram);
    });
    it('parses if statements', function() {
      var program = parse('{ { 99 } if }');
      var expectedProgram = [3, 'jz', 99];
      expect(program).toMatchArray(expectedProgram);
    });
    it('parses ifelse statements', function() {
      var program = parse('{ { 99 } { 44 } ifelse }');
      var expectedProgram = [5, 'jz', 99, 6, 'j', 44];
      expect(program).toMatchArray(expectedProgram);
    });
    it('handles missing brackets', function() {
      expect(function() { parse('{'); }).toThrow(
        new Error('Unexpected symbol: found undefined expected 1.'));
    });
    it('handles junk after the end', function() {
      var number = 3.3;
      var program = parse('{ ' + number + ' }#');
      var expectedProgram = [number];
      expect(program).toMatchArray(expectedProgram);
    });
  });

  describe('PostScriptEvaluator', function() {
    function evaluate(program) {
      var stream = new StringStream(program);
      var parser = new PostScriptParser(new PostScriptLexer(stream));
      var code = parser.parse();
      var evaluator = new PostScriptEvaluator(code);
      var output = evaluator.execute();
      return output;
    }

    it('pushes stack', function() {
      var stack = evaluate('{ 99 }');
      var expectedStack = [99];
      expect(stack).toMatchArray(expectedStack);
    });
    it('handles if with true', function() {
      var stack = evaluate('{ 1 {99} if }');
      var expectedStack = [99];
      expect(stack).toMatchArray(expectedStack);
    });
    it('handles if with false', function() {
      var stack = evaluate('{ 0 {99} if }');
      var expectedStack = [];
      expect(stack).toMatchArray(expectedStack);
    });
    it('handles ifelse with true', function() {
      var stack = evaluate('{ 1 {99} {77} ifelse }');
      var expectedStack = [99];
      expect(stack).toMatchArray(expectedStack);
    });
    it('handles ifelse with false', function() {
      var stack = evaluate('{ 0 {99} {77} ifelse }');
      var expectedStack = [77];
      expect(stack).toMatchArray(expectedStack);
    });
    it('handles nested if', function() {
      var stack = evaluate('{ 1 {1 {77} if} if }');
      var expectedStack = [77];
      expect(stack).toMatchArray(expectedStack);
    });

    it('abs', function() {
      var stack = evaluate('{ -2 abs }');
      var expectedStack = [2];
      expect(stack).toMatchArray(expectedStack);
    });
    it('adds', function() {
      var stack = evaluate('{ 1 2 add }');
      var expectedStack = [3];
      expect(stack).toMatchArray(expectedStack);
    });
    it('boolean and', function() {
      var stack = evaluate('{ true false and }');
      var expectedStack = [false];
      expect(stack).toMatchArray(expectedStack);
    });
    it('bitwise and', function() {
      var stack = evaluate('{ 254 1 and }');
      var expectedStack = [254 & 1];
      expect(stack).toMatchArray(expectedStack);
    });
    it('calculates the inverse tangent of a number', function() {
      var stack = evaluate('{ 90 atan }');
      var expectedStack = [Math.atan(90)];
      expect(stack).toMatchArray(expectedStack);
    });
    it('handles bitshifting ', function() {
      var stack = evaluate('{ 50 2 bitshift }');
      var expectedStack = [200];
      expect(stack).toMatchArray(expectedStack);
    });
    it('calculates the ceiling value', function() {
      var stack = evaluate('{ 9.9 ceiling }');
      var expectedStack = [10];
      expect(stack).toMatchArray(expectedStack);
    });
    it('copies', function() {
      var stack = evaluate('{ 99 98 2 copy }');
      var expectedStack = [99, 98, 99, 98];
      expect(stack).toMatchArray(expectedStack);
    });
    it('calculates the cosine of a number', function() {
      var stack = evaluate('{ 90 cos }');
      var expectedStack = [Math.cos(90)];
      expect(stack).toMatchArray(expectedStack);
    });
    it('converts to int', function() {
      var stack = evaluate('{ 9.9 cvi }');
      var expectedStack = [9];
      expect(stack).toMatchArray(expectedStack);
    });
    it('converts negatives to int', function() {
      var stack = evaluate('{ -9.9 cvi }');
      var expectedStack = [-9];
      expect(stack).toMatchArray(expectedStack);
    });
    it('converts to real', function() {
      var stack = evaluate('{ 55.34 cvr }');
      var expectedStack = [55.34];
      expect(stack).toMatchArray(expectedStack);
    });
    it('divides', function() {
      var stack = evaluate('{ 6 5 div }');
      var expectedStack = [1.2];
      expect(stack).toMatchArray(expectedStack);
    });
    it('maps division by zero to infinity', function() {
      var stack = evaluate('{ 6 0 div }');
      var expectedStack = [Infinity];
      expect(stack).toMatchArray(expectedStack);
    });
    it('duplicates', function() {
      var stack = evaluate('{ 99 dup }');
      var expectedStack = [99, 99];
      expect(stack).toMatchArray(expectedStack);
    });
    it('accepts an equality', function() {
      var stack = evaluate('{ 9 9 eq }');
      var expectedStack = [true];
      expect(stack).toMatchArray(expectedStack);
    });
    it('rejects an inequality', function() {
      var stack = evaluate('{ 9 8 eq }');
      var expectedStack = [false];
      expect(stack).toMatchArray(expectedStack);
    });
    it('exchanges', function() {
      var stack = evaluate('{ 44 99 exch }');
      var expectedStack = [99, 44];
      expect(stack).toMatchArray(expectedStack);
    });
    it('handles exponentiation', function() {
      var stack = evaluate('{ 10 2 exp }');
      var expectedStack = [100];
      expect(stack).toMatchArray(expectedStack);
    });
    it('pushes false onto the stack', function() {
      var stack = evaluate('{ false }');
      var expectedStack = [false];
      expect(stack).toMatchArray(expectedStack);
    });
    it('calculates the floor value', function() {
      var stack = evaluate('{ 9.9 floor }');
      var expectedStack = [9];
      expect(stack).toMatchArray(expectedStack);
    });
    it('handles greater than or equal to', function() {
      var stack = evaluate('{ 10 9 ge }');
      var expectedStack = [true];
      expect(stack).toMatchArray(expectedStack);
    });
    it('rejects less than for greater than or equal to', function() {
      var stack = evaluate('{ 8 9 ge }');
      var expectedStack = [false];
      expect(stack).toMatchArray(expectedStack);
    });
    it('handles greater than', function() {
      var stack = evaluate('{ 10 9 gt }');
      var expectedStack = [true];
      expect(stack).toMatchArray(expectedStack);
    });
    it('rejects less than or equal for greater than', function() {
      var stack = evaluate('{ 9 9 gt }');
      var expectedStack = [false];
      expect(stack).toMatchArray(expectedStack);
    });
    it('divides to integer', function() {
      var stack = evaluate('{ 2 3 idiv }');
      var expectedStack = [0];
      expect(stack).toMatchArray(expectedStack);
    });
    it('divides to negative integer', function() {
      var stack = evaluate('{ -2 3 idiv }');
      var expectedStack = [0];
      expect(stack).toMatchArray(expectedStack);
    });
    it('duplicates index', function() {
      var stack = evaluate('{ 4 3 2 1 2 index }');
      var expectedStack = [4, 3, 2, 1, 3];
      expect(stack).toMatchArray(expectedStack);
    });
    it('handles less than or equal to', function() {
      var stack = evaluate('{ 9 10 le }');
      var expectedStack = [true];
      expect(stack).toMatchArray(expectedStack);
    });
    it('rejects greater than for less than or equal to', function() {
      var stack = evaluate('{ 10 9 le }');
      var expectedStack = [false];
      expect(stack).toMatchArray(expectedStack);
    });
    it('calculates the natural logarithm', function() {
      var stack = evaluate('{ 10 ln }');
      var expectedStack = [Math.log(10)];
      expect(stack).toMatchArray(expectedStack);
    });
    it('calculates the base 10 logarithm', function() {
      var stack = evaluate('{ 100 log }');
      var expectedStack = [2];
      expect(stack).toMatchArray(expectedStack);
    });
    it('handles less than', function() {
      var stack = evaluate('{ 9 10 lt }');
      var expectedStack = [true];
      expect(stack).toMatchArray(expectedStack);
    });
    it('rejects greater than or equal to for less than', function() {
      var stack = evaluate('{ 10 9 lt }');
      var expectedStack = [false];
      expect(stack).toMatchArray(expectedStack);
    });
    it('performs the modulo operation', function() {
      var stack = evaluate('{ 4 3 mod }');
      var expectedStack = [1];
      expect(stack).toMatchArray(expectedStack);
    });
    it('multiplies two numbers (positive result)', function() {
      var stack = evaluate('{ 9 8 mul }');
      var expectedStack = [72];
      expect(stack).toMatchArray(expectedStack);
    });
    it('multiplies two numbers (negative result)', function() {
      var stack = evaluate('{ 9 -8 mul }');
      var expectedStack = [-72];
      expect(stack).toMatchArray(expectedStack);
    });
    it('accepts an inequality', function() {
      var stack = evaluate('{ 9 8 ne }');
      var expectedStack = [true];
      expect(stack).toMatchArray(expectedStack);
    });
    it('rejects an equality', function() {
      var stack = evaluate('{ 9 9 ne }');
      var expectedStack = [false];
      expect(stack).toMatchArray(expectedStack);
    });
    it('negates', function() {
      var stack = evaluate('{ 4.5 neg }');
      var expectedStack = [-4.5];
      expect(stack).toMatchArray(expectedStack);
    });
    it('boolean not', function() {
      var stack = evaluate('{ true not }');
      var expectedStack = [false];
      expect(stack).toMatchArray(expectedStack);
    });
    it('bitwise not', function() {
      var stack = evaluate('{ 12 not }');
      var expectedStack = [-13];
      expect(stack).toMatchArray(expectedStack);
    });
    it('boolean or', function() {
      var stack = evaluate('{ true false or }');
      var expectedStack = [true];
      expect(stack).toMatchArray(expectedStack);
    });
    it('bitwise or', function() {
      var stack = evaluate('{ 254 1 or }');
      var expectedStack = [254 | 1];
      expect(stack).toMatchArray(expectedStack);
    });
    it('pops stack', function() {
      var stack = evaluate('{ 1 2 pop }');
      var expectedStack = [1];
      expect(stack).toMatchArray(expectedStack);
    });
    it('rolls stack right', function() {
      var stack = evaluate('{ 1 3 2 2 4 1 roll }');
      var expectedStack = [2, 1, 3, 2];
      expect(stack).toMatchArray(expectedStack);
    });
    it('rolls stack left', function() {
      var stack = evaluate('{ 1 3 2 2 4 -1 roll }');
      var expectedStack = [3, 2, 2, 1];
      expect(stack).toMatchArray(expectedStack);
    });
    it('rounds a number', function() {
      var stack = evaluate('{ 9.52 round }');
      var expectedStack = [10];
      expect(stack).toMatchArray(expectedStack);
    });
    it('calculates the sine of a number', function() {
      var stack = evaluate('{ 90 sin }');
      var expectedStack = [Math.sin(90)];
      expect(stack).toMatchArray(expectedStack);
    });
    it('calculates a square root (integer)', function() {
      var stack = evaluate('{ 100 sqrt }');
      var expectedStack = [10];
      expect(stack).toMatchArray(expectedStack);
    });
    it('calculates a square root (float)', function() {
      var stack = evaluate('{ 99 sqrt }');
      var expectedStack = [Math.sqrt(99)];
      expect(stack).toMatchArray(expectedStack);
    });
    it('subtracts (positive result)', function() {
      var stack = evaluate('{ 6 4 sub }');
      var expectedStack = [2];
      expect(stack).toMatchArray(expectedStack);
    });
    it('subtracts (negative result)', function() {
      var stack = evaluate('{ 4 6 sub }');
      var expectedStack = [-2];
      expect(stack).toMatchArray(expectedStack);
    });
    it('pushes true onto the stack', function() {
      var stack = evaluate('{ true }');
      var expectedStack = [true];
      expect(stack).toMatchArray(expectedStack);
    });
    it('truncates a number', function() {
      var stack = evaluate('{ 35.004 truncate }');
      var expectedStack = [35];
      expect(stack).toMatchArray(expectedStack);
    });
    it('calculates an exclusive or value', function() {
      var stack = evaluate('{ 3 9 xor }');
      var expectedStack = [10];
      expect(stack).toMatchArray(expectedStack);
    });
  });


  describe('PostScriptCompiler', function() {
    function check(code, domain, range, samples) {
      var compiler = new PostScriptCompiler();
      var compiledCode = compiler.compile(code, domain, range);
      if (samples === null) {
        expect(compiledCode).toBeNull();
      } else {
        expect(compiledCode).not.toBeNull();
        /*jshint -W054 */
        var fn = new Function('src', 'srcOffset', 'dest', 'destOffset',
                              compiledCode);
        for (var i = 0; i < samples.length; i++) {
          var out = new Float32Array(samples[i].output.length);
          fn(samples[i].input, 0, out, 0);
          expect(Array.prototype.slice.call(out, 0)).
            toMatchArray(samples[i].output);
        }
      }
    }

    it('check compiled add', function() {
      check([0.25, 0.5, 'add'], [], [0, 1], [{input: [], output: [0.75]}]);
      check([0, 'add'], [0, 1], [0, 1], [{input: [0.25], output: [0.25]}]);
      check([0.5, 'add'], [0, 1], [0, 1], [{input: [0.25], output: [0.75]}]);
      check([0, 'exch', 'add'], [0, 1], [0, 1],
            [{input: [0.25], output: [0.25]}]);
      check([0.5, 'exch', 'add'], [0, 1], [0, 1],
            [{input: [0.25], output: [0.75]}]);
      check(['add'], [0, 1, 0, 1], [0, 1],
            [{input: [0.25, 0.5], output: [0.75]}]);
      check(['add'], [0, 1], [0, 1], null);
    });
    it('check compiled sub', function() {
      check([0.5, 0.25, 'sub'], [], [0, 1], [{input: [], output: [0.25]}]);
      check([0, 'sub'], [0, 1], [0, 1], [{input: [0.25], output: [0.25]}]);
      check([0.5, 'sub'], [0, 1], [0, 1], [{input: [0.75], output: [0.25]}]);
      check([0, 'exch', 'sub'], [0, 1], [-1, 1],
            [{input: [0.25], output: [-0.25]}]);
      check([0.75, 'exch', 'sub'], [0, 1], [-1, 1],
            [{input: [0.25], output: [0.5]}]);
      check(['sub'], [0, 1, 0, 1], [-1, 1],
            [{input: [0.25, 0.5], output: [-0.25]}]);
      check(['sub'], [0, 1], [0, 1], null);

      check([1, 'dup', 3, 2, 'roll', 'sub', 'sub'], [0, 1], [0, 1],
            [{input: [0.75], output: [0.75]}]);
    });
    it('check compiled mul', function() {
      check([0.25, 0.5, 'mul'], [], [0, 1], [{input: [], output: [0.125]}]);
      check([0, 'mul'], [0, 1], [0, 1], [{input: [0.25], output: [0]}]);
      check([0.5, 'mul'], [0, 1], [0, 1], [{input: [0.25], output: [0.125]}]);
      check([1, 'mul'], [0, 1], [0, 1], [{input: [0.25], output: [0.25]}]);
      check([0, 'exch', 'mul'], [0, 1], [0, 1], [{input: [0.25], output: [0]}]);
      check([0.5, 'exch', 'mul'], [0, 1], [0, 1],
            [{input: [0.25], output: [0.125]}]);
      check([1, 'exch', 'mul'], [0, 1], [0, 1],
            [{input: [0.25], output: [0.25]}]);
      check(['mul'], [0, 1, 0, 1], [0, 1],
            [{input: [0.25, 0.5], output: [0.125]}]);
      check(['mul'], [0, 1], [0, 1], null);
    });
    it('check compiled max', function() {
      check(['dup', 0.75, 'gt', 7, 'jz', 'pop', 0.75], [0, 1], [0, 1],
            [{input: [0.5], output: [0.5]}]);
      check(['dup', 0.75, 'gt', 7, 'jz', 'pop', 0.75], [0, 1], [0, 1],
            [{input: [1], output: [0.75]}]);
      check(['dup', 0.75, 'gt', 5, 'jz', 'pop', 0.75], [0, 1], [0, 1], null);
    });
    it('check pop/roll/index', function() {
      check([1, 'pop'], [0, 1], [0, 1], [{input: [0.5], output: [0.5]}]);
      check([1, 3, -1, 'roll'], [0, 1, 0, 1], [0, 1, 0, 1, 0, 1],
            [{input: [0.25, 0.5], output: [0.5, 1, 0.25]}]);
      check([1, 3, 1, 'roll'], [0, 1, 0, 1], [0, 1, 0, 1, 0, 1],
            [{input: [0.25, 0.5], output: [1, 0.25, 0.5]}]);
      check([1, 3, 1.5, 'roll'], [0, 1, 0, 1], [0, 1, 0, 1, 0, 1], null);
      check([1, 1, 'index'], [0, 1], [0, 1, 0, 1, 0, 1],
            [{input: [0.5], output: [0.5, 1, 0.5]}]);
      check([1, 3, 'index', 'pop'], [0, 1], [0, 1], null);
      check([1, 0.5, 'index', 'pop'], [0, 1], [0, 1], null);
    });
    it('check input boundaries', function () {
      check([], [0, 0.5], [0, 1], [{input: [1], output: [0.5]}]);
      check([], [0.5, 1], [0, 1], [{input: [0], output: [0.5]}]);
      check(['dup'], [0.5, 0.75], [0, 1, 0, 1],
            [{input: [0], output: [0.5, 0.5]}]);
      check([], [100, 1001], [0, 10000], [{input: [1000], output: [1000]}]);
    });
    it('check output boundaries', function () {
      check([], [0, 1], [0, 0.5], [{input: [1], output: [0.5]}]);
      check([], [0, 1], [0.5, 1], [{input: [0], output: [0.5]}]);
      check(['dup'], [0, 1], [0.5, 1, 0.75, 1],
            [{input: [0], output: [0.5, 0.75]}]);
      check([], [0, 10000], [100, 1001], [{input: [1000], output: [1000]}]);
    });
    it('compile optimized', function () {
      var compiler = new PostScriptCompiler();
      var code = [0, 'add', 1, 1, 3, -1, 'roll', 'sub', 'sub', 1, 'mul'];
      var compiledCode = compiler.compile(code, [0, 1], [0, 1]);
      expect(compiledCode).toEqual(
        'dest[destOffset + 0] = Math.max(0, Math.min(1, src[srcOffset + 0]));');

    });
  });
});
