/*
 * Copyright (C) 2016 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.content.res;

import android.annotation.ColorInt;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.pm.ActivityInfo.Config;
import android.content.res.Resources.Theme;
import android.graphics.LinearGradient;
import android.graphics.RadialGradient;
import android.graphics.Shader;
import android.graphics.SweepGradient;
import android.graphics.drawable.GradientDrawable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;

import com.android.internal.R;
import com.android.internal.util.GrowingArrayUtils;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;

/**
 * Lets you define a gradient color, which is used inside
 * {@link android.graphics.drawable.VectorDrawable}.
 *
 * {@link android.content.res.GradientColor}s are created from XML resource files defined in the
 * "color" subdirectory directory of an application's resource directory.  The XML file contains
 * a single "gradient" element with a number of attributes and elements inside.  For example:
 * <pre>
 * &lt;gradient xmlns:android="http://schemas.android.com/apk/res/android"&gt;
 *   &lt;android:startColor="?android:attr/colorPrimary"/&gt;
 *   &lt;android:endColor="?android:attr/colorControlActivated"/&gt;
 *   &lt;.../&gt;
 *   &lt;android:type="linear"/&gt;
 * &lt;/gradient&gt;
 * </pre>
 *
 * This can describe either a {@link android.graphics.LinearGradient},
 * {@link android.graphics.RadialGradient}, or {@link android.graphics.SweepGradient}.
 *
 * Note that different attributes are relevant for different types of gradient.
 * For example, android:gradientRadius is only applied to RadialGradient.
 * android:centerX and android:centerY are only applied to SweepGradient or RadialGradient.
 * android:startX, android:startY, android:endX and android:endY are only applied to LinearGradient.
 *
 * Also note if any color "item" element is defined, then startColor, centerColor and endColor will
 * be ignored.
 * @hide
 */
public class GradientColor extends ComplexColor {
    private static final String TAG = "GradientColor";

    private static final boolean DBG_GRADIENT = false;

    @IntDef(prefix = { "TILE_MODE_" }, value = {
            TILE_MODE_CLAMP,
            TILE_MODE_REPEAT,
            TILE_MODE_MIRROR
    })
    @Retention(RetentionPolicy.SOURCE)
    private @interface GradientTileMode {}

    private static final int TILE_MODE_CLAMP = 0;
    private static final int TILE_MODE_REPEAT = 1;
    private static final int TILE_MODE_MIRROR = 2;

    /** Lazily-created factory for this GradientColor. */
    private GradientColorFactory mFactory;

    private @Config int mChangingConfigurations;
    private int mDefaultColor;

    // After parsing all the attributes from XML, this shader is the ultimate result containing
    // all the XML information.
    private Shader mShader = null;

    // Below are the attributes at the root element <gradient>.
    // NOTE: they need to be copied in the copy constructor!
    private int mGradientType = GradientDrawable.LINEAR_GRADIENT;

    private float mCenterX = 0f;
    private float mCenterY = 0f;

    private float mStartX = 0f;
    private float mStartY = 0f;
    private float mEndX = 0f;
    private float mEndY = 0f;

    private int mStartColor = 0;
    private int mCenterColor = 0;
    private int mEndColor = 0;
    private boolean mHasCenterColor = false;

    private int mTileMode = 0; // Clamp mode.

    private float mGradientRadius = 0f;

    // Below are the attributes for the <item> element.
    private int[] mItemColors;
    private float[] mItemOffsets;

    // Theme attributes for the root and item elements.
    private int[] mThemeAttrs;
    private int[][] mItemsThemeAttrs;

    private GradientColor() {
    }

    private GradientColor(GradientColor copy) {
        if (copy != null) {
            mChangingConfigurations = copy.mChangingConfigurations;
            mDefaultColor = copy.mDefaultColor;
            mShader = copy.mShader;
            mGradientType = copy.mGradientType;
            mCenterX = copy.mCenterX;
            mCenterY = copy.mCenterY;
            mStartX = copy.mStartX;
            mStartY = copy.mStartY;
            mEndX = copy.mEndX;
            mEndY = copy.mEndY;
            mStartColor = copy.mStartColor;
            mCenterColor = copy.mCenterColor;
            mEndColor = copy.mEndColor;
            mHasCenterColor = copy.mHasCenterColor;
            mGradientRadius = copy.mGradientRadius;
            mTileMode = copy.mTileMode;

            if (copy.mItemColors != null) {
                mItemColors = copy.mItemColors.clone();
            }
            if (copy.mItemOffsets != null) {
                mItemOffsets = copy.mItemOffsets.clone();
            }

            if (copy.mThemeAttrs != null) {
                mThemeAttrs = copy.mThemeAttrs.clone();
            }
            if (copy.mItemsThemeAttrs != null) {
                mItemsThemeAttrs = copy.mItemsThemeAttrs.clone();
            }
        }
    }

    // Set the default to clamp mode.
    private static Shader.TileMode parseTileMode(@GradientTileMode int tileMode) {
        switch (tileMode) {
            case TILE_MODE_CLAMP:
                return Shader.TileMode.CLAMP;
            case TILE_MODE_REPEAT:
                return Shader.TileMode.REPEAT;
            case TILE_MODE_MIRROR:
                return Shader.TileMode.MIRROR;
            default:
                return Shader.TileMode.CLAMP;
        }
    }

    /**
     * Update the root level's attributes, either for inflate or applyTheme.
     */
    private void updateRootElementState(TypedArray a) {
        // Extract the theme attributes, if any.
        mThemeAttrs = a.extractThemeAttrs();

        mStartX = a.getFloat(
                R.styleable.GradientColor_startX, mStartX);
        mStartY = a.getFloat(
                R.styleable.GradientColor_startY, mStartY);
        mEndX = a.getFloat(
                R.styleable.GradientColor_endX, mEndX);
        mEndY = a.getFloat(
                R.styleable.GradientColor_endY, mEndY);

        mCenterX = a.getFloat(
                R.styleable.GradientColor_centerX, mCenterX);
        mCenterY = a.getFloat(
                R.styleable.GradientColor_centerY, mCenterY);

        mGradientType = a.getInt(
                R.styleable.GradientColor_type, mGradientType);

        mStartColor = a.getColor(
                R.styleable.GradientColor_startColor, mStartColor);
        mHasCenterColor |= a.hasValue(
                R.styleable.GradientColor_centerColor);
        mCenterColor = a.getColor(
                R.styleable.GradientColor_centerColor, mCenterColor);
        mEndColor = a.getColor(
                R.styleable.GradientColor_endColor, mEndColor);

        mTileMode = a.getInt(
                R.styleable.GradientColor_tileMode, mTileMode);

        if (DBG_GRADIENT) {
            Log.v(TAG, "hasCenterColor is " + mHasCenterColor);
            if (mHasCenterColor) {
                Log.v(TAG, "centerColor:" + mCenterColor);
            }
            Log.v(TAG, "startColor: " + mStartColor);
            Log.v(TAG, "endColor: " + mEndColor);
            Log.v(TAG, "tileMode: " + mTileMode);
        }

        mGradientRadius = a.getFloat(R.styleable.GradientColor_gradientRadius,
                mGradientRadius);
    }

    /**
     * Check if the XML content is valid.
     *
     * @throws XmlPullParserException if errors were found.
     */
    private void validateXmlContent() throws XmlPullParserException {
        if (mGradientRadius <= 0
                && mGradientType == GradientDrawable.RADIAL_GRADIENT) {
            throw new XmlPullParserException(
                    "<gradient> tag requires 'gradientRadius' "
                            + "attribute with radial type");
        }
    }

    /**
     * The shader information will be applied to the native VectorDrawable's path.
     * @hide
     */
    public Shader getShader() {
        return mShader;
    }

    /**
     * A public method to create GradientColor from a XML resource.
     */
    public static GradientColor createFromXml(Resources r, XmlResourceParser parser, Theme theme)
            throws XmlPullParserException, IOException {
        final AttributeSet attrs = Xml.asAttributeSet(parser);

        int type;
        while ((type = parser.next()) != XmlPullParser.START_TAG
                && type != XmlPullParser.END_DOCUMENT) {
            // Seek parser to start tag.
        }

        if (type != XmlPullParser.START_TAG) {
            throw new XmlPullParserException("No start tag found");
        }

        return createFromXmlInner(r, parser, attrs, theme);
    }

    /**
     * Create from inside an XML document. Called on a parser positioned at a
     * tag in an XML document, tries to create a GradientColor from that tag.
     *
     * @return A new GradientColor for the current tag.
     * @throws XmlPullParserException if the current tag is not &lt;gradient>
     */
    @NonNull
    static GradientColor createFromXmlInner(@NonNull Resources r,
            @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)
            throws XmlPullParserException, IOException {
        final String name = parser.getName();
        if (!name.equals("gradient")) {
            throw new XmlPullParserException(
                    parser.getPositionDescription() + ": invalid gradient color tag " + name);
        }

        final GradientColor gradientColor = new GradientColor();
        gradientColor.inflate(r, parser, attrs, theme);
        return gradientColor;
    }

    /**
     * Fill in this object based on the contents of an XML "gradient" element.
     */
    private void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
            @NonNull AttributeSet attrs, @Nullable Theme theme)
            throws XmlPullParserException, IOException {
        final TypedArray a = Resources.obtainAttributes(r, theme, attrs, R.styleable.GradientColor);
        updateRootElementState(a);
        mChangingConfigurations |= a.getChangingConfigurations();
        a.recycle();

        // Check correctness and throw exception if errors found.
        validateXmlContent();

        inflateChildElements(r, parser, attrs, theme);

        onColorsChange();
    }

    /**
     * Inflates child elements "item"s for each color stop.
     *
     * Note that at root level, we need to save ThemeAttrs for theme applied later.
     * Here similarly, at each child item, we need to save the theme's attributes, and apply theme
     * later as applyItemsAttrsTheme().
     */
    private void inflateChildElements(@NonNull Resources r, @NonNull XmlPullParser parser,
            @NonNull AttributeSet attrs, @NonNull Theme theme)
            throws XmlPullParserException, IOException {
        final int innerDepth = parser.getDepth() + 1;
        int type;
        int depth;

        // Pre-allocate the array with some size, for better performance.
        float[] offsetList = new float[20];
        int[] colorList = new int[offsetList.length];
        int[][] themeAttrsList = new int[offsetList.length][];

        int listSize = 0;
        boolean hasUnresolvedAttrs = false;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && ((depth = parser.getDepth()) >= innerDepth
                || type != XmlPullParser.END_TAG)) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            if (depth > innerDepth || !parser.getName().equals("item")) {
                continue;
            }

            final TypedArray a = Resources.obtainAttributes(r, theme, attrs,
                    R.styleable.GradientColorItem);
            boolean hasColor = a.hasValue(R.styleable.GradientColorItem_color);
            boolean hasOffset = a.hasValue(R.styleable.GradientColorItem_offset);
            if (!hasColor || !hasOffset) {
                throw new XmlPullParserException(
                        parser.getPositionDescription()
                                + ": <item> tag requires a 'color' attribute and a 'offset' "
                                + "attribute!");
            }

            final int[] themeAttrs = a.extractThemeAttrs();
            int color = a.getColor(R.styleable.GradientColorItem_color, 0);
            float offset = a.getFloat(R.styleable.GradientColorItem_offset, 0);

            if (DBG_GRADIENT) {
                Log.v(TAG, "new item color " + color + " " + Integer.toHexString(color));
                Log.v(TAG, "offset" + offset);
            }
            mChangingConfigurations |= a.getChangingConfigurations();
            a.recycle();

            if (themeAttrs != null) {
                hasUnresolvedAttrs = true;
            }

            colorList = GrowingArrayUtils.append(colorList, listSize, color);
            offsetList = GrowingArrayUtils.append(offsetList, listSize, offset);
            themeAttrsList = GrowingArrayUtils.append(themeAttrsList, listSize, themeAttrs);
            listSize++;
        }
        if (listSize > 0) {
            if (hasUnresolvedAttrs) {
                mItemsThemeAttrs = new int[listSize][];
                System.arraycopy(themeAttrsList, 0, mItemsThemeAttrs, 0, listSize);
            } else {
                mItemsThemeAttrs = null;
            }

            mItemColors = new int[listSize];
            mItemOffsets = new float[listSize];
            System.arraycopy(colorList, 0, mItemColors, 0, listSize);
            System.arraycopy(offsetList, 0, mItemOffsets, 0, listSize);
        }
    }

    /**
     * Apply theme to all the items.
     */
    private void applyItemsAttrsTheme(Theme t) {
        if (mItemsThemeAttrs == null) {
            return;
        }

        boolean hasUnresolvedAttrs = false;

        final int[][] themeAttrsList = mItemsThemeAttrs;
        final int N = themeAttrsList.length;
        for (int i = 0; i < N; i++) {
            if (themeAttrsList[i] != null) {
                final TypedArray a = t.resolveAttributes(themeAttrsList[i],
                        R.styleable.GradientColorItem);

                // Extract the theme attributes, if any, before attempting to
                // read from the typed array. This prevents a crash if we have
                // unresolved attrs.
                themeAttrsList[i] = a.extractThemeAttrs(themeAttrsList[i]);
                if (themeAttrsList[i] != null) {
                    hasUnresolvedAttrs = true;
                }

                mItemColors[i] = a.getColor(R.styleable.GradientColorItem_color, mItemColors[i]);
                mItemOffsets[i] = a.getFloat(R.styleable.GradientColorItem_offset, mItemOffsets[i]);
                if (DBG_GRADIENT) {
                    Log.v(TAG, "applyItemsAttrsTheme Colors[i] " + i + " " +
                            Integer.toHexString(mItemColors[i]));
                    Log.v(TAG, "Offsets[i] " + i + " " + mItemOffsets[i]);
                }

                // Account for any configuration changes.
                mChangingConfigurations |= a.getChangingConfigurations();

                a.recycle();
            }
        }

        if (!hasUnresolvedAttrs) {
            mItemsThemeAttrs = null;
        }
    }

    private void onColorsChange() {
        int[] tempColors = null;
        float[] tempOffsets = null;

        if (mItemColors != null) {
            int length = mItemColors.length;
            tempColors = new int[length];
            tempOffsets = new float[length];

            for (int i = 0; i < length; i++) {
                tempColors[i] = mItemColors[i];
                tempOffsets[i] = mItemOffsets[i];
            }
        } else {
            if (mHasCenterColor) {
                tempColors = new int[3];
                tempColors[0] = mStartColor;
                tempColors[1] = mCenterColor;
                tempColors[2] = mEndColor;

                tempOffsets = new float[3];
                tempOffsets[0] = 0.0f;
                // Since 0.5f is default value, try to take the one that isn't 0.5f
                tempOffsets[1] = 0.5f;
                tempOffsets[2] = 1f;
            } else {
                tempColors = new int[2];
                tempColors[0] = mStartColor;
                tempColors[1] = mEndColor;
            }
        }
        if (tempColors.length < 2) {
            Log.w(TAG, "<gradient> tag requires 2 color values specified!" + tempColors.length
                    + " " + Arrays.toString(tempColors));
        }

        if (mGradientType == GradientDrawable.LINEAR_GRADIENT) {
            mShader = new LinearGradient(mStartX, mStartY, mEndX, mEndY, tempColors, tempOffsets,
                    parseTileMode(mTileMode));
        } else {
            if (mGradientType == GradientDrawable.RADIAL_GRADIENT) {
                mShader = new RadialGradient(mCenterX, mCenterY, mGradientRadius, tempColors,
                        tempOffsets, parseTileMode(mTileMode));
            } else {
                mShader = new SweepGradient(mCenterX, mCenterY, tempColors, tempOffsets);
            }
        }
        mDefaultColor = tempColors[0];
    }

    /**
     * For Gradient color, the default color is not very useful, since the gradient will override
     * the color information anyway.
     */
    @Override
    @ColorInt
    public int getDefaultColor() {
        return mDefaultColor;
    }

    /**
     * Similar to ColorStateList, setup constant state and its factory.
     * @hide only for resource preloading
     */
    @Override
    public ConstantState<ComplexColor> getConstantState() {
        if (mFactory == null) {
            mFactory = new GradientColorFactory(this);
        }
        return mFactory;
    }

    private static class GradientColorFactory extends ConstantState<ComplexColor> {
        private final GradientColor mSrc;

        public GradientColorFactory(GradientColor src) {
            mSrc = src;
        }

        @Override
        public @Config int getChangingConfigurations() {
            return mSrc.mChangingConfigurations;
        }

        @Override
        public GradientColor newInstance() {
            return mSrc;
        }

        @Override
        public GradientColor newInstance(Resources res, Theme theme) {
            return mSrc.obtainForTheme(theme);
        }
    }

    /**
     * Returns an appropriately themed gradient color.
     *
     * @param t the theme to apply
     * @return a copy of the gradient color the theme applied, or the
     * gradient itself if there were no unresolved theme
     * attributes
     * @hide only for resource preloading
     */
    @Override
    public GradientColor obtainForTheme(Theme t) {
        if (t == null || !canApplyTheme()) {
            return this;
        }

        final GradientColor clone = new GradientColor(this);
        clone.applyTheme(t);
        return clone;
    }

    /**
     * Returns a mask of the configuration parameters for which this gradient
     * may change, requiring that it be re-created.
     *
     * @return a mask of the changing configuration parameters, as defined by
     *         {@link android.content.pm.ActivityInfo}
     *
     * @see android.content.pm.ActivityInfo
     */
    public int getChangingConfigurations() {
        return super.getChangingConfigurations() | mChangingConfigurations;
    }

    private void applyTheme(Theme t) {
        if (mThemeAttrs != null) {
            applyRootAttrsTheme(t);
        }
        if (mItemsThemeAttrs != null) {
            applyItemsAttrsTheme(t);
        }
        onColorsChange();
    }

    private void applyRootAttrsTheme(Theme t) {
        final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.GradientColor);
        // mThemeAttrs will be set to null if if there are no theme attributes in the
        // typed array.
        mThemeAttrs = a.extractThemeAttrs(mThemeAttrs);
        // merging the attributes update inside the updateRootElementState().
        updateRootElementState(a);

        // Account for any configuration changes.
        mChangingConfigurations |= a.getChangingConfigurations();
        a.recycle();
    }


    /**
     * Returns whether a theme can be applied to this gradient color, which
     * usually indicates that the gradient color has unresolved theme
     * attributes.
     *
     * @return whether a theme can be applied to this gradient color.
     * @hide only for resource preloading
     */
    @Override
    public boolean canApplyTheme() {
        return mThemeAttrs != null || mItemsThemeAttrs != null;
    }

}
