import BuilderGroup from '../../../../../../src/ol/render/canvas/BuilderGroup.js';
import CanvasBuilder from '../../../../../../src/ol/render/canvas/Builder.js';
import CanvasLineStringBuilder from '../../../../../../src/ol/render/canvas/LineStringBuilder.js';
import CanvasPolygonBuilder from '../../../../../../src/ol/render/canvas/PolygonBuilder.js';
import ExecutorGroup from '../../../../../../src/ol/render/canvas/ExecutorGroup.js';
import Feature from '../../../../../../src/ol/Feature.js';
import Fill from '../../../../../../src/ol/style/Fill.js';
import GeometryCollection from '../../../../../../src/ol/geom/GeometryCollection.js';
import LineString from '../../../../../../src/ol/geom/LineString.js';
import MultiLineString from '../../../../../../src/ol/geom/MultiLineString.js';
import MultiPoint from '../../../../../../src/ol/geom/MultiPoint.js';
import MultiPolygon from '../../../../../../src/ol/geom/MultiPolygon.js';
import Point from '../../../../../../src/ol/geom/Point.js';
import Polygon from '../../../../../../src/ol/geom/Polygon.js';
import Stroke from '../../../../../../src/ol/style/Stroke.js';
import Style from '../../../../../../src/ol/style/Style.js';
import {
  create as createTransform,
  scale as scaleTransform,
} from '../../../../../../src/ol/transform.js';
import {renderFeature} from '../../../../../../src/ol/renderer/vector.js';

describe('ol.render.canvas.BuilderGroup', function () {
  describe('#replay', function () {
    let context, builder, fillCount, transform;
    let strokeCount, beginPathCount, moveToCount, lineToCount;
    let feature0, feature1, feature2, feature3;
    let fill0, fill1, style1, style2;

    /**
     * @param {BuilderGroup} builder The builder to get instructions from.
     * @param {number} [pixelRatio] The pixel ratio.
     * @param {boolean} [overlaps] Whether there is overlaps.
     */
    function execute(builder, pixelRatio, overlaps) {
      const executor = new ExecutorGroup(
        [-180, -90, 180, 90],
        1,
        pixelRatio || 1,
        !!overlaps,
        builder.finish()
      );
      executor.execute(context, 1, transform, 0, false);
    }

    beforeEach(function () {
      transform = createTransform();
      builder = new BuilderGroup(1, [-180, -90, 180, 90], 1, 1, false);
      feature0 = new Feature(
        new Polygon([
          [
            [-90, 0],
            [-45, 45],
            [0, 0],
            [1, 1],
            [0, -45],
            [-90, 0],
          ],
        ])
      );
      feature1 = new Feature(
        new Polygon([
          [
            [-90, -45],
            [-90, 0],
            [0, 0],
            [0, -45],
            [-90, -45],
          ],
        ])
      );
      feature2 = new Feature(
        new Polygon([
          [
            [90, 45],
            [90, 0],
            [0, 0],
            [0, 45],
            [90, 45],
          ],
        ])
      );
      feature3 = new Feature(
        new Polygon([
          [
            [-90, -45],
            [-90, 45],
            [90, 45],
            [90, -45],
            [-90, -45],
          ],
        ])
      );
      fill0 = new Style({
        fill: new Fill({color: 'black'}),
      });
      fill1 = new Style({
        fill: new Fill({color: 'red'}),
      });
      style1 = new Style({
        fill: new Fill({color: 'black'}),
        stroke: new Stroke({color: 'white', width: 1}),
      });
      style2 = new Style({
        fill: new Fill({color: 'white'}),
        stroke: new Stroke({
          color: 'black',
          width: 1,
          lineDash: [3, 6],
          lineDashOffset: 2,
        }),
      });
      fillCount = 0;
      strokeCount = 0;
      beginPathCount = 0;
      moveToCount = 0;
      lineToCount = 0;
      context = {
        fill: function () {
          fillCount++;
        },
        stroke: function () {
          strokeCount++;
        },
        beginPath: function () {
          beginPathCount++;
        },
        clip: function () {
          // remove beginPath, moveTo and lineTo counts for clipping
          beginPathCount--;
          moveToCount--;
          lineToCount -= 3;
        },
        moveTo: function () {
          moveToCount++;
        },
        lineTo: function () {
          lineToCount++;
        },
        closePath: function () {},
        setLineDash: function () {},
        save: function () {},
        restore: function () {},
      };
    });

    it('omits lineTo for repeated coordinates', function () {
      renderFeature(builder, feature0, fill0, 1);
      execute(builder);
      expect(lineToCount).to.be(4);
      lineToCount = 0;
      scaleTransform(transform, 0.25, 0.25);
      execute(builder);
      expect(lineToCount).to.be(3);
    });

    it('does not omit moveTo for repeated coordinates', function () {
      renderFeature(builder, feature0, fill0, 1);
      renderFeature(builder, feature1, fill1, 1);
      execute(builder);
      expect(moveToCount).to.be(2);
    });

    it('batches fill and stroke instructions for same style', function () {
      renderFeature(builder, feature1, style1, 1);
      renderFeature(builder, feature2, style1, 1);
      renderFeature(builder, feature3, style1, 1);
      execute(builder);
      expect(fillCount).to.be(1);
      expect(strokeCount).to.be(1);
      expect(beginPathCount).to.be(1);
    });

    it('batches fill and stroke instructions for different styles', function () {
      renderFeature(builder, feature1, style1, 1);
      renderFeature(builder, feature2, style1, 1);
      renderFeature(builder, feature3, style2, 1);
      execute(builder);
      expect(fillCount).to.be(2);
      expect(strokeCount).to.be(2);
      expect(beginPathCount).to.be(2);
    });

    it('batches fill and stroke instructions for changing styles', function () {
      renderFeature(builder, feature1, style1, 1);
      renderFeature(builder, feature2, style2, 1);
      renderFeature(builder, feature3, style1, 1);
      execute(builder);
      expect(fillCount).to.be(3);
      expect(strokeCount).to.be(3);
      expect(beginPathCount).to.be(3);
    });

    it('does not batch when overlaps is set to true', function () {
      builder = new BuilderGroup(1, [-180, -90, 180, 90], 1, 1, true);
      renderFeature(builder, feature1, style1, 1);
      renderFeature(builder, feature2, style1, 1);
      renderFeature(builder, feature3, style1, 1);
      execute(builder, {}, 1, true);
      expect(fillCount).to.be(3);
      expect(strokeCount).to.be(3);
      expect(beginPathCount).to.be(3);
    });

    it('applies the pixelRatio to the linedash array and offset', function () {
      // replay with a pixelRatio of 2
      builder = new BuilderGroup(1, [-180, -90, 180, 90], 1, 2, true);

      let lineDash,
        lineDashCount = 0,
        lineDashOffset,
        lineDashOffsetCount = 0;

      context.setLineDash = function (lineDash_) {
        lineDashCount++;
        lineDash = lineDash_.slice();
      };

      Object.defineProperty(context, 'lineDashOffset', {
        set: function (lineDashOffset_) {
          lineDashOffsetCount++;
          lineDashOffset = lineDashOffset_;
        },
      });

      renderFeature(builder, feature1, style2, 1);
      renderFeature(builder, feature2, style2, 1);
      execute(builder, {}, 2, true);

      expect(lineDashCount).to.be(1);
      expect(style2.getStroke().getLineDash()).to.eql([3, 6]);
      expect(lineDash).to.eql([6, 12]);

      expect(lineDashOffsetCount).to.be(1);
      expect(style2.getStroke().getLineDashOffset()).to.be(2);
      expect(lineDashOffset).to.be(4);
    });

    describe('use renderer and hitDetectionRenderer defined in style', function () {
      let point, multipoint, linestring, multilinestring;
      let polygon, multipolygon, geometrycollection;

      /**
       * @param {BuilderGroup} builder The builder to get instructions from.
       * @param {number} [pixelRatio] The pixel ratio.
       * @param {boolean} [overlaps] Whether there is overlaps.
       * @param {Array<number>} [coordinate] Used for hit detection.
       */
      function executeHitDetectionForCoordinate(
        builder,
        pixelRatio,
        overlaps,
        coordinate
      ) {
        const executor = new ExecutorGroup(
          [-180, -90, 180, 90],
          1,
          pixelRatio || 1,
          !!overlaps,
          builder.finish()
        );

        executor.execute(context, 1, transform, 0, false);
        executor.forEachFeatureAtCoordinate(coordinate, 1, 0, 1, () => {});
      }

      beforeEach(function () {
        point = new Feature(new Point([45, 90]));
        multipoint = new Feature(
          new MultiPoint([
            [45, 90],
            [90, 45],
          ])
        );
        linestring = new Feature(
          new LineString([
            [45, 90],
            [45, 45],
            [90, 45],
          ])
        );
        multilinestring = new Feature(
          new MultiLineString([
            linestring.getGeometry().getCoordinates(),
            linestring.getGeometry().getCoordinates(),
          ])
        );
        polygon = feature1;
        multipolygon = new Feature(
          new MultiPolygon([
            polygon.getGeometry().getCoordinates(),
            polygon.getGeometry().getCoordinates(),
          ])
        );
        geometrycollection = new Feature(
          new GeometryCollection([
            point.getGeometry(),
            linestring.getGeometry(),
            polygon.getGeometry(),
          ])
        );
      });
      it('calls the renderer function in hit detection', function () {
        const calls = [];
        const style = new Style({
          renderer: function (coords, state) {
            calls.push({
              coords: coords,
              geometry: state.geometry,
              feature: state.feature,
              context: state.context,
              pixelRatio: state.pixelRatio,
              rotation: state.rotation,
              resolution: state.resolution,
            });
          },
        });

        builder = new BuilderGroup(1, [-180, -90, 180, 90], 1, 1, true);
        renderFeature(builder, point, style, 1);
        renderFeature(builder, multipoint, style, 1);
        renderFeature(builder, linestring, style, 1);
        renderFeature(builder, multilinestring, style, 1);
        renderFeature(builder, polygon, style, 1);
        renderFeature(builder, multipolygon, style, 1);
        renderFeature(builder, geometrycollection, style, 1);
        scaleTransform(transform, 0.1, 0.1);
        executeHitDetectionForCoordinate(builder, 1, true, [45, 90]);

        // since renderer will be used for rendering and hit detection
        // expect calls.length to be ass twice was in rendering
        expect(calls.length).to.be(18);
      });

      it('calls the hit detection renderer in hit detection', function () {
        const calls = [];
        const hitDetectionCalls = [];
        const style = new Style({
          renderer: function (coords, state) {
            calls.push({
              coords: coords,
              geometry: state.geometry,
              feature: state.feature,
              context: state.context,
              pixelRatio: state.pixelRatio,
              rotation: state.rotation,
              resolution: state.resolution,
            });
          },
          hitDetectionRenderer: function (coords, state) {
            hitDetectionCalls.push({
              coords: coords,
              geometry: state.geometry,
              feature: state.feature,
              context: state.context,
              pixelRatio: state.pixelRatio,
              rotation: state.rotation,
              resolution: state.resolution,
            });
          },
        });

        builder = new BuilderGroup(1, [-180, -90, 180, 90], 1, 1, true);
        renderFeature(builder, point, style, 1);
        renderFeature(builder, multipoint, style, 1);
        renderFeature(builder, linestring, style, 1);
        renderFeature(builder, multilinestring, style, 1);
        renderFeature(builder, polygon, style, 1);
        renderFeature(builder, multipolygon, style, 1);
        renderFeature(builder, geometrycollection, style, 1);
        scaleTransform(transform, 0.1, 0.1);
        executeHitDetectionForCoordinate(builder, 1, true, [45, 90]);
        expect(calls.length).to.be(9);
        expect(hitDetectionCalls.length).to.be(9);
      });

      it('calls the renderer function configured for the style', function () {
        const calls = [];
        const style = new Style({
          renderer: function (coords, state) {
            calls.push({
              coords: coords,
              geometry: state.geometry,
              feature: state.feature,
              context: state.context,
              pixelRatio: state.pixelRatio,
              rotation: state.rotation,
              resolution: state.resolution,
            });
          },
        });

        builder = new BuilderGroup(1, [-180, -90, 180, 90], 1, 1, true);
        renderFeature(builder, point, style, 1);
        renderFeature(builder, multipoint, style, 1);
        renderFeature(builder, linestring, style, 1);
        renderFeature(builder, multilinestring, style, 1);
        renderFeature(builder, polygon, style, 1);
        renderFeature(builder, multipolygon, style, 1);
        renderFeature(builder, geometrycollection, style, 1);
        scaleTransform(transform, 0.1, 0.1);
        execute(builder, 1, true);
        expect(calls.length).to.be(9);
        expect(calls[0].geometry).to.be(point.getGeometry());
        expect(calls[0].feature).to.be(point);
        expect(calls[0].context).to.be(context);
        expect(calls[0].pixelRatio).to.be(1);
        expect(calls[0].rotation).to.be(0);
        expect(calls[0].resolution).to.be(1);
        expect(calls[0].coords).to.eql([4.5, 9]);
        expect(calls[1].feature).to.be(multipoint);
        expect(calls[1].coords[0]).to.eql([4.5, 9]);
        expect(calls[2].feature).to.be(linestring);
        expect(calls[2].coords[0]).to.eql([4.5, 9]);
        expect(calls[3].feature).to.be(multilinestring);
        expect(calls[3].coords[0][0]).to.eql([4.5, 9]);
        expect(calls[4].feature).to.be(polygon);
        expect(calls[4].coords[0][0]).to.eql([-9, -4.5]);
        expect(calls[5].feature).to.be(multipolygon);
        expect(calls[5].coords[0][0][0]).to.eql([-9, -4.5]);
        expect(calls[6].feature).to.be(geometrycollection);
        expect(calls[6].geometry.getCoordinates()).to.eql([45, 90]);
        expect(calls[7].geometry.getCoordinates()[0]).to.eql([45, 90]);
        expect(calls[8].geometry.getCoordinates()[0][0]).to.eql([-90, -45]);
      });
    });
  });
});

describe('ol.render.canvas.Builder', function () {
  describe('constructor', function () {
    it('creates a new replay batch', function () {
      const tolerance = 10;
      const extent = [-180, -90, 180, 90];
      const replay = new CanvasBuilder(tolerance, extent, 1, 1, true);
      expect(replay).to.be.a(CanvasBuilder);
    });
  });

  describe('#appendFlatLineCoordinates()', function () {
    let replay;
    beforeEach(function () {
      replay = new CanvasBuilder(1, [-180, -90, 180, 90], 1, 1, true);
    });

    it('appends coordinates that are within the max extent', function () {
      const flat = [-110, 45, 110, 45, 110, -45, -110, -45];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, false, false);
      expect(replay.coordinates).to.eql(flat);
    });

    it('appends polygon coordinates that are within the max extent', function () {
      const flat = [-110, 45, 110, 45, 110, -45, -110, -45, -110, 45];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, true, false);
      expect(replay.coordinates).to.eql(flat);
    });

    it('appends polygon coordinates that are within the max extent (skipping first)', function () {
      const flat = [-110, 45, 110, 45, 110, -45, -110, -45, -110, 45];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, true, true);
      expect(replay.coordinates).to.eql([
        110, 45, 110, -45, -110, -45, -110, 45,
      ]);
    });

    it('works with a single coordinate (inside)', function () {
      const flat = [-110, 45];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, false, false);
      expect(replay.coordinates).to.eql(flat);
    });

    it('always appends first point (even if outside)', function () {
      // this could be changed, but to make the code simpler for properly
      // closing rings, we always add the first point
      const flat = [-110, 145];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, false, false);
      expect(replay.coordinates).to.eql(flat);
    });

    it('always appends first polygon vertex (even if outside)', function () {
      // this could be changed, but to make the code simpler for properly
      // closing rings, we always add the first point
      const flat = [-110, 145, -110, 145];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, true, false);
      expect(replay.coordinates).to.eql(flat);
    });

    it('skips first polygon vertex upon request (also when outside)', function () {
      const flat = [-110, 145, -110, 145];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, true, true);
      expect(replay.coordinates).to.eql([-110, 145]);
    });

    it('appends points when segments cross (top to bottom)', function () {
      // this means we get a few extra points when coordinates are not
      // part of a linestring or ring, but only a few extra
      const flat = [0, 200, 0, -200];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, false, false);
      expect(replay.coordinates).to.eql(flat);
    });

    it('appends points when segments cross (top to inside)', function () {
      const flat = [0, 200, 0, 0];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, false, false);
      expect(replay.coordinates).to.eql(flat);
    });

    it('always appends the first segment (even when outside)', function () {
      // this could be changed, but to make the code simpler for properly
      // closing rings, we always add the first segment
      const flat = [-10, 200, 10, 200];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, false, false);
      expect(replay.coordinates).to.eql(flat);
    });

    it('always appends the first polygon segment (even when outside)', function () {
      // this could be changed, but to make the code simpler for properly
      // closing rings, we always add the first segment
      const flat = [-10, 200, 10, 200, -10, 200];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, true, false);
      expect(replay.coordinates).to.eql(flat);
    });

    it('skips first polygon segment upon request (also when outside)', function () {
      const flat = [-10, 200, 10, 200, -10, 200];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, true, true);
      expect(replay.coordinates).to.eql([10, 200, -10, 200]);
    });

    it('eliminates segments outside (and not changing rel)', function () {
      const flat = [0, 0, 0, 200, 5, 200, 10, 200];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, false, false);
      expect(replay.coordinates).to.eql([0, 0, 0, 200]);
    });

    it('eliminates polygon segments outside (and not changing rel)', function () {
      const flat = [0, 0, 0, 200, 5, 200, 10, 200, 0, 0];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, true, false);
      expect(replay.coordinates).to.eql([0, 0, 0, 200, 10, 200, 0, 0]);
    });

    it('eliminates polygon segments outside (skipping first and not changing rel)', function () {
      const flat = [0, 0, 0, 10, 0, 200, 5, 200, 10, 200, 0, 0];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, true, true);
      expect(replay.coordinates).to.eql([0, 10, 0, 200, 10, 200, 0, 0]);
    });

    it('eliminates segments outside (and not changing rel)', function () {
      const flat = [0, 0, 0, 200, 10, 200];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, false, false);
      expect(replay.coordinates).to.eql([0, 0, 0, 200]);
    });

    it('includes polygon segments outside (and not changing rel) when on last segment', function () {
      const flat = [0, 0, 0, 200, 10, 200, 0, 0];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, true, false);
      expect(replay.coordinates).to.eql(flat);
    });

    it('includes polygon segments outside (skipping first and not changing rel) when on last segment', function () {
      const flat = [0, 0, 0, 200, 10, 200, 0, 0];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, true, true);
      expect(replay.coordinates).to.eql([0, 200, 10, 200, 0, 0]);
    });

    it('includes outside segments that change relationship', function () {
      const flat = [0, 0, 0, 200, 200, 200, 250, 200];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, false, false);
      expect(replay.coordinates).to.eql([0, 0, 0, 200, 200, 200]);
    });

    it('includes outside polygon segments that change relationship when on last segment', function () {
      const flat = [0, 0, 0, 200, 200, 200, 250, 200, 0, 0];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, true, false);
      expect(replay.coordinates).to.eql(flat);
    });

    it('includes outside polygon segments that change relationship when on last segment (when skipping first)', function () {
      const flat = [0, 0, 0, 200, 200, 200, 250, 200, 0, 0];
      replay.appendFlatLineCoordinates(flat, 0, flat.length, 2, true, true);
      expect(replay.coordinates).to.eql([0, 200, 200, 200, 250, 200, 0, 0]);
    });
  });
});

describe('ol.render.canvas.LineStringBuilder', function () {
  describe('#getBufferedMaxExtent()', function () {
    it('buffers the max extent to accommodate stroke width', function () {
      const tolerance = 1;
      const extent = [-180, -90, 180, 90];
      const resolution = 10;
      const replay = new CanvasLineStringBuilder(tolerance, extent, resolution);
      const stroke = new Stroke({
        width: 2,
      });
      replay.setFillStrokeStyle(null, stroke);
      const buffered = replay.getBufferedMaxExtent();
      expect(buffered).to.eql([-195, -105, 195, 105]);
    });
  });
});

describe('ol.render.canvas.PolygonBuilder', function () {
  let replay;

  beforeEach(function () {
    const tolerance = 1;
    const extent = [-180, -90, 180, 90];
    const resolution = 10;
    replay = new CanvasPolygonBuilder(tolerance, extent, resolution);
  });

  describe('#drawFlatCoordinatess_()', function () {
    it('returns correct offset', function () {
      const coords = [1, 2, 3, 4, 5, 6, 1, 2, 1, 2, 3, 4, 5, 6, 1, 2];
      const ends = [7, 14];
      const stroke = new Stroke({
        width: 5,
      });
      replay.setFillStrokeStyle(null, stroke);
      let offset = replay.drawFlatCoordinatess_(coords, 0, ends, 2);
      expect(offset).to.be(14);
      replay.setFillStrokeStyle(null, null);
      offset = replay.drawFlatCoordinatess_(coords, 0, ends, 2);
      expect(offset).to.be(14);
    });
  });

  describe('#getBufferedMaxExtent()', function () {
    it('buffers the max extent to accommodate stroke width', function () {
      const stroke = new Stroke({
        width: 5,
      });
      replay.setFillStrokeStyle(null, stroke);
      const buffered = replay.getBufferedMaxExtent();
      expect(buffered).to.eql([-210, -120, 210, 120]);
    });
  });
});
