/*****************************************************************
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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.
 ****************************************************************/
package no.ecc.vectortile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.locationtech.jts.algorithm.Orientation;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;

import junit.framework.TestCase;
import no.ecc.vectortile.VectorTileDecoder.Feature;
import vector_tile.VectorTile;

public class VectorTileEncoderTest extends TestCase {

    private GeometryFactory gf = new GeometryFactory();

    public void testEncode() {
        VectorTileEncoder vtm = new VectorTileEncoder(256);

        List<Coordinate> cs = new ArrayList<Coordinate>();
        cs.add(new Coordinate(3, 6));
        cs.add(new Coordinate(8, 12));
        cs.add(new Coordinate(20, 34));
        Geometry geometry = gf.createLineString(cs.toArray(new Coordinate[cs.size()]));

        cs.add(new Coordinate(33, 72));
        Geometry geometry2 = gf.createLineString(cs.toArray(new Coordinate[cs.size()]));

        Map<String, Object> attributes = new HashMap<String, Object>();

        vtm.addFeature("DEPCNT", attributes, geometry);
        vtm.addFeature("DEPCNT", attributes, geometry2);

        byte[] encoded = vtm.encode();
        assertNotSame(0, encoded.length);

    }

    public void testToGeomType() {
        List<Coordinate> cs = new ArrayList<Coordinate>();
        cs.add(new Coordinate(3, 6));
        cs.add(new Coordinate(8, 12));
        cs.add(new Coordinate(20, 34));
        Geometry geometry = gf.createLineString(cs.toArray(new Coordinate[cs.size()]));
        assertEquals(VectorTile.Tile.GeomType.LINESTRING, VectorTileEncoder.toGeomType(geometry));
    }

    public void testCommands() {

        // Ex.: MoveTo(3, 6), LineTo(8, 12), LineTo(20, 34), ClosePath
        List<Coordinate> cs = new ArrayList<Coordinate>();
        cs.add(new Coordinate(3, 6));
        cs.add(new Coordinate(8, 12));
        cs.add(new Coordinate(20, 34));

        List<Integer> commands = new VectorTileEncoder(256).commands(cs.toArray(new Coordinate[cs.size()]), true);
        assertNotNull(commands);
        // Encoded as: [ 9 6 12 18 10 12 24 44 15 ]
        assertCommand(9, commands, 0);
        assertCommand(6, commands, 1);
        assertCommand(12, commands, 2);
        assertCommand(18, commands, 3);
        assertCommand(10, commands, 4);
        assertCommand(12, commands, 5);
        assertCommand(24, commands, 6);
        assertCommand(44, commands, 7);
        assertCommand(15, commands, 8);
        assertEquals(9, commands.size());

    }
    
    public void testPolygonCommands() {
        
        // https://github.com/mapbox/vector-tile-spec/blob/master/2.1/README.md

        // Ex.: MoveTo(3, 6), LineTo(8, 12), LineTo(20, 34), ClosePath
        List<Coordinate> cs = new ArrayList<Coordinate>();
        cs.add(new Coordinate(3, 6));
        cs.add(new Coordinate(8, 12));
        cs.add(new Coordinate(20, 34));
        cs.add(new Coordinate(3, 6));
        Polygon polygon = gf.createPolygon(cs.toArray(new Coordinate[cs.size()]));

        List<Integer> commands = new VectorTileEncoder(256).commands(polygon);
        assertNotNull(commands);
        // Encoded as: [ 9 6 12 18 10 12 24 44 15 ]
        assertCommand(9, commands, 0);
        assertCommand(6, commands, 1);
        assertCommand(12, commands, 2);
        assertCommand(18, commands, 3);
        assertCommand(10, commands, 4);
        assertCommand(12, commands, 5);
        assertCommand(24, commands, 6);
        assertCommand(44, commands, 7);
        assertCommand(15, commands, 8);
        assertEquals(9, commands.size());

    }
    
    public void testPolygonCommandsReverse() {
        
        // https://github.com/mapbox/vector-tile-spec/blob/master/2.1/README.md

        // Ex.: MoveTo(3, 6), LineTo(8, 12), LineTo(20, 34), ClosePath
        List<Coordinate> cs = new ArrayList<Coordinate>();
        cs.add(new Coordinate(3, 6));
        cs.add(new Coordinate(8, 12));
        cs.add(new Coordinate(20, 34));
        cs.add(new Coordinate(3, 6));
        Collections.reverse(cs);
        Polygon polygon = gf.createPolygon(cs.toArray(new Coordinate[cs.size()]));

        List<Integer> commands = new VectorTileEncoder(256).commands(polygon);
        assertNotNull(commands);
        // Encoded as: [ 9 6 12 18 10 12 24 44 15 ]
        assertCommand(9, commands, 0);
        assertCommand(6, commands, 1);
        assertCommand(12, commands, 2);
        assertCommand(18, commands, 3);
        assertCommand(10, commands, 4);
        assertCommand(12, commands, 5);
        assertCommand(24, commands, 6);
        assertCommand(44, commands, 7);
        assertCommand(15, commands, 8);
        assertEquals(9, commands.size());

    }



    public void testCommandsFilter() {

        // Ex.: MoveTo(3, 6), LineTo(8, 12), LineTo(20, 34), ClosePath
        List<Coordinate> cs = new ArrayList<Coordinate>();
        cs.add(new Coordinate(3, 6));
        cs.add(new Coordinate(8, 12));
        cs.add(new Coordinate(8, 12));
        cs.add(new Coordinate(20, 34));

        List<Integer> commands = new VectorTileEncoder(256).commands(cs.toArray(new Coordinate[cs.size()]), true);
        assertNotNull(commands);
        // Encoded as: [ 9 6 12 18 10 12 24 44 15 ]
        assertCommand(9, commands, 0);
        assertCommand(6, commands, 1);
        assertCommand(12, commands, 2);
        assertCommand(18, commands, 3);
        assertCommand(10, commands, 4);
        assertCommand(12, commands, 5);
        assertCommand(24, commands, 6);
        assertCommand(44, commands, 7);
        assertCommand(15, commands, 8);
        assertEquals(9, commands.size());

    }

    public void testPoint() {

        List<Coordinate> cs = new ArrayList<Coordinate>();
        cs.add(new Coordinate(3, 6));

        List<Integer> commands = new VectorTileEncoder(256).commands(cs.toArray(new Coordinate[cs.size()]), false);
        assertNotNull(commands);

        assertCommand(9, commands, 0);
        assertCommand(6, commands, 1);
        assertCommand(12, commands, 2);
        assertEquals(3, commands.size());

    }

    public void testMultiPoint() {

        List<Coordinate> cs = new ArrayList<Coordinate>();
        cs.add(new Coordinate(5, 7));
        cs.add(new Coordinate(3, 2));

        List<Integer> commands = new VectorTileEncoder(256).commands(cs.toArray(new Coordinate[cs.size()]), false,
                true);
        assertNotNull(commands);

        assertCommand(17, commands, 0);
        assertCommand(10, commands, 1);
        assertCommand(14, commands, 2);
        assertCommand(3, commands, 3);
        assertCommand(9, commands, 4);
        assertEquals(5, commands.size());

    }

    public void testCCWPolygon() {

        // Exterior ring in counter-clockwise order.
        List<Coordinate> cs = new ArrayList<Coordinate>();
        cs.add(new Coordinate(3, 6));
        cs.add(new Coordinate(8, 12));
        cs.add(new Coordinate(20, 34));
        cs.add(new Coordinate(3, 6));

        Coordinate[] coordinates = cs.toArray(new Coordinate[cs.size()]);
        assertTrue(Orientation.isCCW(coordinates));

        Polygon polygon = gf.createPolygon(coordinates);

        List<Integer> commands = new VectorTileEncoder(256).commands(polygon);
        assertNotNull(commands);

        assertCommand(9, commands, 0);
        assertCommand(6, commands, 1);
        assertCommand(12, commands, 2);
        assertCommand(18, commands, 3);
        assertCommand(10, commands, 4);
        assertCommand(12, commands, 5);
        assertCommand(24, commands, 6);
        assertCommand(44, commands, 7);
        assertCommand(15, commands, 8);
        assertEquals(9, commands.size());
    }

    public void testCWPolygon() {

        // Exterior ring in clockwise order.
        List<Coordinate> cs = new ArrayList<Coordinate>();
        cs.add(new Coordinate(3, 6));
        cs.add(new Coordinate(20, 34));
        cs.add(new Coordinate(8, 12));
        cs.add(new Coordinate(3, 6));

        Coordinate[] coordinates = cs.toArray(new Coordinate[cs.size()]);
        assertFalse(Orientation.isCCW(coordinates));

        Polygon polygon = gf.createPolygon(coordinates);

        List<Integer> commands = new VectorTileEncoder(256).commands(polygon);
        assertNotNull(commands);

        assertCommand(9, commands, 0);
        assertCommand(6, commands, 1);
        assertCommand(12, commands, 2);
        assertCommand(18, commands, 3);
        assertCommand(10, commands, 4);
        assertCommand(12, commands, 5);
        assertCommand(24, commands, 6);
        assertCommand(44, commands, 7);
        assertCommand(15, commands, 8);
        assertEquals(9, commands.size());
    }

    public void testExtentWithScale() {

        List<Coordinate> cs = new ArrayList<Coordinate>();
        cs.add(new Coordinate(3, 6));

        List<Integer> commands = new VectorTileEncoder(512).commands(cs.toArray(new Coordinate[cs.size()]), false);
        assertNotNull(commands);

        assertCommand(9, commands, 0);
        assertCommand(12, commands, 1);
        assertCommand(24, commands, 2);
        assertEquals(3, commands.size());
    }

    public void testExtentWithoutScale() {

        List<Coordinate> cs = new ArrayList<Coordinate>();
        cs.add(new Coordinate(6, 300));

        List<Integer> commands = new VectorTileEncoder(512, 8, false).commands(cs.toArray(new Coordinate[cs.size()]),
                false);
        assertNotNull(commands);

        assertCommand(9, commands, 0);
        assertCommand(12, commands, 1);
        assertCommand(600, commands, 2);
        assertEquals(3, commands.size());
    }

    public void testFourEqualPoints() {

        List<Coordinate> cs = new ArrayList<Coordinate>();
        cs.add(new Coordinate(3, 6));
        cs.add(new Coordinate(3, 6));
        cs.add(new Coordinate(3, 6));
        cs.add(new Coordinate(3, 6));

        List<Integer> commands = new VectorTileEncoder(256).commands(cs.toArray(new Coordinate[cs.size()]), false);
        assertNotNull(commands);

        assertCommand(9, commands, 0);
        assertCommand(6, commands, 1);
        assertCommand(12, commands, 2);
        assertEquals(3, commands.size());

    }

    private void assertCommand(int expected, List<Integer> commands, int index) {
        assertEquals(expected, commands.get(index).intValue());
    }

    public void testCommandAndLength() {
        assertEquals(9, VectorTileEncoder.commandAndLength(Command.MoveTo, 1));
        assertEquals(18, VectorTileEncoder.commandAndLength(Command.LineTo, 2));
        assertEquals(15, VectorTileEncoder.commandAndLength(Command.ClosePath, 1));
    }

    public void testZigZagEncode() {
        // https://developers.google.com/protocol-buffers/docs/encoding#types
        assertEquals(0, VectorTileEncoder.zigZagEncode(0));
        assertEquals(1, VectorTileEncoder.zigZagEncode(-1));
        assertEquals(2, VectorTileEncoder.zigZagEncode(1));
        assertEquals(3, VectorTileEncoder.zigZagEncode(-2));
    }

    public void testNullAttributeValue() throws IOException {
        VectorTileEncoder vtm = new VectorTileEncoder(256);
        Geometry geometry = gf.createPoint(new Coordinate(3, 6));

        Map<String, Object> attributes = new HashMap<String, Object>();
        attributes.put("key1", "value1");
        attributes.put("key2", null);
        attributes.put("key3", "value3");

        vtm.addFeature("DEPCNT", attributes, geometry);

        byte[] encoded = vtm.encode();
        assertNotSame(0, encoded.length);

        VectorTileDecoder decoder = new VectorTileDecoder();
        assertEquals(1, decoder.decode(encoded, "DEPCNT").asList().size());
        Map<String, Object> decodedAttributes = decoder.decode(encoded, "DEPCNT").asList().get(0).getAttributes();
        assertEquals("value1", decodedAttributes.get("key1"));
        assertEquals("value3", decodedAttributes.get("key3"));
        assertFalse(decodedAttributes.containsKey("key2"));

    }

    public void testAttributeTypes() throws IOException {
        VectorTileEncoder vtm = new VectorTileEncoder(256);
        Geometry geometry = gf.createPoint(new Coordinate(3, 6));

        Map<String, Object> attributes = new HashMap<String, Object>();
        attributes.put("key1", "value1");
        attributes.put("key2", Integer.valueOf(123));
        attributes.put("key3", Float.valueOf(234.1f));
        attributes.put("key4", Double.valueOf(567.123d));
        attributes.put("key5", Long.valueOf(-123));
        attributes.put("key6", "value6");
        attributes.put("key7", Boolean.TRUE);
        attributes.put("key8", Boolean.FALSE);

        vtm.addFeature("DEPCNT", attributes, geometry);

        byte[] encoded = vtm.encode();
        assertNotSame(0, encoded.length);

        VectorTileDecoder decoder = new VectorTileDecoder();
        assertEquals(1, decoder.decode(encoded, "DEPCNT").asList().size());
        Map<String, Object> decodedAttributes = decoder.decode(encoded, "DEPCNT").asList().get(0).getAttributes();
        assertEquals("value1", decodedAttributes.get("key1"));
        assertEquals(Long.valueOf(123), decodedAttributes.get("key2"));
        assertEquals(Float.valueOf(234.1f), decodedAttributes.get("key3"));
        assertEquals(Double.valueOf(567.123d), decodedAttributes.get("key4"));
        assertEquals(Long.valueOf(-123), decodedAttributes.get("key5"));
        assertEquals("value6", decodedAttributes.get("key6"));
        assertEquals(Boolean.TRUE, decodedAttributes.get("key7"));
        assertEquals(Boolean.FALSE, decodedAttributes.get("key8"));
    }

    public void testProvidedIds() throws IOException {
        VectorTileEncoder vtm = new VectorTileEncoder(256);

        Geometry geometry = gf.createPoint(new Coordinate(3, 6));
        Map<String, String> attributes = Collections.singletonMap("key1", "value1");
        vtm.addFeature("DEPCNT", attributes, geometry, 50);

        List<Feature> features = encodeDecodeFeatures(vtm);
        assertEquals(1, features.size());
        assertEquals(50, features.get(0).getId());
    }

    public void testAutoincrementIds() throws IOException {
        VectorTileEncoder vtm = new VectorTileEncoder(256, 8, true, true);

        for (int i = 0; i < 10; i++) {
            Geometry geometry = gf.createPoint(new Coordinate(3 * i, 6 * i));
            Map<String, String> attributes = Collections.singletonMap("key" + i, "value" + i);
            vtm.addFeature("DEPCNT", attributes, geometry);
        }

        List<Feature> features = encodeDecodeFeatures(vtm);
        for (int i = 0; i < features.size(); i++) {
            assertEquals(i + 1, features.get(i).getId());
        }
    }

    public void testProvidedAndAutoincrementIds() throws IOException {
        VectorTileEncoder vtm = new VectorTileEncoder(256, 8, true, true);

        Geometry geometry = gf.createPoint(new Coordinate(3, 6));
        Map<String, String> attributes = Collections.singletonMap("key1", "value1");
        vtm.addFeature("DEPCNT", attributes, geometry, 50);

        geometry = gf.createPoint(new Coordinate(3, 6));
        attributes = Collections.singletonMap("key1", "value1");
        vtm.addFeature("DEPCNT", attributes, geometry);

        geometry = gf.createPoint(new Coordinate(3, 6));
        attributes = Collections.singletonMap("key1", "value1");
        vtm.addFeature("DEPCNT", attributes, geometry, 27);

        geometry = gf.createPoint(new Coordinate(3, 6));
        attributes = Collections.singletonMap("key1", "value1");
        vtm.addFeature("DEPCNT", attributes, geometry);

        List<Feature> features = encodeDecodeFeatures(vtm);
        assertEquals(4, features.size());
        assertEquals(50, features.get(0).getId());
        assertEquals(51, features.get(1).getId());
        assertEquals(27, features.get(2).getId());
        assertEquals(52, features.get(3).getId());
    }

    public void testNullIds() throws IOException {
        VectorTileEncoder vtm = new VectorTileEncoder(256);

        Geometry geometry = gf.createPoint(new Coordinate(3, 6));
        Map<String, String> attributes = Collections.singletonMap("key1", "value1");
        vtm.addFeature("DEPCNT", attributes, geometry, 50);

        geometry = gf.createPoint(new Coordinate(3, 6));
        attributes = Collections.singletonMap("key1", "value1");
        vtm.addFeature("DEPCNT", attributes, geometry);

        List<Feature> features = encodeDecodeFeatures(vtm);
        assertEquals(2, features.size());
        assertEquals(50, features.get(0).getId());
        assertEquals(0, features.get(1).getId());
    }

    public void testMultiPolygonCommands() throws IOException {
        // see https://github.com/mapbox/vector-tile-spec/blob/master/2.1/README.md

        Coordinate[] cs1 = new Coordinate[5];
        cs1[0] = new Coordinate(0, 0);
        cs1[1] = new Coordinate(10, 0);
        cs1[2] = new Coordinate(10, 10);
        cs1[3] = new Coordinate(0, 10);
        cs1[4] = new Coordinate(0, 0);

        Coordinate[] cs2 = new Coordinate[5];
        cs2[0] = new Coordinate(11, 11);
        cs2[1] = new Coordinate(20, 11);
        cs2[2] = new Coordinate(20, 20);
        cs2[3] = new Coordinate(11, 20);
        cs2[4] = new Coordinate(11, 11);

        Coordinate[] cs2i = new Coordinate[5];
        cs2i[0] = new Coordinate(13, 13);
        cs2i[1] = new Coordinate(13, 17);
        cs2i[2] = new Coordinate(17, 17);
        cs2i[3] = new Coordinate(17, 13);
        cs2i[4] = new Coordinate(13, 13);

        Polygon[] polygons = new Polygon[2];
        polygons[0] = gf.createPolygon(cs1);
        polygons[1] = gf.createPolygon(gf.createLinearRing(cs2), new LinearRing[] { gf.createLinearRing(cs2i) });
        MultiPolygon mp = gf.createMultiPolygon(polygons);

        VectorTileEncoder vte = new VectorTileEncoder(256);
        List<Integer> commands = vte.commands(mp);
        assertEquals(Arrays.asList(9, 0, 0, 26, 20, 0, 0, 20, 19, 0, 15, 9, 22, 2, 26, 18, 0, 0, 18, 17, 0, 15, 9, 4,
                13, 26, 0, 8, 8, 0, 0, 7, 15), commands);
        
        vte = new VectorTileEncoder(256);
        vte.addFeature("x", Collections.emptyMap(), mp);
        
        VectorTileDecoder vtd = new VectorTileDecoder();
        List<Feature> features = vtd.decode(vte.encode()).asList();
        assertEquals(1, features.size());
        MultiPolygon mpe = (MultiPolygon) features.get(0).getGeometry();
        assertEquals(2, mpe.getNumGeometries());
    }

    public void testMultiPolygon() throws IOException {
        Polygon[] polygons = new Polygon[2];
        polygons[0] = (Polygon) gf.createPoint(new Coordinate(13, 16)).buffer(3);
        polygons[1] = (Polygon) gf.createPoint(new Coordinate(24, 25)).buffer(5)
                .symDifference(gf.createPoint(new Coordinate(24, 25)).buffer(1.0));
        MultiPolygon mp = gf.createMultiPolygon(polygons);
        assertTrue(mp.isValid());
        System.out.println(mp.toString());

        Map<String, String> attributes = Collections.singletonMap("key1", "value1");

        VectorTileEncoder vtm = new VectorTileEncoder(256);
        vtm.addFeature("mp", attributes, mp);

        byte[] encoded = vtm.encode();
        assertTrue(encoded.length > 0);

        VectorTileDecoder decoder = new VectorTileDecoder();
        List<Feature> features = decoder.decode(encoded).asList();
        assertEquals(1, features.size());
        MultiPolygon mp2 = (MultiPolygon) features.get(0).getGeometry();
        assertEquals(mp.getNumGeometries(), mp2.getNumGeometries());
    }

    public void testGeometryCollection() throws IOException {
        Geometry[] geometries = new Geometry[2];
        geometries[0] = (Polygon) gf.createPoint(new Coordinate(13, 16)).buffer(3);
        geometries[1] = gf.createPoint(new Coordinate(24, 25));
        GeometryCollection gc = gf.createGeometryCollection(geometries);
        Map<String, String> attributes = Collections.singletonMap("key1", "value1");

        VectorTileEncoder vtm = new VectorTileEncoder(256);
        vtm.addFeature("gc", attributes, gc);

        byte[] encoded = vtm.encode();
        assertTrue(encoded.length > 0);

        VectorTileDecoder decoder = new VectorTileDecoder();
        List<Feature> features = decoder.decode(encoded).asList();
        assertEquals(2, features.size());
        assertTrue(features.get(0).getGeometry() instanceof Polygon);
        assertTrue(features.get(1).getGeometry() instanceof Point);
    }

    private List<Feature> encodeDecodeFeatures(VectorTileEncoder vtm) throws IOException {
        byte[] encoded = vtm.encode();
        assertNotSame(0, encoded.length);

        VectorTileDecoder decoder = new VectorTileDecoder();
        return decoder.decode(encoded, "DEPCNT").asList();
    }
}