/*
 * 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.geometry;

import java.awt.geom.Rectangle2D;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.DirectPosition;

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

import static java.lang.Double.NaN;
import static org.apache.sis.referencing.crs.HardCodedCRS.WGS84;
import static org.junit.Assert.*;
import static org.opengis.test.Validators.validate;
import static org.apache.sis.referencing.Assertions.assertContains;
import static org.apache.sis.referencing.Assertions.assertDisjoint;

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


/**
 * Tests the methods defined in the {@link AbstractEnvelope} class.
 * Various implementations are used for each test.
 *
 * @author  Martin Desruisseaux (IRD, Geomatys)
 */
@DependsOn(GeneralDirectPositionTest.class)
public final class AbstractEnvelopeTest extends TestCase {
    /**
     * Creates a new test case.
     */
    public AbstractEnvelopeTest() {
    }

    /**
     * Enumeration of implementations to be tested.
     * The {@code LAST} constant is for stopping the loops.
     */
    private static final int GENERAL=0, IMMUTABLE=1, RECTANGLE=2, SUBENVELOPE=3, LAST=4;

    /**
     * Creates an envelope of the given type. The type shall be one of the
     * {@link #GENERAL}, {@link #IMMUTABLE} or {@link #RECTANGLE} constants.
     */
    private static Envelope create(final int type,
            final double xmin, final double xmax,
            final double ymin, final double ymax)
    {
        final Envelope envelope;
        switch (type) {
            case GENERAL: {
                final GeneralEnvelope ge = new GeneralEnvelope(2);
                ge.setCoordinateReferenceSystem(WGS84);
                ge.setRange(0, xmin, xmax);
                ge.setRange(1, ymin, ymax);
                envelope = ge;
                break;
            }
            case IMMUTABLE: {
                envelope = new ImmutableEnvelope(new double[] {xmin, ymin}, new double[] {xmax, ymax}, WGS84);
                break;
            }
            case RECTANGLE: {
                envelope = new Envelope2D(WGS84, xmin, ymin, xmax - xmin, ymax - ymin);
                break;
            }
            case SUBENVELOPE: {
                GeneralEnvelope ge = new GeneralEnvelope(5);
                ge.setRange(1, xmin, xmax);
                ge.setRange(2, ymin, ymax);
                ge.setRange(0, 2, 3);               // Following values will be verified in verifyInvariants(…)
                ge.setRange(3, 4, 6);
                ge.setRange(4, 8, 9);
                ge = ge.subEnvelope(1, 3);
                ge.setCoordinateReferenceSystem(WGS84);
                envelope = ge;
                break;
            }
            default: throw new IllegalArgumentException(String.valueOf(type));
        }
        if (PENDING_NEXT_GEOAPI_RELEASE) {
            validate(envelope);
        }
        return envelope;
    }

    /**
     * Verifies some invariants for the given envelope of the given type.
     */
    private static void verifyInvariants(final int type, final Envelope envelope) {
        assertSame(WGS84, envelope.getCoordinateReferenceSystem());
        switch (type) {
            case SUBENVELOPE: {
                // Asserts that other dimensions in the original envelope has not been modified.
                final double[] coordinates = ((SubEnvelope) envelope).coordinates;
                assertEquals(2, coordinates[0], STRICT);
                assertEquals(3, coordinates[5], STRICT);
                assertEquals(4, coordinates[3], STRICT);
                assertEquals(6, coordinates[8], STRICT);
                assertEquals(8, coordinates[4], STRICT);
                assertEquals(9, coordinates[9], STRICT);
                break;
            }
        }
    }

    /**
     * Tests the simple case (no anti-meridian crossing).
     *
     * <pre class="text">
     *     ┌─────────────┐
     *     │  ┌───────┐  │
     *     │  └───────┘  │
     *     └─────────────┘</pre>
     */
    @Test
    public void testSimpleEnvelope() {
        final DirectPosition2D inside  = new DirectPosition2D( 3, 32);
        final DirectPosition2D outside = new DirectPosition2D(-5, 32);
        final Envelope2D contained = (Envelope2D) create(RECTANGLE, -2, 10, 35, 40);
        final Envelope2D intersect = (Envelope2D) create(RECTANGLE, -2, 16, 35, 40);
        final Envelope2D disjoint  = (Envelope2D) create(RECTANGLE, 14, 16, 35, 40);
        for (int type=0; type<LAST; type++) {
            final String label = "Type " + type;
            final Envelope envelope = create(type, -4, 12, 30, 50);
            assertEquals(label, 30, envelope.getMinimum(1), STRICT);
            assertEquals(label, 50, envelope.getMaximum(1), STRICT);
            assertEquals(label, 40, envelope.getMedian (1), STRICT);
            assertEquals(label, 20, envelope.getSpan   (1), STRICT);
            assertEquals(label, -4, envelope.getMinimum(0), STRICT);
            assertEquals(label, 12, envelope.getMaximum(0), STRICT);
            assertEquals(label,  4, envelope.getMedian (0), STRICT);
            assertEquals(label, 16, envelope.getSpan   (0), STRICT);
            switch (type) {
                default: {
                    final AbstractEnvelope ext = (AbstractEnvelope) envelope;
                    assertTrue (label, ext.contains  (inside));
                    assertFalse(label, ext.contains  (outside));
                    assertFalse(label, ext.contains  (intersect, false));
                    assertTrue (label, ext.intersects(intersect, false));
                    assertDisjoint(ext, disjoint);
                    assertContains(ext, contained);
                    break;
                }
                case RECTANGLE: {
                    final Rectangle2D ext = (Rectangle2D) envelope;
                    assertTrue (label, ext.contains  (inside));
                    assertFalse(label, ext.contains  (outside));
                    assertFalse(label, ext.contains  (intersect));
                    assertTrue (label, ext.intersects(intersect));
                    assertDisjoint(ext, disjoint);
                    assertContains(ext, contained);
                    break;
                }
            }
            verifyInvariants(type, envelope);
        }
    }

    /**
     * Tests a case crossing the anti-meridian.
     *
     * <pre class="text">
     *      ─────┐  ┌─────────              ─────┐      ┌─────
     *           │  │  ┌────┐       or      ──┐  │      │  ┌──
     *           │  │  └────┘               ──┘  │      │  └──
     *      ─────┘  └─────────              ─────┘      └─────</pre>
     */
    @Test
    public void testCrossingAntiMeridian() {
        final DirectPosition2D inside  = new DirectPosition2D(18, 32);
        final DirectPosition2D outside = new DirectPosition2D( 3, 32);
        final Envelope2D contained  = (Envelope2D) create(RECTANGLE, 14, 16, 35, 40);
        final Envelope2D intersect  = (Envelope2D) create(RECTANGLE, -2, 16, 35, 40);
        final Envelope2D disjoint   = (Envelope2D) create(RECTANGLE, -2, 10, 35, 40);
        final Envelope2D spanning   = (Envelope2D) create(RECTANGLE, 16, -8, 35, 40);
        for (int type=0; type<LAST; type++) {
            final String label = "Type " + type;
            final Envelope envelope = create(type, 12, -4, 30, 50);
            final DirectPosition lower = envelope.getLowerCorner();
            final DirectPosition upper = envelope.getUpperCorner();
            assertEquals(label,   30, envelope.getMinimum (1), STRICT);
            assertEquals(label,   50, envelope.getMaximum (1), STRICT);
            assertEquals(label,   40, envelope.getMedian  (1), STRICT);
            assertEquals(label,   20, envelope.getSpan    (1), STRICT);
            assertEquals(label,   12, lower   .getOrdinate(0), STRICT);
            assertEquals(label, -180, envelope.getMinimum (0), STRICT);
            assertEquals(label,   -4, upper   .getOrdinate(0), STRICT);
            assertEquals(label, +180, envelope.getMaximum (0), STRICT);
            assertEquals(label, -176, envelope.getMedian  (0), STRICT);
            assertEquals(label,  344, envelope.getSpan    (0), STRICT);         // 360° - testSimpleEnvelope()
            switch (type) {
                default: {
                    final AbstractEnvelope ext = (AbstractEnvelope) envelope;
                    assertTrue (label, ext.contains  (inside));
                    assertFalse(label, ext.contains  (outside));
                    assertFalse(label, ext.contains  (intersect, false));
                    assertTrue (label, ext.intersects(intersect, false));
                    assertDisjoint(ext, disjoint);
                    assertContains(ext, contained);
                    assertContains(ext, spanning);
                    break;
                }
                case RECTANGLE: {
                    final Rectangle2D ext = (Rectangle2D) envelope;
                    assertTrue (label, ext.contains  (inside));
                    assertFalse(label, ext.contains  (outside));
                    assertFalse(label, ext.contains  (intersect));
                    assertTrue (label, ext.intersects(intersect));
                    assertDisjoint(ext, disjoint);
                    assertContains(ext, contained);
                    assertContains(ext, spanning);
                    break;
                }
            }
            verifyInvariants(type, envelope);
        }
    }

    /**
     * Tests a the anti-meridian case with a larger empty space
     * on the left side.
     *
     * <pre class="text">
     *      ───┐    ┌─────────              ───┐      ┌─────
     *         │    │  ┌────┐       or      ───┼──┐   │  ┌──
     *         │    │  └────┘               ───┼──┘   │  └──
     *      ───┘    └─────────              ───┘      └─────</pre>
     */
    @Test
    public void testCrossingAntiMeridianTwice() {
        final DirectPosition2D inside  = new DirectPosition2D(18, 32);
        final DirectPosition2D outside = new DirectPosition2D( 3, 32);
        final Envelope2D contained  = (Envelope2D) create(RECTANGLE, 14, 16, 35, 40);
        final Envelope2D intersect  = (Envelope2D) create(RECTANGLE, -2, 16, 35, 40);
        final Envelope2D disjoint   = (Envelope2D) create(RECTANGLE, -2, 10, 35, 40);
        final Envelope2D spanning   = (Envelope2D) create(RECTANGLE, 16, -8, 35, 40);
        for (int type=0; type<LAST; type++) {
            final String label = "Type " + type;
            final Envelope envelope = create(type, 12, -364, 30, 50);
            final DirectPosition lower = envelope.getLowerCorner();
            final DirectPosition upper = envelope.getUpperCorner();
            assertEquals(label,   30, envelope.getMinimum (1), STRICT);
            assertEquals(label,   50, envelope.getMaximum (1), STRICT);
            assertEquals(label,   40, envelope.getMedian  (1), STRICT);
            assertEquals(label,   20, envelope.getSpan    (1), STRICT);
            assertEquals(label,   12, lower   .getOrdinate(0), STRICT);
            assertEquals(label, -180, envelope.getMinimum (0), STRICT);
            assertEquals(label, -364, upper   .getOrdinate(0), STRICT);
            assertEquals(label, +180, envelope.getMaximum (0), STRICT);
            assertEquals(label,    4, envelope.getMedian  (0), STRICT);     // Note the alternance with the previous test methods.
            assertEquals(label,  NaN, envelope.getSpan    (0), STRICT);     // testCrossingAntiMeridian() + 360°.
            if (envelope instanceof AbstractEnvelope) {
                final AbstractEnvelope ext = (AbstractEnvelope) envelope;
                assertTrue (label, ext.contains  (inside));
                assertFalse(label, ext.contains  (outside));
                assertFalse(label, ext.contains  (intersect, false));
                assertTrue (label, ext.intersects(intersect, false));
                assertFalse(label, ext.contains  (spanning,  false));
                assertTrue (label, ext.intersects(spanning,  false));
                assertDisjoint(ext, disjoint);
                assertContains(ext, contained);
                break;
            }
            if (envelope instanceof Rectangle2D) {
                final Rectangle2D ext = (Rectangle2D) envelope;
                assertTrue (label, ext.contains  (inside));
                assertFalse(label, ext.contains  (outside));
                assertFalse(label, ext.contains  (intersect));
                assertTrue (label, ext.intersects(intersect));
                assertFalse(label, ext.contains  (spanning));
                assertTrue (label, ext.intersects(spanning));
                assertDisjoint(ext, disjoint);
                assertContains(ext, contained);
                break;
            }
            verifyInvariants(type, envelope);
        }
    }

    /**
     * Tests a the anti-meridian case with a larger empty space
     * on the left and right sides.
     */
    @Test
    public void testCrossingAntiMeridianThreeTimes() {
        final DirectPosition2D wasInside = new DirectPosition2D(18, 32);
        final DirectPosition2D outside   = new DirectPosition2D( 3, 32);
        final Envelope2D wasContained = (Envelope2D) create(RECTANGLE, 14, 16, 35, 40);
        final Envelope2D wasIntersect = (Envelope2D) create(RECTANGLE, -2, 16, 35, 40);
        final Envelope2D disjoint     = (Envelope2D) create(RECTANGLE, -2, 10, 35, 40);
        final Envelope2D spanning     = (Envelope2D) create(RECTANGLE, 16, -8, 35, 40);
        for (int type=0; type<LAST; type++) {
            final String label = "Type " + type;
            final Envelope envelope = create(type, 372, -364, 30, 50);
            final DirectPosition lower = envelope.getLowerCorner();
            final DirectPosition upper = envelope.getUpperCorner();
            assertEquals(label,   30, envelope.getMinimum (1), STRICT);
            assertEquals(label,   50, envelope.getMaximum (1), STRICT);
            assertEquals(label,   40, envelope.getMedian  (1), STRICT);
            assertEquals(label,   20, envelope.getSpan    (1), STRICT);
            assertEquals(label,  372, lower   .getOrdinate(0), STRICT);
            assertEquals(label, -180, envelope.getMinimum (0), STRICT);
            assertEquals(label, -364, upper   .getOrdinate(0), STRICT);
            assertEquals(label, +180, envelope.getMaximum (0), STRICT);
            assertEquals(label, -176, envelope.getMedian  (0), STRICT);     // Note the alternance with the previous test methods.
            assertEquals(label,  NaN, envelope.getSpan    (0), STRICT);     // testCrossingAntiMeridianTwice() + 360°.
            switch (type) {
                default: {
                    final AbstractEnvelope ext = (AbstractEnvelope) envelope;
                    assertFalse(label, ext.contains  (wasInside));
                    assertFalse(label, ext.contains  (outside));
                    assertFalse(label, ext.contains  (spanning,  false));
                    assertTrue (label, ext.intersects(spanning,  false));
                    assertDisjoint(ext, wasIntersect);
                    assertDisjoint(ext, disjoint);
                    assertDisjoint(ext, wasContained);
                    break;
                }
                case RECTANGLE: {
                    final Rectangle2D ext = (Rectangle2D) envelope;
                    assertFalse(label, ext.contains  (wasInside));
                    assertFalse(label, ext.contains  (outside));
                    assertFalse(label, ext.contains  (spanning));
                    assertTrue (label, ext.intersects(spanning));
                    assertDisjoint(ext, wasIntersect);
                    assertDisjoint(ext, disjoint);
                    assertDisjoint(ext, wasContained);
                    break;
                }
            }
            verifyInvariants(type, envelope);
        }
    }

    /**
     * Tests an empty envelope from -0 to 0°
     */
    @Test
    public void testRange0() {
        final DirectPosition2D wasInside = new DirectPosition2D(18, 32);
        final DirectPosition2D outside   = new DirectPosition2D( 3, 32);
        final Envelope2D wasContained = (Envelope2D) create(RECTANGLE, 14, 16, 35, 40);
        final Envelope2D intersect    = (Envelope2D) create(RECTANGLE, -2, 16, 35, 40);
        final Envelope2D spanning     = (Envelope2D) create(RECTANGLE, 16, -8, 35, 40);
        for (int type=0; type<LAST; type++) {
            final String label = "Type " + type;
            final Envelope envelope = create(type, -0.0, 0.0, 30, 50);
            assertEquals(label,   30, envelope.getMinimum(1), STRICT);
            assertEquals(label,   50, envelope.getMaximum(1), STRICT);
            assertEquals(label,   40, envelope.getMedian (1), STRICT);
            assertEquals(label,   20, envelope.getSpan   (1), STRICT);
            assertEquals(label, -0.0, envelope.getMinimum(0), STRICT);
            assertEquals(label,  0.0, envelope.getMaximum(0), STRICT);
            assertEquals(label,    0, envelope.getMedian (0), STRICT);
            assertEquals(label,    0, envelope.getSpan   (0), STRICT);
            switch (type) {
                default: {
                    final AbstractEnvelope ext = (AbstractEnvelope) envelope;
                    assertFalse(label, ext.contains  (wasInside));
                    assertFalse(label, ext.contains  (outside));
                    assertFalse(label, ext.contains  (intersect, false));
                    assertTrue (label, ext.intersects(intersect, false));
                    assertFalse(label, ext.contains  (spanning,  false));
                    assertFalse(label, ext.intersects(spanning,  false));
                    assertFalse(label, ext.intersects(spanning,  false));
                    assertDisjoint(ext, wasContained);
                    break;
                }
                case RECTANGLE: {
                    final Rectangle2D ext = (Rectangle2D) envelope;
                    assertFalse(label, ext.contains  (wasInside));
                    assertFalse(label, ext.contains  (outside));
                    assertFalse(label, ext.contains  (intersect));
                    assertTrue (label, ext.intersects(intersect));
                    assertFalse(label, ext.contains  (spanning));
                    assertFalse(label, ext.intersects(spanning));
                    assertFalse(label, ext.intersects(spanning));
                    assertDisjoint(ext, wasContained);
                    break;
                }
            }
            verifyInvariants(type, envelope);
        }
    }

    /**
     * Tests a case crossing the anti-meridian crossing, from 0° to -0°.
     */
    @Test
    public void testRange360() {
        final DirectPosition2D inside     = new DirectPosition2D(18, 32);
        final DirectPosition2D wasOutside = new DirectPosition2D( 3, 32);
        final Envelope2D contained = (Envelope2D) create(RECTANGLE, 14, 16, 35, 40);
        final Envelope2D intersect = (Envelope2D) create(RECTANGLE, -2, 16, 35, 40);
        final Envelope2D spanning  = (Envelope2D) create(RECTANGLE, 16, -8, 35, 40);
        for (int type=0; type<LAST; type++) {
            final String label = "Type " + type;
            final Envelope envelope = create(type, 0.0, -0.0, 30, 50);
            final DirectPosition lower = envelope.getLowerCorner();
            final DirectPosition upper = envelope.getUpperCorner();
            assertEquals(label,   30, envelope.getMinimum (1), STRICT);
            assertEquals(label,   50, envelope.getMaximum (1), STRICT);
            assertEquals(label,   40, envelope.getMedian  (1), STRICT);
            assertEquals(label,   20, envelope.getSpan    (1), STRICT);
            assertEquals(label,  0.0, lower   .getOrdinate(0), STRICT);
            assertEquals(label, -180, envelope.getMinimum (0), STRICT);
            assertEquals(label, -0.0, upper   .getOrdinate(0), STRICT);
            assertEquals(label, +180, envelope.getMaximum (0), STRICT);
            assertEquals(label,  180, envelope.getMedian  (0), STRICT);
            assertEquals(label,  360, envelope.getSpan    (0), STRICT);
            switch (type) {
                default: {
                    final AbstractEnvelope ext = (AbstractEnvelope) envelope;
                    assertTrue(label, ext.contains(inside));
                    assertTrue(label, ext.contains(wasOutside));
                    assertTrue(label, ext.intersects(intersect, false));
                    assertContains(ext, contained);
                    assertContains(ext, spanning);
                    break;
                }
                case RECTANGLE: {
                    final Rectangle2D ext = (Rectangle2D) envelope;
                    assertTrue(label, ext.contains(inside));
                    assertTrue(label, ext.contains(wasOutside));
                    assertTrue(label, ext.intersects(intersect));
                    assertContains(ext, contained);
                    assertContains(ext, spanning);
                    break;
                }
            }
            verifyInvariants(type, envelope);
        }
    }

    /**
     * Tests {@link AbstractEnvelope#toSimpleEnvelopes()} on an empty envelope.
     */
    @Test
    public void testToSimpleEnvelopesOnEmptyEnvelope() {
        for (int type=0; type<LAST; type++) {
            if (type != RECTANGLE) {
                final AbstractEnvelope envelope = (AbstractEnvelope) create(type, 0, 0, 0, 0);
                assertEquals(0, envelope.toSimpleEnvelopes().length);
            }
        }
    }

    /**
     * Tests {@link AbstractEnvelope#toSimpleEnvelopes()} on a simple envelope having no wraparound axis.
     */
    @Test
    @DependsOnMethod("testToSimpleEnvelopesOnEmptyEnvelope")
    public void testToSimpleEnvelopesOnSimpleEnvelope() {
        for (int type=0; type<LAST; type++) {
            if (type != RECTANGLE) {
                final AbstractEnvelope envelope = (AbstractEnvelope) create(type, -20, 30, -10, 15);
                final Envelope[] simples = envelope.toSimpleEnvelopes();
                assertEquals(1, simples.length);
                assertSame(envelope, simples[0]);
            }
        }
    }

    /**
     * Tests {@link AbstractEnvelope#toSimpleEnvelopes()} on a simple envelope having no wraparound axis.
     */
    @Test
    @DependsOnMethod("testToSimpleEnvelopesOnEmptyEnvelope")
    public void testToSimpleEnvelopesOverAntiMeridian() {
        for (int type=0; type<LAST; type++) {
            if (type != RECTANGLE) {
                final AbstractEnvelope envelope = (AbstractEnvelope) create(type, 155, -150, 0, 50);
                final Envelope[] simples = envelope.toSimpleEnvelopes();
                assertEquals(2, simples.length);
                final AbstractEnvelope e0 = (AbstractEnvelope) simples[0];
                final AbstractEnvelope e1 = (AbstractEnvelope) simples[1];

                assertEquals( 155.0, e0.getLower(0), STRICT);
                assertEquals(   0.0, e0.getLower(1), STRICT);
                assertEquals( 180.0, e0.getUpper(0), STRICT);
                assertEquals(  50.0, e0.getUpper(1), STRICT);
                assertEquals(  25.0, e0.getSpan (0), STRICT);
                assertEquals(  50.0, e0.getSpan (1), STRICT);

                assertEquals(-180.0, e1.getLower(0), STRICT);
                assertEquals(   0.0, e1.getLower(1), STRICT);
                assertEquals(-150.0, e1.getUpper(0), STRICT);
                assertEquals(  50.0, e1.getUpper(1), STRICT);
                assertEquals(  30.0, e1.getSpan (0), STRICT);
                assertEquals(  50.0, e1.getSpan (1), STRICT);
            }
        }
    }
}
