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

import java.util.HashMap;
import java.util.Iterator;
import java.io.InputStream;
import jakarta.xml.bind.JAXBException;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.parameter.ParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.GeodeticCRS;
import org.opengis.referencing.operation.OperationMethod;
import org.apache.sis.referencing.operation.provider.Mercator1SP;
import org.apache.sis.referencing.operation.transform.LinearTransform;
import org.apache.sis.referencing.operation.matrix.Matrix3;
import org.apache.sis.parameter.ParameterBuilder;
import org.apache.sis.measure.Units;
import org.apache.sis.system.Loggers;
import org.apache.sis.xml.Namespaces;
import org.apache.sis.xml.XML;
import static org.apache.sis.metadata.iso.citation.Citations.EPSG;

// Test dependencies
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.opengis.test.Validators;
import org.apache.sis.xml.bind.referencing.CC_OperationParameterGroupTest;
import org.apache.sis.xml.test.TestCase;
import static org.apache.sis.test.TestUtilities.getSingleton;
import static org.apache.sis.test.TestUtilities.getScope;
import static org.apache.sis.test.TestUtilities.getDomainOfValidity;
import static org.apache.sis.metadata.Assertions.assertXmlEquals;

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


/**
 * Tests XML (un)marshalling of single operations (conversions and transformations).
 *
 * @author  Martin Desruisseaux (Geomatys)
 */
public final class SingleOperationMarshallingTest extends TestCase.WithLogs {
    /**
     * Creates a new test case.
     */
    public SingleOperationMarshallingTest() {
        super(Loggers.XML);
    }

    /**
     * Opens the stream to the XML file in this package containing an operation definition.
     *
     * @param  transformation  {@code true} for a transformation or {@code false} for a conversion.
     * @return stream opened on the XML document to use for testing purpose.
     */
    private static InputStream openTestFile(final boolean transformation) {
        // Call to `getResourceAsStream(…)` is caller sensitive: it must be in the same module.
        return SingleOperationMarshallingTest.class.getResourceAsStream(
                transformation ? "Transformation.xml" : "Conversion.xml");
    }

    /**
     * Creates the test operation method.
     */
    private static DefaultOperationMethod createMercatorMethod() {
        final var builder = new ParameterBuilder();
        builder.setCodeSpace(EPSG, "EPSG").setRequired(true);
        ParameterDescriptor<?>[] parameters = {
            builder.addIdentifier("8801").addName("Latitude of natural origin" ).create(0, Units.DEGREE),
            builder.addIdentifier("8802").addName("Longitude of natural origin").create(0, Units.DEGREE)
            // There is more parameters for a Mercator projection, but 2 is enough for this test.
        };
        builder.addName(null, "Mercator (1SP)");
        final ParameterDescriptorGroup descriptor = builder.createGroup(parameters);
        final var properties = new HashMap<String,Object>(4);
        properties.put(DefaultOperationMethod.NAME_KEY, descriptor.getName());
        properties.put(DefaultOperationMethod.FORMULA_KEY, new DefaultFormula("See EPSG guide."));
        return new DefaultOperationMethod(properties, descriptor);
    }

    /**
     * Tests (un)marshalling of an operation method.
     *
     * @throws JAXBException if an error occurred during marshalling or unmarshalling.
     */
    @Test
    public void testOperationMethod() throws JAXBException {
        final String xml = XML.marshal(createMercatorMethod());
        assertXmlEquals("<gml:OperationMethod xmlns:gml=\"" + Namespaces.GML + "\">\n" +
                        "  <gml:name>Mercator (1SP)</gml:name>\n" +
                        "  <gml:formula>See EPSG guide.</gml:formula>\n" +
                        "  <gml:parameter>\n" +
                        "    <gml:OperationParameter gml:id=\"epsg-parameter-8801\">\n" +
                        "      <gml:identifier codeSpace=\"IOGP\">urn:ogc:def:parameter:EPSG::8801</gml:identifier>\n" +
                        "      <gml:name codeSpace=\"EPSG\">Latitude of natural origin</gml:name>\n" +
                        "    </gml:OperationParameter>\n" +
                        "  </gml:parameter>\n" +
                        "  <gml:parameter>\n" +
                        "    <gml:OperationParameter gml:id=\"epsg-parameter-8802\">\n" +
                        "      <gml:identifier codeSpace=\"IOGP\">urn:ogc:def:parameter:EPSG::8802</gml:identifier>\n" +
                        "      <gml:name codeSpace=\"EPSG\">Longitude of natural origin</gml:name>\n" +
                        "    </gml:OperationParameter>\n" +
                        "  </gml:parameter>\n" +
                        "</gml:OperationMethod>", xml, "xmlns:*");

        final var method = (OperationMethod) XML.unmarshal(xml);
        verifyMethod(method);
        Validators.validate(method);
        loggings.assertNoUnexpectedLog();
    }

    /**
     * Verifies the unmarshalled parameter descriptors.
     */
    private static void verifyMethod(final OperationMethod method) {
        assertIdentifierEquals(null, null, null, "Mercator (1SP)", method.getName(), "name");
        assertEquals("See EPSG guide.", method.getFormula().getFormula().toString(), "formula");
        final ParameterDescriptorGroup parameters = method.getParameters();
        assertEquals("Mercator (1SP)", parameters.getName().getCode(), "parameters.name");
        final Iterator<GeneralParameterDescriptor> it = parameters.descriptors().iterator();
        CC_OperationParameterGroupTest.verifyMethodParameter(Mercator1SP.LATITUDE_OF_ORIGIN,  (ParameterDescriptor<?>) it.next());
        CC_OperationParameterGroupTest.verifyMethodParameter(Mercator1SP.LONGITUDE_OF_ORIGIN, (ParameterDescriptor<?>) it.next());
        assertFalse(it.hasNext());
    }

    /**
     * Tests unmarshalling of a defining conversion.
     *
     * @throws JAXBException if an error occurred during marshalling or unmarshalling.
     */
    @Test
    public void testConversionUnmarshalling() throws JAXBException {
        final DefaultConversion c = unmarshalFile(DefaultConversion.class, openTestFile(false));
        assertEquals("World Mercator", c.getName().getCode(), "name");
        assertEquals("3395", getSingleton(c.getIdentifiers()).getCode(), "identifier");
        assertEquals("Very small scale mapping.", getScope(c), "scope");
        assertNull  (c.getOperationVersion(), "operationVersion");

        final GeographicBoundingBox e = getDomainOfValidity(c);
        assertEquals(+180, e.getEastBoundLongitude(), "eastBoundLongitude");
        assertEquals(-180, e.getWestBoundLongitude(), "westBoundLongitude");
        assertEquals(  84, e.getNorthBoundLatitude(), "northBoundLatitude");
        assertEquals( -80, e.getSouthBoundLatitude(), "southBoundLatitude");

        // This is a defining conversion, so we do not expect CRS.
        assertNull(c.getSourceCRS(), "sourceCRS");
        assertNull(c.getTargetCRS(), "targetCRS");
        assertTrue(c.getInterpolationCRS().isEmpty(), "interpolationCRS");
        assertNull(c.getMathTransform(), "mathTransform");

        // The most difficult part.
        final OperationMethod method = c.getMethod();
        assertNotNull(method, "method");
        verifyMethod(method);

        final ParameterValueGroup parameters = c.getParameterValues();
        assertNotNull(parameters, "parameters");
        final Iterator<GeneralParameterValue> it = parameters.values().iterator();
        verifyParameter(method, parameters,  -0.0, (ParameterValue<?>) it.next());
        verifyParameter(method, parameters, -90.0, (ParameterValue<?>) it.next());
        assertFalse(it.hasNext());
        /*
         * Validate object, then discard warnings caused by duplicated identifiers.
         * Those duplications are intentional, see comment in `Conversion.xml`.
         */
        Validators.validate(c);
        loggings.assertNextLogContains("EPSG::8801");
        loggings.assertNextLogContains("EPSG::8802");
        loggings.assertNoUnexpectedLog();
    }

    /**
     * Verify a parameter value. The descriptor is expected to be the same instance as the descriptors
     * defined in the {@link ParameterValueGroup} and in the {@link OperationMethod}.
     *
     * @param  method         the method of the enclosing operation.
     * @param  group          the group which contain the given parameter.
     * @param  expectedValue  the expected parameter value.
     * @param  parameter      the parameter to verify.
     */
    private static void verifyParameter(final OperationMethod method, final ParameterValueGroup group,
            final double expectedValue, final ParameterValue<?> parameter)
    {
        final ParameterDescriptor<?> descriptor = parameter.getDescriptor();
        final String name = descriptor.getName().getCode();
        assertSame(descriptor,  group.getDescriptor().descriptor(name), "parameterValues.descriptor");
        assertSame(descriptor, method.getParameters().descriptor(name), "method.descriptor");
        assertEquals(expectedValue, parameter.doubleValue(), "value");
    }

    /**
     * Tests unmarshalling of a transformation.
     *
     * @throws JAXBException if an error occurred during marshalling or unmarshalling.
     */
    @Test
    public void testTransformationUnmarshalling() throws JAXBException {
        final DefaultTransformation c = unmarshalFile(DefaultTransformation.class, openTestFile(true));
        assertEquals("NTF (Paris) to NTF (1)", c.getName().getCode(), "name");
        assertEquals("1763", getSingleton(c.getIdentifiers()).getCode(), "identifier");
        assertEquals("Change of prime meridian.", getScope(c), "scope");
        assertEquals("IGN-Fra", c.getOperationVersion(), "operationVersion");

        final OperationMethod method = c.getMethod();
        assertNotNull(method, "method");
        assertEquals("Longitude rotation", method.getName().getCode(), "method.name");
        assertEquals("9601", getSingleton(method.getIdentifiers()).getCode(), "method.identifier");
        assertEquals("Target_longitude = Source_longitude + longitude_offset.", method.getFormula().getFormula().toString(), "method.formula");

        final var descriptor = (ParameterDescriptor<?>) getSingleton(method.getParameters().descriptors());
        assertEquals("Longitude offset", descriptor.getName().getCode(), "descriptor.name");
        assertEquals("8602", getSingleton(descriptor.getIdentifiers()).getCode(), "descriptor.identifier");
        assertEquals(Double.class, descriptor.getValueClass(), "descriptor.valueClass");

        final ParameterValueGroup parameters = c.getParameterValues();
        assertNotNull(parameters, "parameters");
        assertSame(method.getParameters(), parameters.getDescriptor(), "parameters.descriptors");

        final var parameter = (ParameterValue<?>) getSingleton(parameters.values());
        assertSame  (descriptor, parameter.getDescriptor(), "parameters.descriptor");
        assertEquals(Units.GRAD, parameter.getUnit(),       "parameters.unit");
        assertEquals(2.5969213,  parameter.getValue(),      "parameters.value");

        final CoordinateReferenceSystem sourceCRS = c.getSourceCRS();
        assertInstanceOf(GeodeticCRS.class, sourceCRS, "sourceCRS");
        assertEquals("NTF (Paris)", sourceCRS.getName().getCode(), "sourceCRS.name");
        assertEquals("Geodetic survey.", getScope(sourceCRS), "sourceCRS.scope");
        assertEquals("4807", getSingleton(sourceCRS.getIdentifiers()).getCode(), "sourceCRS.identifier");

        final CoordinateReferenceSystem targetCRS = c.getTargetCRS();
        assertInstanceOf(GeodeticCRS.class,  targetCRS, "targetCRS");
        assertEquals("NTF", targetCRS.getName().getCode(), "targetCRS.name");
        assertEquals("Geodetic survey.", getScope(targetCRS), "targetCRS.scope");
        assertEquals("4275", getSingleton(targetCRS.getIdentifiers()).getCode(), "targetCRS.identifier");

        final var tr = assertInstanceOf(LinearTransform.class, c.getMathTransform());
        assertMatrixEquals(new Matrix3(1, 0, 0,
                                       0, 1, 2.33722917,
                                       0, 0, 1),
                tr.getMatrix(), STRICT, "mathTransform.matrix");
        /*
         * Validate object, then discard warnings caused by duplicated identifiers.
         * Those duplications are intentional, see comment in `Transformation.xml`.
         */
        Validators.validate(c);
        loggings.assertNextLogContains("EPSG::8602");
        loggings.assertNoUnexpectedLog();
    }
}
