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

import java.util.Optional;
import java.awt.Point;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.awt.image.WritableRenderedImage;
import java.awt.image.RasterFormatException;
import java.nio.Buffer;
import java.nio.IntBuffer;
import java.nio.FloatBuffer;
import java.nio.DoubleBuffer;
import org.apache.sis.internal.feature.Resources;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.ArgumentChecks;


/**
 * Default iterator used when no specialized implementation is available.
 * This iterator uses the {@link Raster} API for traversing the pixels in each tile.
 * Calls to {@link #next()} move the current position by increasing the following values, in order:
 *
 * <ol>
 *   <li>Column index in a single tile (from left to right)</li>
 *   <li>Row index in a single tile (from top to bottom).</li>
 *   <li>Then, {@code tileX} index from left to right.</li>
 *   <li>Then, {@code tileY} index from top to bottom.</li>
 * </ol>
 *
 * @author  Rémi Maréchal (Geomatys)
 * @author  Martin Desruisseaux (Geomatys)
 * @version 1.0
 * @since   1.0
 * @module
 *
 * @todo Change iteration order on tiles for using Hilbert iterator.
 */
class DefaultIterator extends WritablePixelIterator {
    /**
     * Tile coordinate of {@link #currentRaster}.
     * The {@code tileY >= tileUpperY} condition is used for detecting when we reached iteration end.
     */
    int tileX, tileY;

    /**
     * Current column index in current raster.
     * The {@code x >= lowerX} condition is used for detecting if iteration started.
     */
    int x;

    /**
     * Current row index in current raster.
     */
    int y;

    /**
     * Bounds of the region traversed by the iterator in current raster.
     * When iteration reaches the upper coordinates, the iterator needs to move to next tile.
     */
    int currentLowerX, currentUpperX, currentUpperY;

    /**
     * Creates an iterator for the given region in the given raster.
     *
     * @param  input    the raster which contains the sample values to read.
     * @param  output   the raster where to write the sample values, or {@code null} for read-only iterator.
     * @param  subArea  the raster region where to perform the iteration, or {@code null} for iterating over all the raster domain.
     * @param  window   size of the window to use in {@link #createWindow(TransferType)} method, or {@code null} if none.
     */
    DefaultIterator(final Raster input, final WritableRaster output, final Rectangle subArea, final Dimension window) {
        super(input, output, subArea, window);
        currentLowerX = lowerX;
        currentUpperX = upperX;
        currentUpperY = upperY;
        x = Math.decrementExact(lowerX);        // Set the position before first pixel.
        y = lowerY;
    }

    /**
     * Creates an iterator for the given region in the given image.
     *
     * @param  input    the image which contains the sample values to read.
     * @param  output   the image where to write the sample values, or {@code null} for read-only iterator.
     * @param  subArea  the image region where to perform the iteration, or {@code null} for iterating over all the image domain.
     * @param  window   size of the window to use in {@link #createWindow(TransferType)} method, or {@code null} if none.
     */
    DefaultIterator(final RenderedImage input, final WritableRenderedImage output, final Rectangle subArea, final Dimension window) {
        super(input, output, subArea, window);
        tileX = Math.decrementExact(tileLowerX);
        tileY = tileLowerY;
        currentLowerX = lowerX;
        currentUpperX = lowerX;                 // Really 'lower', so the position is the tile before the first tile.
        currentUpperY = lowerY;
        x = Math.decrementExact(lowerX);        // Set the position before first pixel.
        y = lowerY;
        /*
         * We need to ensure that `tileUpperY+1 > tileUpperY` will alway be true because `tileY` may be equal
         * to `tileUpperY` when the `if (++tileY >= tileUpperY)` statement is excuted in the `next()` method.
         * This is because `tileY` is used as a sentinel value for detecting when we reached iteration end.
         */
        if (tileUpperY == Integer.MAX_VALUE) {
            throw new ArithmeticException(Errors.format(Errors.Keys.IntegerOverflow_1, Integer.SIZE));
        }
    }

    /**
     * Restores this iterator to the same state it was after construction.
     */
    @Override
    public void rewind() {
        close();                        // Release current writable raster, if any.
        if (image == null) {
            tileX = 0;
            tileY = 0;
            currentUpperX = upperX;
            currentUpperY = upperY;
        } else {
            tileX = tileLowerX - 1;     // Note: no need for decrementExact(…) because already checked by constructor.
            tileY = tileLowerY;
            currentUpperX = lowerX;     // Really 'lower', so the position is the tile before the first tile.
            currentUpperY = lowerY;
        }
        currentLowerX = lowerX;
        x = lowerX - 1;                 // Set the position before first pixel.
        y = lowerY;
    }

    /**
     * Returns the order in which pixels are traversed.
     */
    @Override
    public Optional<SequenceType> getIterationOrder() {
        if (image == null || (tileUpperX - tileLowerX) <=1 && (tileUpperY - tileLowerY) <= 1) {
            return Optional.of(SequenceType.LINEAR);
        } else {
            return Optional.empty();                // Undefined order.
        }
    }

    /**
     * Returns the column (x) and row (y) indices of the current pixel.
     * This implementation {@link #x} and {@link #tileY} for determining if the iteration is valid.
     *
     * @return column and row indices of current iterator position.
     * @throws IllegalStateException if this method is invoked before the first call to {@link #next()}
     *         or {@link #moveTo(int, int)}, or after {@code next()} returned {@code false}.
     */
    @Override
    public Point getPosition() {
        final short message;
        if (x < lowerX) {
            message = Resources.Keys.IterationNotStarted;
        } else if (tileY >= tileUpperY) {
            message = Resources.Keys.IterationIsFinished;
        } else {
            return new Point(x,y);
        }
        throw new IllegalStateException(Resources.format(message));
    }

    /**
     * Moves the pixel iterator to the given column (x) and row (y) indices.
     *
     * @param  px  the column index of the pixel to make current.
     * @param  py  the row index of the pixel to make current.
     * @throws IndexOutOfBoundsException if the given indices are outside the iteration domain.
     */
    @Override
    public void moveTo(final int px, final int py) {
        if (px < lowerX || px >= upperX ||  py < lowerY || py >= upperY) {
            throw new IndexOutOfBoundsException(Resources.format(Resources.Keys.OutOfIteratorDomain_2, px, py));
        }
        if (image != null) {
            final int tx = Math.floorDiv(px - tileGridXOffset, tileWidth);
            final int ty = Math.floorDiv(py - tileGridYOffset, tileHeight);
            if (tx != tileX || ty != tileY) {
                close();                                            // Release current writable raster, if any.
                tileX = tx;
                tileY = ty;
                if (fetchTile() > py || currentLowerX > px) {       // `fetchTile()` must be before `currentLowerX`.
                    throw new RasterFormatException(Resources.format(Resources.Keys.IncompatibleTile_2, tileX, tileY));
                }
            }
        }
        x = px;
        y = py;
    }

    /**
     * Moves the iterator to the next pixel. This default implementation moves to the next tile only after
     * all pixels in current tiles have been traversed, but subclasses may apply a different strategy.
     *
     * @return {@code true} if the current pixel is valid, or {@code false} if there is no more pixels.
     * @throws IllegalStateException if this iterator already reached end of iteration in a previous call
     *         to {@code next()}, and {@link #rewind()} or {@link #moveTo(int,int)} have not been invoked.
     */
    @Override
    public boolean next() {
        if (++x >= currentUpperX) {
            if (++y >= currentUpperY) {                     // Strict equality (==) would work, but use >= as a safety.
                close();                                    // Release current writable raster, if any.
                if (++tileX >= tileUpperX) {                // Strict equality (==) would work, but use >= as a safety.
                    if (++tileY >= tileUpperY) {
                        endOfIteration();
                        return false;
                    }
                    tileX = tileLowerX;
                }
                y = fetchTile();
            }
            x = currentLowerX;
        }
        return true;
    }

    /**
     * Fetches from the image a tile for the current {@link #tileX} and {@link #tileY} coordinates.
     * All fields prefixed by {@code current} are updated by this method. The caller is responsible
     * for updating the {@link #x} and {@link #y} fields.
     *
     * <p>Note that there is no {@code currentLowerY} field in this {@code DefaultIterator} class.
     * Instead, the value that would be have been set to that field is returned by this method.</p>
     *
     * @return the {@link #y} value of the first row of new tile.
     */
    final int fetchTile() {
        currentRaster = null;
        if (destination != null) {
            destRaster = destination.getWritableTile(tileX, tileY);
            if (destination == image) {
                currentRaster = destRaster;
            }
        }
        if (currentRaster == null) {
            currentRaster = image.getTile(tileX, tileY);
        }
        final int minX = currentRaster.getMinX();
        final int minY = currentRaster.getMinY();
        currentLowerX  = Math.max(lowerX, minX);
        currentUpperX  = Math.min(upperX, minX + tileWidth);
        currentUpperY  = Math.min(upperY, minY + tileHeight);
        if (currentRaster.getNumBands() != numBands) {
            throw new RasterFormatException(Resources.format(Resources.Keys.IncompatibleTile_2, tileX, tileY));
        }
        return Math.max(lowerY, minY);
    }

    /**
     * Invoked when a call to {@link #next()} moved to the end of iteration. This method sets fields to values
     * that will allow {@link #moveTo(int,int)} and {@link #next()} to detect that we already finished iteration.
     */
    final void endOfIteration() {
        /*
         * The `tileY` value is used for checking if next() is invoked again, in order to avoid a
         * common misuse pattern. In principle `tileY` needs to be compared only to `tileUpperY`,
         * but we also compare to `tileLowerY + 1` for handling the empty iterator case.
         */
        final boolean error = tileY > Math.max(tileUpperY, tileLowerY + 1);
        /*
         * Paranoiac safety: keep the x, y and tileX variables before their limits
         * in order to avoid overflow in the `if (++foo >= limit)` statements.
         */
        x =  currentUpperX - 1;
        y =  currentUpperY - 1;
        tileX = tileUpperX - 1;
        tileY = tileUpperY;             // Sentinel value for detecting following error condition.
        if (error) {
            throw new IllegalStateException(Resources.format(Resources.Keys.IterationIsFinished));
        }
    }

    /**
     * Returns the sample value in the specified band of current pixel, rounded toward zero.
     * This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
     */
    @Override
    public int getSample(final int band) {
        return currentRaster.getSample(x, y, band);
    }

    /**
     * Returns the sample value in the specified band of current pixel as a single-precision floating point number.
     * This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
     */
    @Override
    public float getSampleFloat(final int band) {
        return currentRaster.getSampleFloat(x, y, band);
    }

    /**
     * Returns the sample value in the specified band of current pixel, without precision lost.
     * This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
     */
    @Override
    public double getSampleDouble(final int band) {
        return currentRaster.getSampleDouble(x, y, band);
    }

    /**
     * Writes a sample value in the specified band of current pixel.
     * This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
     */
    @Override
    public void setSample(final int band, final int value) {
        destRaster.setSample(x, y, band, value);
    }

    /**
     * Writes a sample value in the specified band of current pixel.
     * This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
     */
    @Override
    public void setSample(final int band, final float value) {
        destRaster.setSample(x, y, band, value);
    }

    /**
     * Writes a sample value in the specified band of current pixel.
     * This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
     */
    @Override
    public void setSample(final int band, final double value) {
        destRaster.setSample(x, y, band, value);
    }

    /**
     * Returns the sample values of current pixel for all bands.
     * This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
     */
    @Override
    public int[] getPixel​(int[] dest) {
        return currentRaster.getPixel(x, y, dest);
    }

    /**
     * Returns the sample values of current pixel for all bands.
     * This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
     */
    @Override
    public float[] getPixel​(float[] dest) {
        return currentRaster.getPixel(x, y, dest);
    }

    /**
     * Returns the sample values of current pixel for all bands.
     * This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
     */
    @Override
    public double[] getPixel​(double[] dest) {
        return currentRaster.getPixel(x, y, dest);
    }

    /**
     * Sets the sample values of current pixel for all bands.
     * This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
     */
    @Override
    public void setPixel​(int[] dest) {
        destRaster.setPixel(x, y, dest);
    }

    /**
     * Sets the sample values of current pixel for all bands.
     * This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
     */
    @Override
    public void setPixel​(float[] dest) {
        destRaster.setPixel(x, y, dest);
    }

    /**
     * Sets the sample values of current pixel for all bands.
     * This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
     */
    @Override
    public void setPixel​(double[] dest) {
        destRaster.setPixel(x, y, dest);
    }

    /**
     * Returns a moving window over the sample values in a rectangular region starting at iterator position.
     */
    @Override
    @SuppressWarnings("unchecked")
    public <T extends Buffer> Window<T> createWindow(final TransferType<T> type) {
        ArgumentChecks.ensureNonNull("type", type);
        final int length = numBands * windowWidth * windowHeight;
        final int transferLength = length - numBands * Math.min(windowWidth, windowHeight);
        // 'transfer' will always have at least one row or one column less than 'data'.
        switch (type.dataBufferType) {
            case DataBuffer.TYPE_INT:    return (Window<T>) new IntWindow   (new int   [length], new int   [transferLength]);
            case DataBuffer.TYPE_FLOAT:  return (Window<T>) new FloatWindow (new float [length], new float [transferLength]);
            case DataBuffer.TYPE_DOUBLE: return (Window<T>) new DoubleWindow(new double[length], new double[transferLength]);
            default: throw new AssertionError(type);  // Should never happen unless we updated TransferType and forgot to update this method.
        }
    }

    /**
     * The base class of all {@link Window} implementations provided by {@link DefaultIterator}.
     * This iterator defines a callback method required by {@link DefaultIterator#update(WindowBase, Object)}.
     *
     * @todo keep trace of last location and use {@code System#arraycopy(…)} for moving the values that we already have.
     */
    private abstract static class WindowBase<T extends Buffer> extends Window<T> {
        /**
         * Creates a new window which will store the sample values in the given buffer.
         */
        WindowBase(final T buffer) {
            super(buffer);
        }

        /**
         * Returns an array containing all samples for a rectangle of pixels in the given raster, one sample
         * per array element. Subclasses shall delegate to one of the {@code Raster#getPixels(…)} methods
         * depending on the buffer data type.
         *
         * @param  raster     the raster from which to get the pixel values.
         * @param  subX       the X coordinate of the upper-left pixel location.
         * @param  subY       the Y coordinate of the upper-left pixel location.
         * @param  subWidth   width of the pixel rectangle.
         * @param  subHeight  height of the pixel rectangle.
         * @param  direct     {@code true} for storing directly in the final array,
         *                     or {@code false} for using the transfer array.
         * @return the array in which sample values have been stored.
         */
        abstract Object getPixels(Raster raster, int subX, int subY, int subWidth, int subHeight, boolean direct);
    }

    /**
     * {@link Window} implementation backed by an array of {@code int[]}.
     */
    private final class IntWindow extends WindowBase<IntBuffer> {
        /**
         * Sample values in the window ({@code data}) and a temporary array ({@code transfer}).
         * Those arrays are overwritten when {@link #update()} is invoked.
         */
        private final int[] data, transfer;

        /**
         * Creates a new window which will store the sample values in the given {@code data} array.
         */
        IntWindow(final int[] data, final int[] transfer) {
            super(IntBuffer.wrap(data).asReadOnlyBuffer());
            this.data = data;
            this.transfer = transfer;
        }

        /**
         * Performs the transfer between the underlying raster and this window.
         */
        @Override
        Object getPixels(Raster raster, int subX, int subY, int subWidth, int subHeight, boolean direct) {
            return raster.getPixels(subX, subY, subWidth, subHeight, direct ? data : transfer);
        }

        /**
         * Updates this window with the sample values in the region starting at current iterator position.
         * This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
         */
        @Override
        public void update() {
            values.clear();
            DefaultIterator.this.update(this, data);
        }
    }

    /**
     * {@link Window} implementation backed by an array of {@code float[]}.
     */
    private final class FloatWindow extends WindowBase<FloatBuffer> {
        /**
         * Sample values in the window ({@code data}) and a temporary array ({@code transfer}).
         * Those arrays are overwritten when {@link #update()} is invoked.
         */
        private final float[] data, transfer;

        /**
         * Creates a new window which will store the sample values in the given {@code data} array.
         */
        FloatWindow(final float[] data, final float[] transfer) {
            super(FloatBuffer.wrap(data).asReadOnlyBuffer());
            this.data = data;
            this.transfer = transfer;
        }

        /**
         * Performs the transfer between the underlying raster and this window.
         */
        @Override
        Object getPixels(Raster raster, int subX, int subY, int subWidth, int subHeight, boolean direct) {
            return raster.getPixels(subX, subY, subWidth, subHeight, direct ? data : transfer);
        }

        /**
         * Updates this window with the sample values in the region starting at current iterator position.
         * This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
         */
        @Override
        public void update() {
            values.clear();
            DefaultIterator.this.update(this, data);
        }
    }

    /**
     * {@link Window} implementation backed by an array of {@code double[]}.
     */
    private final class DoubleWindow extends WindowBase<DoubleBuffer> {
        /**
         * Sample values in the window ({@code data}) and a temporary array ({@code transfer}).
         * Those arrays are overwritten when {@link #update()} is invoked.
         */
        private final double[] data, transfer;

        /**
         * Creates a new window which will store the sample values in the given {@code data} array.
         */
        DoubleWindow(final double[] data, final double[] transfer) {
            super(DoubleBuffer.wrap(data).asReadOnlyBuffer());
            this.data = data;
            this.transfer = transfer;
        }

        /**
         * Performs the transfer between the underlying raster and this window.
         */
        @Override
        Object getPixels(Raster raster, int subX, int subY, int subWidth, int subHeight, boolean direct) {
            return raster.getPixels(subX, subY, subWidth, subHeight, direct ? data : transfer);
        }

        /**
         * Updates this window with the sample values in the region starting at current iterator position.
         * This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
         */
        @Override
        public void update() {
            values.clear();
            DefaultIterator.this.update(this, data);
        }
    }

    /**
     * Updates the content of given window with the sample values in the region starting at current iterator position.
     *
     * @param window  the window to update.
     * @param data    the array of primitive type where sample values are stored.
     */
    @SuppressWarnings("SuspiciousSystemArraycopy")
    final void update(final WindowBase<?> window, final Object data) {
        Raster  raster    = currentRaster;
        int     subEndX   = (raster.getMinX() - x) + raster.getWidth();
        int     subEndY   = (raster.getMinY() - y) + raster.getHeight();
        int     subWidth  = Math.min(windowWidth,  subEndX);
        int     subHeight = Math.min(windowHeight, subEndY);
        boolean fullWidth = (subWidth == windowWidth);
        if (fullWidth && subHeight == windowHeight) {
            /*
             * Optimization for the case where the full window is inside current raster.
             * This is the vast majority of cases, so we perform this check soon before
             * to compute more internal variables.
             */
            final Object transfer = window.getPixels(raster, x, y, subWidth, subHeight, true);
            if (transfer != data) {     // Paranoiac check (arrays should always be same).
                System.arraycopy(transfer, 0, data, 0, numBands * subWidth * subHeight);
            }
            return;
        }
        /*
         * At this point, we determined that the window is overlapping two or more tiles.
         * We will need more variables for iterating over the tiles around 'currentRaster'.
         */
        int destOffset   = 0;                       // Index in 'window' array where to copy the sample values.
        int subX         = 0;                       // Upper-left corner of a sub-window inside the window.
        int subY         = 0;
        int tileSubX     = tileX;                   // The tile where is located the (subX, subY) coordinate.
        int tileSubY     = tileY;
        final int stride = windowWidth * numBands;  // Number of samples between two rows in the 'windows' array.
        final int rewind = subEndX;
        for (;;) {
            if (subWidth > 0 && subHeight > 0) {
                final Object transfer = window.getPixels(raster, x + subX, y + subY, subWidth, subHeight, false);
                if (fullWidth) {
                    final int fullLength = stride * subHeight;
                    System.arraycopy(transfer, 0, data, destOffset, fullLength);
                    destOffset += fullLength;
                } else {
                    final int  rowLength = numBands  * subWidth;
                    final int fullLength = rowLength * subHeight;
                    for (int srcOffset=0; srcOffset < fullLength; srcOffset += rowLength) {
                        System.arraycopy(transfer, srcOffset, data, destOffset, rowLength);
                        destOffset += stride;
                    }
                }
            }
            /*
             * At this point, we copied all sample values that we could obtain from the current tile.
             * Move to the next tile on current row, or if we reached the end of row move to the next row.
             */
            if (subEndX < windowWidth) {
                subX     = subEndX;
                subEndX += tileWidth;                       // Next tile on the same row.
                tileSubX++;
            } else {
                if (subEndY >= windowHeight) {
                    return;                                 // Completed last row of tiles.
                }
                subY     = subEndY;
                subEndY += tileHeight;                      // Tile on the next row.
                tileSubY++;
                tileSubX = tileX;
                subEndX  = rewind;
                subX     = 0;                               // Move x position back to the window left border.
            }
            raster     = image.getTile(tileSubX, tileSubY);
            destOffset = (subY * windowWidth + subX) * numBands;
            subWidth   = Math.min(windowWidth,  subEndX) - subX;
            subHeight  = Math.min(windowHeight, subEndY) - subY;
            fullWidth  = (subWidth == windowWidth);
        }
    }

    /**
     * Releases the tiles acquired by this iterator, if any.
     * This method does nothing if the iterator is read-only.
     */
    @Override
    public final void close() {
        if (destination != null && destRaster != null) {
            destRaster = null;
            destination.releaseWritableTile(tileX, tileY);
        }
    }
}
