/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed 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 android.graphics;

import static java.lang.annotation.RetentionPolicy.SOURCE;

import android.annotation.IntDef;
import android.annotation.NonNull;

import dalvik.annotation.optimization.CriticalNative;
import dalvik.system.VMRuntime;

import libcore.util.NativeAllocationRegistry;

import java.lang.annotation.Retention;
import java.util.ConcurrentModificationException;
import java.util.Iterator;

/**
 * <code>PathIterator</code> can be used to query a given {@link Path} object, to discover its
 * operations and point values.
 */
@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class PathIterator implements Iterator<PathIterator.Segment> {

    private final float[] mPointsArray;
    private final long mPointsAddress;
    private int mCachedVerb = -1;
    private boolean mDone = false;
    private final long mNativeIterator;
    private final Path mPath;
    private final int mPathGenerationId;
    private static final int POINT_ARRAY_SIZE = 8;
    private static final boolean IS_DALVIK = "dalvik".equalsIgnoreCase(
            System.getProperty("java.vm.name"));

    private static class NoImagePreloadHolder {
        private static final NativeAllocationRegistry sRegistry =
                NativeAllocationRegistry.createMalloced(
                        PathIterator.class.getClassLoader(), nGetFinalizer());
    }

    /**
     * The <code>Verb</code> indicates the operation for a given segment of a path. These
     * operations correspond exactly to the primitive operations on {@link Path}, such as
     * {@link Path#moveTo(float, float)} and {@link Path#lineTo(float, float)}, except for
     * {@link #VERB_DONE}, which means that there are no more operations in this path.
     */
    @Retention(SOURCE)
    @IntDef({VERB_MOVE, VERB_LINE, VERB_QUAD, VERB_CONIC, VERB_CUBIC, VERB_CLOSE, VERB_DONE})
    @interface Verb {}
    // these must match the values in SkPath.h
    public static final int VERB_MOVE = 0;
    public static final int VERB_LINE = 1;
    public static final int VERB_QUAD = 2;
    public static final int VERB_CONIC = 3;
    public static final int VERB_CUBIC = 4;
    public static final int VERB_CLOSE = 5;
    public static final int VERB_DONE = 6;


    static {
        // Keep <cinit> exist in bytecode
    }

    /**
     * Returns a {@link PathIterator} object for this path, which can be used to query the
     * data (operations and points) in the path. Iterators can only be used on Path objects
     * that have not been modified since the iterator was created. Calling
     * {@link #next(float[], int)}, {@link #next()}, or {@link #hasNext()} on an
     * iterator for a modified path will result in a {@link ConcurrentModificationException}.
     *
     * @param path The {@link Path} for which this iterator can be queried.
     */
    PathIterator(@NonNull Path path) {
        mPath = path;
        mNativeIterator = nCreate(mPath.mNativePath);
        mPathGenerationId = mPath.getGenerationId();
        if (IS_DALVIK) {
            final VMRuntime runtime = VMRuntime.getRuntime();
            mPointsArray = (float[]) runtime.newNonMovableArray(float.class, POINT_ARRAY_SIZE);
            mPointsAddress = runtime.addressOf(mPointsArray);
        } else {
            mPointsArray = new float[POINT_ARRAY_SIZE];
            mPointsAddress = 0;
        }
        NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativeIterator);
    }

    /**
     * Returns the next verb in this iterator's {@link Path}, and fills entries in the
     * <code>points</code> array with the point data (if any) for that operation.
     * Each two floats represent the data for a single point of that operation.
     * The number of pairs of floats supplied in the resulting array depends on the verb:
     * <ul>
     * <li>{@link #VERB_MOVE}: 1 pair (indices 0 to 1)</li>
     * <li>{@link #VERB_LINE}: 2 pairs (indices 0 to 3)</li>
     * <li>{@link #VERB_QUAD}: 3 pairs (indices 0 to 5)</li>
     * <li>{@link #VERB_CONIC}: 3.5 pairs (indices 0 to 6), the seventh entry has the conic
     * weight</li>
     * <li>{@link #VERB_CUBIC}: 4 pairs (indices 0 to 7)</li>
     * <li>{@link #VERB_CLOSE}: 0 pairs</li>
     * <li>{@link #VERB_DONE}: 0 pairs</li>
     * </ul>
     * @param points The point data for this operation, must have at least
     *               8 items available to hold up to 4 pairs of point values
     * @param offset An offset into the <code>points</code> array where entries should be placed.
     * @return the operation for the next element in the iteration
     * @throws ArrayIndexOutOfBoundsException if the points array is too small
     * @throws ConcurrentModificationException if the underlying path was modified
     * since this iterator was created.
     */
    @NonNull
    public @Verb int next(@NonNull float[] points, int offset) {
        if (points.length < offset + POINT_ARRAY_SIZE) {
            throw new ArrayIndexOutOfBoundsException("points array must be able to "
                    + "hold at least 8 entries");
        }
        @Verb int returnVerb = getReturnVerb(mCachedVerb);
        mCachedVerb = -1;
        System.arraycopy(mPointsArray, 0, points, offset, POINT_ARRAY_SIZE);
        return returnVerb;
    }

    /**
     * Returns true if the there are more elements in this iterator to be returned.
     * A return value of <code>false</code> means there are no more elements, and an
     * ensuing call to {@link #next()} or {@link #next(float[], int)} )} will return
     * {@link #VERB_DONE}.
     *
     * @return true if there are more elements to be iterated through, false otherwise
     * @throws ConcurrentModificationException if the underlying path was modified
     * since this iterator was created.
     */
    @Override
    public boolean hasNext() {
        if (mCachedVerb == -1) {
            mCachedVerb = nextInternal();
        }
        return mCachedVerb != VERB_DONE;
    }

    /**
     * Returns the next verb in the iteration, or {@link #VERB_DONE} if there are no more
     * elements.
     *
     * @return the next verb in the iteration, or {@link #VERB_DONE} if there are no more
     * elements
     * @throws ConcurrentModificationException if the underlying path was modified
     * since this iterator was created.
     */
    @NonNull
    public @Verb int peek() {
        if (mPathGenerationId != mPath.getGenerationId()) {
            throw new ConcurrentModificationException(
                    "Iterator cannot be used on modified Path");
        }
        if (mDone) {
            return VERB_DONE;
        }
        return nPeek(mNativeIterator);
    }

    /**
     * This is where the work is done for {@link #next()}. Using this internal method
     * is helfpul for managing the cached segment used by {@link #hasNext()}.
     *
     * @return the segment to be returned by {@link #next()}
     * @throws ConcurrentModificationException if the underlying path was modified
     * since this iterator was created.
     */
    @NonNull
    private @Verb int nextInternal() {
        if (mDone) {
            return VERB_DONE;
        }
        if (mPathGenerationId != mPath.getGenerationId()) {
            throw new ConcurrentModificationException(
                    "Iterator cannot be used on modified Path");
        }
        @Verb int verb = IS_DALVIK
            ? nNext(mNativeIterator, mPointsAddress) : nNextHost(mNativeIterator, mPointsArray);
        if (verb == VERB_DONE) {
            mDone = true;
        }
        return verb;
    }

    /**
     * Returns the next {@link Segment} element in this iterator.
     *
     * There are two versions of <code>next()</code>. This version is slightly more
     * expensive at runtime, since it allocates a new {@link Segment} object with
     * every call. The other version, {@link #next(float[], int)} requires no such allocation, but
     * requires a little more manual effort to use.
     *
     * @return the next segment in this iterator
     * @throws ConcurrentModificationException if the underlying path was modified
     * since this iterator was created.
     */
    @NonNull
    @Override
    public Segment next() {
        @Verb int returnVerb = getReturnVerb(mCachedVerb);
        mCachedVerb = -1;
        float conicWeight = 0f;
        if (returnVerb == VERB_CONIC) {
            conicWeight = mPointsArray[6];
        }
        float[] returnPoints = new float[8];
        System.arraycopy(mPointsArray, 0, returnPoints, 0, POINT_ARRAY_SIZE);
        return new Segment(returnVerb, returnPoints, conicWeight);
    }

    private @Verb int getReturnVerb(int cachedVerb) {
        switch (cachedVerb) {
            case VERB_MOVE: return VERB_MOVE;
            case VERB_LINE: return VERB_LINE;
            case VERB_QUAD: return VERB_QUAD;
            case VERB_CONIC: return VERB_CONIC;
            case VERB_CUBIC: return VERB_CUBIC;
            case VERB_CLOSE: return VERB_CLOSE;
            case VERB_DONE: return VERB_DONE;
        }
        return nextInternal();
    }

    /**
     * This class holds the data for a given segment in a path, as returned by
     * {@link #next()}.
     */
    public static class Segment {
        private final @Verb int mVerb;
        private final float[] mPoints;
        private final float mConicWeight;

        /**
         * The operation for this segment.
         *
         * @return the verb which indicates the operation happening in this segment
         */
        @NonNull
        public @Verb int getVerb() {
            return mVerb;
        }

        /**
         * The point data for this segment.
         *
         * Each two floats represent the data for a single point of that operation.
         * The number of pairs of floats supplied in the resulting array depends on the verb:
         * <ul>
         * <li>{@link #VERB_MOVE}: 1 pair (indices 0 to 1)</li>
         * <li>{@link #VERB_LINE}: 2 pairs (indices 0 to 3)</li>
         * <li>{@link #VERB_QUAD}: 3 pairs (indices 0 to 5)</li>
         * <li>{@link #VERB_CONIC}: 4 pairs (indices 0 to 7), the last pair contains the
         * conic weight twice</li>
         * <li>{@link #VERB_CUBIC}: 4 pairs (indices 0 to 7)</li>
         * <li>{@link #VERB_CLOSE}: 0 pairs</li>
         * <li>{@link #VERB_DONE}: 0 pairs</li>
         * </ul>
         * @return the point data for this segment
         */
        @NonNull
        public float[] getPoints() {
            return mPoints;
        }

        /**
         * The weight for the conic operation in this segment. If the verb in this segment
         * is not equal to {@link #VERB_CONIC}, the weight value is undefined.
         *
         * @see Path#conicTo(float, float, float, float, float)
         * @return the weight for the conic operation in this segment, if any
         */
        public float getConicWeight() {
            return mConicWeight;
        }

        Segment(@NonNull @Verb int verb, @NonNull float[] points, float conicWeight) {
            mVerb = verb;
            mPoints = points;
            mConicWeight = conicWeight;
        }
    }

    // ------------------ Regular JNI ------------------------

    private static native long nCreate(long nativePath);
    private static native long nGetFinalizer();

    /* nNextHost should be used for host runtimes, e.g. LayoutLib */
    private static native int nNextHost(long nativeIterator, float[] points);

    // ------------------ Critical JNI ------------------------

    @CriticalNative
    private static native int nNext(long nativeIterator, long pointsAddress);

    @CriticalNative
    private static native int nPeek(long nativeIterator);
}
