/*
 * 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 org.apache.sis.referencing.operation.transform;

import java.util.Map;
import java.util.List;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransformFactory;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.referencing.util.WraparoundApplicator;
import org.apache.sis.referencing.operation.AbstractCoordinateOperation;
import org.apache.sis.referencing.operation.matrix.Matrix3;
import org.apache.sis.referencing.operation.matrix.Matrix4;
import org.apache.sis.referencing.cs.AxesConvention;
import org.apache.sis.referencing.crs.HardCodedCRS;

// Test dependencies
import org.junit.Test;
import org.apache.sis.test.TestCase;

import static org.junit.Assert.*;

// Specific to the main branch:
import static org.apache.sis.test.GeoapiAssert.assertMatrixEquals;


/**
 * Tests {@link WraparoundTransform}.
 *
 * @author  Martin Desruisseaux (Geomatys)
 */
public final class WraparoundTransformTest extends TestCase {
    /**
     * Creates a new test case.
     */
    public WraparoundTransformTest() {
    }

    /**
     * Tests {@link WraparoundTransform#inverse()}.
     *
     * @throws TransformException if a coordinate cannot be computed.
     */
    @Test
    public void testInverse() throws TransformException {
        /*
         * Source range: [ 20 … 380]°.
         * Target range: [-90 … 270]°
         */
        final MathTransform forward = WraparoundTransform.create(3, 1, 360, 200, 90);
        final MathTransform inverse = forward.inverse();
        assertSame(forward, inverse.inverse());
        assertSame(inverse, forward.inverse());             // Expect cached instance.
        final double[] sources = new double[] {
            45, -100, 90,
            60,  -80, 70,
            12,  300, 33
        };
        final double[] targets = new double[9];
        forward.transform(sources, 0, targets, 0, 3);
        assertArrayEquals(new double[] {
            45, -100 + 360, 90,             // Wraparound shall be applied.
            60,  -80,       70,             // No wraparound because already in [-90 … 270]° range.
            12,  300 - 360, 33              // Wraparound in opposite direction.
        }, targets, STRICT);

        inverse.transform(sources, 0, targets, 0, 3);
        assertArrayEquals(new double[] {
            45, -100 + 360, 90,             // Wraparound shall be applied.
            60,  -80 + 360, 70,             // Idem.
            12,  300,       33,             // No wraparound because already in [ 20 … 380]° range.
        }, targets, STRICT);
    }

    /**
     * Tests wraparound on one axis.
     *
     * @throws TransformException if a coordinate cannot be computed.
     */
    @Test
    public void testOneAxis() throws TransformException {
        final var op = new AbstractCoordinateOperation(
                Map.of(AbstractCoordinateOperation.NAME_KEY, "Wrapper"),
                HardCodedCRS.WGS84_LATITUDE_FIRST,
                HardCodedCRS.WGS84_LATITUDE_FIRST.forConvention(AxesConvention.POSITIVE_RANGE),
                null, MathTransforms.scale(3, 5));
        /*
         * Transform should be  [scale & normalization]  →  [wraparound]  →  [denormalization].
         * The wraparound is applied on target coordinates, which is why it appears after [scale].
         * Wrararound is often (but not always) unnecessary on source coordinates if the operation
         * uses trigonometric functions.
         */
        final MathTransform wt = WraparoundApplicator.forTargetCRS(op);
        final List<MathTransform> steps = MathTransforms.getSteps(wt);
        assertEquals(3, steps.size());
        assertEquals(1, ((WraparoundTransform) steps.get(1)).wraparoundDimension);
        /*
         * WraparoundTransform outputs are in [−180 … 180] range, so we expect
         * a 180° shift for getting results in the [0 … 360]° range.
         */
        assertMatrixEquals("denormalize", new Matrix3(
                1,   0,    0,                           // Latitude (no wrap around)
                0,   1,  180,                           // Longitude in [0 … 360] range.
                0,   0,    1),
                MathTransforms.getMatrix(steps.get(2)), STRICT);
        /*
         * The normalization is the inverse of above matrix.
         * But we expect the normalization matrix to be concatenated with the (3, 5) scale operation.
         */
        assertMatrixEquals("normalize", new Matrix3(
                3,  0,    0,                            // 3 is a factor in MathTransforms.scale(…).
                0,  5, -180,                            // 5 is (idem).
                0,  0,    1),
                MathTransforms.getMatrix(steps.get(0)), STRICT);
        /*
         * Test transforming some points.
         */
        final double[] pts = {
            2, -100/5,
            6, -200/5,
            9,  200/5,
            3,  400/5};
        wt.transform(pts, 0, pts, 0, 4);
        assertArrayEquals(new double[] {
             6, 260,
            18, 160,
            27, 200,
             9,  40}, pts, STRICT);
    }

    /**
     * Tests wraparound on two axes. We expect two instances of {@link WraparoundTransform} without linear
     * transform between them. The absence of separation between the two {@link WraparoundTransform}s is an
     * indirect test of {@link WraparoundTransform#tryConcatenate(boolean, MathTransform, MathTransformFactory)}.
     *
     * @throws TransformException if a coordinate cannot be computed.
     */
    @Test
    public void testTwoAxes() throws TransformException {
        final var op = new AbstractCoordinateOperation(
                Map.of(AbstractCoordinateOperation.NAME_KEY, "Wrapper"),
                HardCodedCRS.WGS84_WITH_TIME.forConvention(AxesConvention.POSITIVE_RANGE),
                HardCodedCRS.WGS84_WITH_CYCLIC_TIME, null, MathTransforms.scale(3, 2, 5));
        /*
         * Transform should be  [scale & normalization]  →  [wraparound 1]  →  [wraparound 2]  →  [denormalization].
         * At first an affine transform existed between the two [wraparound] operations, but that affine transform
         * should have been moved by `WraparoundTransform.tryConcatenate(…)` in order to combine them with initial
         * [normalization] and final [denormalization].
         */
        final MathTransform wt = WraparoundApplicator.forTargetCRS(op);
        final List<MathTransform> steps = MathTransforms.getSteps(wt);
        assertEquals(4, steps.size());
        assertEquals(0, ((WraparoundTransform) steps.get(1)).wraparoundDimension);
        assertEquals(2, ((WraparoundTransform) steps.get(2)).wraparoundDimension);
        /*
         * WraparoundTransform outputs are in [−180 … 180] range in longitude case,
         * so we expect a 180° shift for getting results in the [0 … 360]° range.
         */
        assertMatrixEquals("denormalize", new Matrix4(
                1,   0,   0,   0,                           // Longitude in [-180 … 180] range.
                0,   1,   0,   0,                           // Latitude (no wrap around)
                0,   0,   1, 183.5,                         // Day of year in [1 … 366] range.
                0,   0,   0,   1),
                MathTransforms.getMatrix(steps.get(3)), STRICT);
        /*
         * The normalization is the inverse of above matrix (when source and target axes have the same span).
         * But we expect the normalization matrix to be concatenated with the (3, 2, 5) scale operation.
         */
        assertMatrixEquals("normalize", new Matrix4(
                3,   0,   0,    0,                          // 3 is a factor in MathTransforms.scale(…).
                0,   2,   0,    0,                          // 2 is (idem).
                0,   0,   5, -183.5,                        // 5 is (idem).
                0,   0,   0,    1),
                MathTransforms.getMatrix(steps.get(0)), 1E-15);
    }
}
