/*
 * Copyright (C) 2008 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 com.android.layoutlib.bridge;

import com.android.internal.util.XmlUtils;
import com.android.layoutlib.api.IResourceValue;
import com.android.layoutlib.api.IStyleResourceValue;

import org.kxml2.io.KXmlParser;
import org.xmlpull.v1.XmlPullParser;

import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.ViewGroup.LayoutParams;

import java.io.File;
import java.io.FileReader;
import java.util.Map;

/**
 * TODO: describe.
 */
public final class BridgeTypedArray extends TypedArray {

    @SuppressWarnings("hiding")
    private BridgeResources mResources;
    private BridgeContext mContext;
    @SuppressWarnings("hiding")
    private IResourceValue[] mData;
    private String[] mNames;
    private final boolean mPlatformFile;

    public BridgeTypedArray(BridgeResources resources, BridgeContext context, int len,
            boolean platformFile) {
        super(null, null, null, 0);
        mResources = resources;
        mContext = context;
        mPlatformFile = platformFile;
        mData = new IResourceValue[len];
        mNames = new String[len];
    }

    /** A bridge-specific method that sets a value in the type array */
    public void bridgeSetValue(int index, String name, IResourceValue value) {
        mData[index] = value;
        mNames[index] = name;
    }

    /**
     * Seals the array after all calls to {@link #bridgeSetValue(int, String, IResourceValue)} have
     * been done.
     * <p/>This allows to compute the list of non default values, permitting
     * {@link #getIndexCount()} to return the proper value.
     */
    public void sealArray() {
        // fills TypedArray.mIndices which is used to implement getIndexCount/getIndexAt
        // first count the array size
        int count = 0;
        for (IResourceValue data : mData) {
            if (data != null) {
                count++;
            }
        }

        // allocate the table with an extra to store the size
        mIndices = new int[count+1];
        mIndices[0] = count;

        // fill the array with the indices.
        int index = 1;
        for (int i = 0 ; i < mData.length ; i++) {
            if (mData[i] != null) {
                mIndices[index++] = i;
            }
        }
    }

    /**
     * Return the number of values in this array.
     */
    @Override
    public int length() {
        return mData.length;
    }

    /**
     * Return the Resources object this array was loaded from.
     */
    @Override
    public Resources getResources() {
        return mResources;
    }

    /**
     * Retrieve the styled string value for the attribute at <var>index</var>.
     *
     * @param index Index of attribute to retrieve.
     *
     * @return CharSequence holding string data.  May be styled.  Returns
     *         null if the attribute is not defined.
     */
    @Override
    public CharSequence getText(int index) {
        if (mData[index] != null) {
            // FIXME: handle styled strings!
            return mData[index].getValue();
        }

        return null;
    }

    /**
     * Retrieve the string value for the attribute at <var>index</var>.
     *
     * @param index Index of attribute to retrieve.
     *
     * @return String holding string data.  Any styling information is
     * removed.  Returns null if the attribute is not defined.
     */
    @Override
    public String getString(int index) {
        if (mData[index] != null) {
            return mData[index].getValue();
        }

        return null;
    }

    /**
     * Retrieve the boolean value for the attribute at <var>index</var>.
     *
     * @param index Index of attribute to retrieve.
     * @param defValue Value to return if the attribute is not defined.
     *
     * @return Attribute boolean value, or defValue if not defined.
     */
    @Override
    public boolean getBoolean(int index, boolean defValue) {
        if (mData[index] == null) {
            return defValue;
        }

        String s = mData[index].getValue();
        if (s != null) {
            return XmlUtils.convertValueToBoolean(s, defValue);
        }

        return defValue;
    }

    /**
     * Retrieve the integer value for the attribute at <var>index</var>.
     *
     * @param index Index of attribute to retrieve.
     * @param defValue Value to return if the attribute is not defined.
     *
     * @return Attribute int value, or defValue if not defined.
     */
    @Override
    public int getInt(int index, int defValue) {
        if (mData[index] == null) {
            return defValue;
        }

        String s = mData[index].getValue();

        try {
            return (s == null) ? defValue : XmlUtils.convertValueToInt(s, defValue);
        } catch (NumberFormatException e) {
            // pass
        }

        // Field is not null and is not an integer.
        // Check for possible constants and try to find them.
        // Get the map of attribute-constant -> IntegerValue
        Map<String, Integer> map = Bridge.getEnumValues(mNames[index]);

        if (map != null) {
            // accumulator to store the value of the 1+ constants.
            int result = 0;

            // split the value in case this is a mix of several flags.
            String[] keywords = s.split("\\|");
            for (String keyword : keywords) {
                Integer i = map.get(keyword.trim());
                if (i != null) {
                    result |= i.intValue();
                } else {
                    mContext.getLogger().warning(String.format(
                            "Unknown constant \"%s\" in attribute \"%2$s\"",
                            keyword, mNames[index]));
                }
            }
            return result;
        }

        return defValue;
    }

    /**
     * Retrieve the float value for the attribute at <var>index</var>.
     *
     * @param index Index of attribute to retrieve.
     *
     * @return Attribute float value, or defValue if not defined..
     */
    @Override
    public float getFloat(int index, float defValue) {
        if (mData[index] == null) {
            return defValue;
        }

        String s = mData[index].getValue();

        if (s != null) {
            try {
                return Float.parseFloat(s);
            } catch (NumberFormatException e) {
                mContext.getLogger().warning(String.format(
                        "Unable to convert \"%s\" into a float in attribute \"%2$s\"",
                        s, mNames[index]));

                // we'll return the default value below.
            }
        }
        return defValue;
    }

    /**
     * Retrieve the color value for the attribute at <var>index</var>.  If
     * the attribute references a color resource holding a complex
     * {@link android.content.res.ColorStateList}, then the default color from
     * the set is returned.
     *
     * @param index Index of attribute to retrieve.
     * @param defValue Value to return if the attribute is not defined or
     *                 not a resource.
     *
     * @return Attribute color value, or defValue if not defined.
     */
    @Override
    public int getColor(int index, int defValue) {
        if (mData[index] == null) {
            return defValue;
        }

        String s = mData[index].getValue();
        try {
            return ResourceHelper.getColor(s);
        } catch (NumberFormatException e) {
            mContext.getLogger().warning(String.format(
                    "Unable to convert \"%s\" into a color in attribute \"%2$s\"",
                    s, mNames[index]));

            // we'll return the default value below.
        }

        return defValue;
    }

    /**
     * Retrieve the ColorStateList for the attribute at <var>index</var>.
     * The value may be either a single solid color or a reference to
     * a color or complex {@link android.content.res.ColorStateList} description.
     *
     * @param index Index of attribute to retrieve.
     *
     * @return ColorStateList for the attribute, or null if not defined.
     */
    @Override
    public ColorStateList getColorStateList(int index) {
        if (mData[index] == null) {
            return null;
        }

        String value = mData[index].getValue();

        if (value == null) {
            return null;
        }

        try {
            int color = ResourceHelper.getColor(value);
            return ColorStateList.valueOf(color);
        } catch (NumberFormatException e) {
            // if it's not a color value, we'll attempt to read the xml based color below.
        }

        // let the framework inflate the ColorStateList from the XML file.
        try {
            File f = new File(value);
            if (f.isFile()) {
                KXmlParser parser = new KXmlParser();
                parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
                parser.setInput(new FileReader(f));

                ColorStateList colorStateList = ColorStateList.createFromXml(
                        mContext.getResources(),
                        // FIXME: we need to know if this resource is platform or not
                        new BridgeXmlBlockParser(parser, mContext, false));
                return colorStateList;
            }
        } catch (Exception e) {
            // this is an error and not warning since the file existence is checked before
            // attempting to parse it.
            mContext.getLogger().error(e);

            // return null below.
        }

        // looks like were unable to resolve the color value.
        mContext.getLogger().warning(String.format(
                "Unable to resolve color value \"%1$s\" in attribute \"%2$s\"",
                value, mNames[index]));

        return null;
    }

    /**
     * Retrieve the integer value for the attribute at <var>index</var>.
     *
     * @param index Index of attribute to retrieve.
     * @param defValue Value to return if the attribute is not defined or
     *                 not a resource.
     *
     * @return Attribute integer value, or defValue if not defined.
     */
    @Override
    public int getInteger(int index, int defValue) {
        if (mData[index] == null) {
            return defValue;
        }

        String s = mData[index].getValue();

        if (s != null) {
            try {
                return Integer.parseInt(s);
            } catch (NumberFormatException e) {
                mContext.getLogger().warning(String.format(
                        "Unable to convert \"%s\" into a integer in attribute \"%2$s\"",
                        s, mNames[index]));

                // The default value is returned below.
            }
        }

        return defValue;
    }

    /**
     * Retrieve a dimensional unit attribute at <var>index</var>.  Unit
     * conversions are based on the current {@link DisplayMetrics}
     * associated with the resources this {@link TypedArray} object
     * came from.
     *
     * @param index Index of attribute to retrieve.
     * @param defValue Value to return if the attribute is not defined or
     *                 not a resource.
     *
     * @return Attribute dimension value multiplied by the appropriate
     * metric, or defValue if not defined.
     *
     * @see #getDimensionPixelOffset
     * @see #getDimensionPixelSize
     */
    @Override
    public float getDimension(int index, float defValue) {
        if (mData[index] == null) {
            return defValue;
        }

        String s = mData[index].getValue();

        if (s == null) {
            return defValue;
        } else if (s.equals(BridgeConstants.MATCH_PARENT) ||
                s.equals(BridgeConstants.FILL_PARENT)) {
            return LayoutParams.MATCH_PARENT;
        } else if (s.equals(BridgeConstants.WRAP_CONTENT)) {
            return LayoutParams.WRAP_CONTENT;
        }

        if (ResourceHelper.stringToFloat(s, mValue)) {
            return mValue.getDimension(mResources.mMetrics);
        }

        // looks like we were unable to resolve the dimension value
        mContext.getLogger().warning(String.format(
                "Unable to resolve dimension value \"%1$s\" in attribute \"%2$s\"",
                s, mNames[index]));

        return defValue;
    }

    /**
     * Retrieve a dimensional unit attribute at <var>index</var> for use
     * as an offset in raw pixels.  This is the same as
     * {@link #getDimension}, except the returned value is converted to
     * integer pixels for you.  An offset conversion involves simply
     * truncating the base value to an integer.
     *
     * @param index Index of attribute to retrieve.
     * @param defValue Value to return if the attribute is not defined or
     *                 not a resource.
     *
     * @return Attribute dimension value multiplied by the appropriate
     * metric and truncated to integer pixels, or defValue if not defined.
     *
     * @see #getDimension
     * @see #getDimensionPixelSize
     */
    @Override
    public int getDimensionPixelOffset(int index, int defValue) {
        return (int) getDimension(index, defValue);
    }

    /**
     * Retrieve a dimensional unit attribute at <var>index</var> for use
     * as a size in raw pixels.  This is the same as
     * {@link #getDimension}, except the returned value is converted to
     * integer pixels for use as a size.  A size conversion involves
     * rounding the base value, and ensuring that a non-zero base value
     * is at least one pixel in size.
     *
     * @param index Index of attribute to retrieve.
     * @param defValue Value to return if the attribute is not defined or
     *                 not a resource.
     *
     * @return Attribute dimension value multiplied by the appropriate
     * metric and truncated to integer pixels, or defValue if not defined.
     *
     * @see #getDimension
     * @see #getDimensionPixelOffset
     */
    @Override
    public int getDimensionPixelSize(int index, int defValue) {
        if (mData[index] == null) {
            return defValue;
        }

        String s = mData[index].getValue();

        if (s == null) {
            return defValue;
        } else if (s.equals(BridgeConstants.MATCH_PARENT) ||
                s.equals(BridgeConstants.FILL_PARENT)) {
            return LayoutParams.MATCH_PARENT;
        } else if (s.equals(BridgeConstants.WRAP_CONTENT)) {
            return LayoutParams.WRAP_CONTENT;
        }

        // FIXME huh?

        float f = getDimension(index, defValue);
        final int res = (int)(f+0.5f);
        if (res != 0) return res;
        if (f == 0) return 0;
        if (f > 0) return 1;

        throw new UnsupportedOperationException("Can't convert to dimension: " +
                Integer.toString(index));
    }

    /**
     * Special version of {@link #getDimensionPixelSize} for retrieving
     * {@link android.view.ViewGroup}'s layout_width and layout_height
     * attributes.  This is only here for performance reasons; applications
     * should use {@link #getDimensionPixelSize}.
     *
     * @param index Index of the attribute to retrieve.
     * @param name Textual name of attribute for error reporting.
     *
     * @return Attribute dimension value multiplied by the appropriate
     * metric and truncated to integer pixels.
     */
    @Override
    public int getLayoutDimension(int index, String name) {
        return getDimensionPixelSize(index, 0);
    }

    @Override
    public int getLayoutDimension(int index, int defValue) {
        return getDimensionPixelSize(index, defValue);
    }

    /**
     * Retrieve a fractional unit attribute at <var>index</var>.
     *
     * @param index Index of attribute to retrieve.
     * @param base The base value of this fraction.  In other words, a
     *             standard fraction is multiplied by this value.
     * @param pbase The parent base value of this fraction.  In other
     *             words, a parent fraction (nn%p) is multiplied by this
     *             value.
     * @param defValue Value to return if the attribute is not defined or
     *                 not a resource.
     *
     * @return Attribute fractional value multiplied by the appropriate
     * base value, or defValue if not defined.
     */
    @Override
    public float getFraction(int index, int base, int pbase, float defValue) {
        if (mData[index] == null) {
            return defValue;
        }

        String value = mData[index].getValue();
        if (value == null) {
            return defValue;
        }

        if (ResourceHelper.stringToFloat(value, mValue)) {
            return mValue.getFraction(base, pbase);
        }

        // looks like we were unable to resolve the fraction value
        mContext.getLogger().warning(String.format(
                "Unable to resolve fraction value \"%1$s\" in attribute \"%2$s\"",
                value, mNames[index]));

        return defValue;
    }

    /**
     * Retrieve the resource identifier for the attribute at
     * <var>index</var>.  Note that attribute resource as resolved when
     * the overall {@link TypedArray} object is retrieved.  As a
     * result, this function will return the resource identifier of the
     * final resource value that was found, <em>not</em> necessarily the
     * original resource that was specified by the attribute.
     *
     * @param index Index of attribute to retrieve.
     * @param defValue Value to return if the attribute is not defined or
     *                 not a resource.
     *
     * @return Attribute resource identifier, or defValue if not defined.
     */
    @Override
    public int getResourceId(int index, int defValue) {
        // get the IResource for this index
        IResourceValue resValue = mData[index];

        // no data, return the default value.
        if (resValue == null) {
            return defValue;
        }

        // check if this is a style resource
        if (resValue instanceof IStyleResourceValue) {
            // get the id that will represent this style.
            return mContext.getDynamicIdByStyle((IStyleResourceValue)resValue);
        }

        // if the attribute was a reference to an id, and not a declaration of an id (@+id), then
        // the xml attribute value was "resolved" which leads us to a IResourceValue with
        // getType() returning "id" and getName() returning the id name
        // (and getValue() returning null!). We need to handle this!
        if (resValue.getType() != null && resValue.getType().equals(BridgeConstants.RES_ID)) {
            // if this is a framework id
            if (mPlatformFile || resValue.isFramework()) {
                // look for idName in the android R classes
                return mContext.getFrameworkIdValue(resValue.getName(), defValue);
            }

            // look for idName in the project R class.
            return mContext.getProjectIdValue(resValue.getName(), defValue);
        }

        // else, try to get the value, and resolve it somehow.
        String value = resValue.getValue();
        if (value == null) {
            return defValue;
        }

        // if the value is just an integer, return it.
        try {
            int i = Integer.parseInt(value);
            if (Integer.toString(i).equals(value)) {
                return i;
            }
        } catch (NumberFormatException e) {
            // pass
        }

        // Handle the @id/<name>, @+id/<name> and @android:id/<name>
        // We need to return the exact value that was compiled (from the various R classes),
        // as these values can be reused internally with calls to findViewById().
        // There's a trick with platform layouts that not use "android:" but their IDs are in
        // fact in the android.R and com.android.internal.R classes.
        // The field mPlatformFile will indicate that all IDs are to be looked up in the android R
        // classes exclusively.

        // if this is a reference to an id, find it.
        if (value.startsWith("@id/") || value.startsWith("@+") ||
                value.startsWith("@android:id/")) {

            int pos = value.indexOf('/');
            String idName = value.substring(pos + 1);

            // if this is a framework id
            if (mPlatformFile || value.startsWith("@android") || value.startsWith("@+android")) {
                // look for idName in the android R classes
                return mContext.getFrameworkIdValue(idName, defValue);
            }

            // look for idName in the project R class.
            return mContext.getProjectIdValue(idName, defValue);
        }

        // not a direct id valid reference? resolve it
        Integer idValue = null;

        if (resValue.isFramework()) {
            idValue = Bridge.getResourceValue(resValue.getType(), resValue.getName());
        } else {
            idValue = mContext.getProjectCallback().getResourceValue(
                    resValue.getType(), resValue.getName());
        }

        if (idValue != null) {
            return idValue.intValue();
        }

        mContext.getLogger().warning(String.format(
                "Unable to resolve id \"%1$s\" for attribute \"%2$s\"", value, mNames[index]));
        return defValue;
    }

    /**
     * Retrieve the Drawable for the attribute at <var>index</var>.  This
     * gets the resource ID of the selected attribute, and uses
     * {@link Resources#getDrawable Resources.getDrawable} of the owning
     * Resources object to retrieve its Drawable.
     *
     * @param index Index of attribute to retrieve.
     *
     * @return Drawable for the attribute, or null if not defined.
     */
    @Override
    public Drawable getDrawable(int index) {
        if (mData[index] == null) {
            return null;
        }

        IResourceValue value = mData[index];
        String stringValue = value.getValue();
        if (stringValue == null || BridgeConstants.REFERENCE_NULL.equals(stringValue)) {
            return null;
        }

        Drawable d = ResourceHelper.getDrawable(value, mContext, mData[index].isFramework());

        if (d != null) {
            return d;
        }

        // looks like we were unable to resolve the drawable
        mContext.getLogger().warning(String.format(
                "Unable to resolve drawable \"%1$s\" in attribute \"%2$s\"", stringValue,
                mNames[index]));

        return null;
    }


    /**
     * Retrieve the CharSequence[] for the attribute at <var>index</var>.
     * This gets the resource ID of the selected attribute, and uses
     * {@link Resources#getTextArray Resources.getTextArray} of the owning
     * Resources object to retrieve its String[].
     *
     * @param index Index of attribute to retrieve.
     *
     * @return CharSequence[] for the attribute, or null if not defined.
     */
    @Override
    public CharSequence[] getTextArray(int index) {
        if (mData[index] == null) {
            return null;
        }

        String value = mData[index].getValue();
        if (value != null) {
            return new CharSequence[] { value };
        }

        mContext.getLogger().warning(String.format(
                String.format("Unknown value for getTextArray(%d) => %s", //DEBUG
                index, mData[index].getName())));

        return null;
    }

    /**
     * Retrieve the raw TypedValue for the attribute at <var>index</var>.
     *
     * @param index Index of attribute to retrieve.
     * @param outValue TypedValue object in which to place the attribute's
     *                 data.
     *
     * @return Returns true if the value was retrieved, else false.
     */
    @Override
    public boolean getValue(int index, TypedValue outValue) {
        if (mData[index] == null) {
            return false;
        }

        String s = mData[index].getValue();

        return ResourceHelper.stringToFloat(s, outValue);
    }

    /**
     * Determines whether there is an attribute at <var>index</var>.
     *
     * @param index Index of attribute to retrieve.
     *
     * @return True if the attribute has a value, false otherwise.
     */
    @Override
    public boolean hasValue(int index) {
        return mData[index] != null;
    }

    /**
     * Retrieve the raw TypedValue for the attribute at <var>index</var>
     * and return a temporary object holding its data.  This object is only
     * valid until the next call on to {@link TypedArray}.
     *
     * @param index Index of attribute to retrieve.
     *
     * @return Returns a TypedValue object if the attribute is defined,
     *         containing its data; otherwise returns null.  (You will not
     *         receive a TypedValue whose type is TYPE_NULL.)
     */
    @Override
    public TypedValue peekValue(int index) {
        if (getValue(index, mValue)) {
            return mValue;
        }

        return null;
    }

    /**
     * Returns a message about the parser state suitable for printing error messages.
     */
    @Override
    public String getPositionDescription() {
        return "<internal -- stub if needed>";
    }

    /**
     * Give back a previously retrieved StyledAttributes, for later re-use.
     */
    @Override
    public void recycle() {
        // pass
    }

    @Override
    public boolean getValueAt(int index, TypedValue outValue) {
        // pass
        return false;
    }

    @Override
    public String toString() {
        return mData.toString();
    }
 }
