/*
 * 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.Iterator;
import java.util.Spliterator;
import java.util.stream.Stream;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import org.opengis.util.FactoryException;
import org.opengis.geometry.DirectPosition;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.internal.Resources;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.util.collection.BackingStoreException;
import org.apache.sis.coordinate.DefaultCoordinateMetadata;
import org.apache.sis.coordinate.AbstractCoordinateSet;

// Specific to the main branch:
import org.opengis.geometry.MismatchedDimensionException;


/**
 * The result of transforming coordinate tuples using the math transform of a given coordinate operation.
 *
 * @author  Martin Desruisseaux (Geomatys)
 *
 * @todo The current implementation is inefficient.
 */
final class TransformedCoordinateSet extends AbstractCoordinateSet implements UnaryOperator<DirectPosition> {
    /**
     * Serial number for inter-operability with different versions.
     */
    private static final long serialVersionUID = -6533100977777070894L;

    /**
     * The data to transform.
     */
    private final AbstractCoordinateSet data;

    /**
     * The transform to apply on coordinate tuples.
     *
     * @see #iterator()
     * @see #stream()
     */
    @SuppressWarnings("serial")     // Apache SIS implementations of this interface are serializable.
    private final MathTransform transform;

    /**
     * Creates a new transformed coordinate set.
     *
     * @param  op    the coordinate operation to apply.
     * @param  data  the coordinate tuples to transform.
     * @throws TransformException if the transform cannot be prepared.
     */
    TransformedCoordinateSet(final AbstractCoordinateOperation op, final AbstractCoordinateSet data)
            throws TransformException
    {
        super(new DefaultCoordinateMetadata(op.getTargetCRS(), op.getTargetEpoch().orElse(null)));
        @SuppressWarnings("LocalVariableHidesMemberVariable")
        MathTransform transform = op.getMathTransform();
        if (transform == null) {
            throw new TransformException(Resources.format(Resources.Keys.OperationHasNoTransform_2, op.getClass(), op.getName()));
        }
        final DefaultCoordinateMetadata metadata = data.getCoordinateMetadata();
        if (metadata != null) try {
            GeographicBoundingBox aoi = CRS.getGeographicBoundingBox(op);
            final var step = new DefaultCoordinateMetadata(op.getSourceCRS(), op.getSourceEpoch().orElse(null));
            transform = MathTransforms.concatenate(CRS.findOperation(metadata, step, aoi).getMathTransform(), transform);
        } catch (FactoryException | MismatchedDimensionException e) {
            throw new TransformException(e.getMessage(), e);
        }
        this.transform = transform;
        this.data = data;
    }

    /**
     * Returns the number of dimension of output coordinate tuples.
     * This method is overridden in case that {@link #crs} is null.
     */
    @Override
    public int getDimension() {
        return transform.getTargetDimensions();
    }

    /**
     * Returns the transformed positions described by coordinate tuples.
     */
    @Override
    public Iterator<DirectPosition> iterator() {
        return stream().iterator();
    }

    /**
     * Returns the transformed positions described by coordinate tuples.
     */
    @Override
    public Spliterator<DirectPosition> spliterator() {
        return stream().spliterator();
    }

    /**
     * Returns a stream of transformed coordinate tuples.
     */
    @Override
    public Stream<DirectPosition> stream() {
        return data.stream().map(this);
    }

    /**
     * Performs an action for each coordinate tuple of this stream.
     *
     * @param action the action to perform.
     */
    @Override
    public void forEach(Consumer<? super DirectPosition> action) {
        stream().forEach(action);
    }

    /**
     * Transforms the given coordinate tuples.
     *
     * @param  source  coordinates in source CRS.
     * @return coordinates in target CRS.
     */
    @Override
    public DirectPosition apply(final DirectPosition source) {
        try {
            return transform.transform(source, null);
        } catch (TransformException e) {
            throw new BackingStoreException(e);
        }
    }
}
