/* Copyright 2017 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
  PostScriptCompiler,
  PostScriptEvaluator,
} from "../../src/core/function.js";
import { PostScriptLexer, PostScriptParser } from "../../src/core/ps_parser.js";
import { StringStream } from "../../src/core/stream.js";

describe("function", function () {
  beforeEach(function () {
    jasmine.addMatchers({
      toMatchArray(util, customEqualityTesters) {
        return {
          compare(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 (Array.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();
        // eslint-disable-next-line no-new-func
        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]));"
      );
    });
  });
});
