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

import java.util.List;
import java.util.Arrays;
import java.awt.image.RenderedImage;
import org.opengis.referencing.datum.PixelInCell;
import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.coverage.grid.GridCoverage;
import org.apache.sis.coverage.grid.GridCoverageBuilder;
import org.apache.sis.coverage.grid.GridCoverageProcessor;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.coverage.grid.GridRoundingMode;
import org.apache.sis.storage.AbstractGridCoverageResource;
import org.apache.sis.storage.event.StoreListeners;
import org.apache.sis.util.ArgumentChecks;


/**
 * A {@link org.apache.sis.storage.GridCoverageResource} in memory.
 * This resource wraps an arbitrary {@link GridCoverage} specified at construction time.
 * Metadata can be specified by overriding {@link #createMetadata()}.
 *
 * @author  Johann Sorel (Geomatys)
 * @author  Martin Desruisseaux (Geomatys)
 */
public final class MemoryGridResource extends AbstractGridCoverageResource {
    /**
     * The grid coverage specified at construction time.
     */
    public final GridCoverage coverage;

    /**
     * The grid coverage processor to use for selecting bands.
     * It may be configured with a colorizer for determining the color models.
     */
    private final GridCoverageProcessor processor;

    /**
     * Creates a new coverage stored in memory.
     *
     * @param  parent     listeners of the parent resource, or {@code null} if none.
     * @param  coverage   stored coverage retained as-is (not copied). Cannot be null.
     * @param  processor  the grid coverage processor for selecting bands, or {@code null} for default.
     */
    public MemoryGridResource(final StoreListeners parent, final GridCoverage coverage, final GridCoverageProcessor processor) {
        super(parent, false);
        ArgumentChecks.ensureNonNull("coverage", coverage);
        this.coverage = coverage;
        this.processor = (processor != null) ? processor : new GridCoverageProcessor();
    }

    /**
     * Returns information about the <cite>domain</cite> of wrapped grid coverage.
     *
     * @return extent of grid coordinates together with their mapping to "real world" coordinates.
     */
    @Override
    public GridGeometry getGridGeometry() {
        return coverage.getGridGeometry();
    }

    /**
     * Returns information about the <cite>ranges</cite> of wrapped grid coverage.
     *
     * @return ranges of sample values together with their mapping to "real values".
     */
    @Override
    public List<SampleDimension> getSampleDimensions() {
        return coverage.getSampleDimensions();
    }

    /**
     * Returns a subset of the wrapped grid coverage. If a non-null grid geometry is specified, then
     * this method tries to return a grid coverage matching the given grid geometry on a best-effort basis.
     * In current implementation this is either a {@link org.apache.sis.coverage.grid.GridCoverage2D} or
     * the original grid coverage.
     *
     * @param  domain  desired grid extent and resolution, or {@code null} for the whole domain.
     * @param  ranges  0-based indices of sample dimensions to read, or {@code null} or an empty sequence for reading them all.
     * @return the grid coverage for the specified domain and ranges.
     */
    @Override
    public GridCoverage read(GridGeometry domain, final int... ranges) {
        /*
         * If the user requested a subset of the bands, select the bands on the grid coverage.
         * It is slightly more expensive than selecting the bands on the image produced at the
         * end of this method, except if the coverage is a `BandAggregateGridCoverage` because
         * the latter will do rendering only on the selected coverage components.
         */
        GridCoverage subset = coverage;
        if (ranges != null && ranges.length != 0) {
            subset = processor.selectSampleDimensions(subset, ranges);
        }
        /*
         * The given `domain` may use arbitrary `gridToCRS` and `CRS` properties.
         * For this simple implementation we need the same `gridToCRS` and `CRS`
         * than the wrapped coverage; only domain `extent` is allowed to differ.
         * Subsampling is ignored for now because it is an expensive operation.
         * Clipping and ranges selection are light and do not copy any data.
         *
         * TODO: a future implementation may apply subsampling efficiently,
         *       by adjusting the pixel stride in SampleModel.
         */
        GridExtent intersection = null;
        final GridGeometry source = subset.getGridGeometry();
        if (domain == null) {
            domain = source;
        } else {
            intersection = source.derive()
                    .rounding(GridRoundingMode.ENCLOSING)
                    .subgrid(domain).getIntersection();             // Take in account the change of CRS if needed.
            if (intersection.equals(source.getExtent())) {
                intersection = null;                                // Will request the whole image.
                domain = source;
            }
        }
        /*
         * Quick check before to invoke the potentially costly `coverage.render(…)` method.
         */
        if (intersection == null) {
            return subset;
        }
        /*
         * After `render(…)` execution, the (minX, minY) image coordinates are the differences between
         * the extent that we requested and the one that we got. If that differences is not zero, then
         * we need to translate the `GridExtent` in order to make it matches what we got. But before to
         * apply that translation, we adjust the grid size (because it may add another translation).
         */
        RenderedImage data = subset.render(intersection);
        if (intersection != null) {
            final int[]  sd      = intersection.getSubspaceDimensions(2);
            final int    dimX    = sd[0];
            final int    dimY    = sd[1];
            final long   ox      = intersection.getLow(dimX);
            final long   oy      = intersection.getLow(dimY);
            final long[] changes = new long[Math.max(dimX, dimY) + 1];
            for (int i = changes.length; --i >= 0;) {
                changes[i] = intersection.getSize(i);       // We need only the dimensions that may change.
            }
            changes[dimX] = data.getWidth();
            changes[dimY] = data.getHeight();
            intersection  = intersection.resize(changes);
            /*
             * Apply the translation after we resized the grid extent, because the resize operation
             * may have caused an additional translation. We cancel that translation with terms that
             * restore the (ox,oy) lower coordinates before to add the data minimum X,Y.
             */
            Arrays.fill(changes, 0);
            changes[dimX] = Math.addExact(ox - intersection.getLow(dimX), data.getMinX());
            changes[dimY] = Math.addExact(oy - intersection.getLow(dimY), data.getMinX());
            intersection  = intersection.translate(changes);
            /*
             * If the result is the same intersection than the source coverage,
             * we can return that coverage directly.
             */
            if (intersection.equals(source.getExtent())) {
                return subset;
            } else {
                domain = new GridGeometry(intersection, PixelInCell.CELL_CORNER,
                        source.getGridToCRS(PixelInCell.CELL_CORNER),
                        source.getCoordinateReferenceSystem());
            }
        }
        return new GridCoverageBuilder()
                .setValues(data)
                .setDomain(domain)
                .setRanges(subset.getSampleDimensions())
                .build();
    }
}
