/*
 * 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 com.android.ide.common.rendering.api.ArrayResourceValue;
import com.android.ide.common.rendering.api.AssetRepository;
import com.android.ide.common.rendering.api.DensityBasedResourceValue;
import com.android.ide.common.rendering.api.ILayoutLog;
import com.android.ide.common.rendering.api.LayoutlibCallback;
import com.android.ide.common.rendering.api.PluralsResourceValue;
import com.android.ide.common.rendering.api.RenderResources;
import com.android.ide.common.rendering.api.ResourceNamespace;
import com.android.ide.common.rendering.api.ResourceReference;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.rendering.api.ResourceValueImpl;
import com.android.ide.common.rendering.api.TextResourceValue;
import com.android.ide.common.resources.ValueXmlHelper;
import com.android.layoutlib.bridge.Bridge;
import com.android.layoutlib.bridge.BridgeConstants;
import com.android.layoutlib.bridge.android.BridgeContext;
import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
import com.android.layoutlib.bridge.android.UnresolvedResourceValue;
import com.android.layoutlib.bridge.impl.ParserFactory;
import com.android.layoutlib.bridge.impl.ResourceHelper;
import com.android.layoutlib.bridge.util.NinePatchInputStream;
import com.android.resources.ResourceType;
import com.android.resources.ResourceUrl;
import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
import com.android.tools.layoutlib.annotations.VisibleForTesting;

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

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.Resources.NotFoundException;
import android.content.res.Resources.Theme;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.DrawableInflater_Delegate;
import android.icu.text.PluralRules;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.LruCache;
import android.util.Pair;
import android.util.TypedValue;
import android.view.DisplayAdjustments;
import android.view.ViewGroup.LayoutParams;

import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.WeakHashMap;

import static android.content.res.AssetManager.ACCESS_STREAMING;
import static com.android.ide.common.rendering.api.AndroidConstants.ANDROID_PKG;
import static com.android.ide.common.rendering.api.AndroidConstants.APP_PREFIX;
import static com.android.ide.common.rendering.api.AndroidConstants.PREFIX_RESOURCE_REF;

public class Resources_Delegate {
    private static final WeakHashMap<Resources, LayoutlibCallback> sLayoutlibCallbacks =
            new WeakHashMap<>();
    private static final WeakHashMap<Resources, BridgeContext> sContexts = new WeakHashMap<>();

    // TODO: This cache is cleared every time a render session is disposed. Look into making this
    // more long lived.
    private static final LruCache<String, Drawable.ConstantState> sDrawableCache = new LruCache<>(50);

    public static Resources initSystem(@NonNull BridgeContext context,
            @NonNull AssetManager assets,
            @NonNull DisplayMetrics metrics,
            @NonNull Configuration config,
            @NonNull LayoutlibCallback layoutlibCallback) {
        assert Resources.mSystem == null  :
                "Resources_Delegate.initSystem called twice before disposeSystem was called";
        Resources resources = new Resources(Resources_Delegate.class.getClassLoader());
        resources.setImpl(new ResourcesImpl(assets, metrics, config, new DisplayAdjustments()));
        resources.getConfiguration().windowConfiguration.setMaxBounds(0, 0, metrics.widthPixels,
                metrics.heightPixels);
        sContexts.put(resources, Objects.requireNonNull(context));
        sLayoutlibCallbacks.put(resources, Objects.requireNonNull(layoutlibCallback));
        return Resources.mSystem = resources;
    }

    /** Returns the {@link BridgeContext} associated to the given {@link Resources} */
    @VisibleForTesting
    @NonNull
    public static BridgeContext getContext(@NonNull Resources resources) {
        assert sContexts.containsKey(resources) :
                "Resources_Delegate.getContext called before initSystem";
        return sContexts.get(resources);
    }

    /** Returns the {@link LayoutlibCallback} associated to the given {@link Resources} */
    @VisibleForTesting
    @NonNull
    public static LayoutlibCallback getLayoutlibCallback(@NonNull Resources resources) {
        assert sLayoutlibCallbacks.containsKey(resources) :
                "Resources_Delegate.getLayoutlibCallback called before initSystem";
        return sLayoutlibCallbacks.get(resources);
    }

    /**
     * Disposes the static {@link Resources#mSystem} to make sure we don't leave objects around that
     * would prevent us from unloading the library.
     */
    public static void disposeSystem() {
        sDrawableCache.evictAll();
        sContexts.clear();
        sLayoutlibCallbacks.clear();
        DrawableInflater_Delegate.clearConstructorCache();
        Resources.mSystem = null;
    }

    public static BridgeTypedArray newTypeArray(Resources resources, int numEntries) {
        return new BridgeTypedArray(resources, getContext(resources), numEntries);
    }

    private static ResourceReference getResourceInfo(Resources resources, int id) {
        // first get the String related to this id in the framework
        ResourceReference resourceInfo = Bridge.resolveResourceId(id);

        assert Resources.mSystem != null : "Resources_Delegate.initSystem wasn't called";
        // Set the layoutlib callback and context for resources
        if (resources != Resources.mSystem &&
                (!sContexts.containsKey(resources) || !sLayoutlibCallbacks.containsKey(resources))) {
            sLayoutlibCallbacks.put(resources, getLayoutlibCallback(Resources.mSystem));
            sContexts.put(resources, getContext(Resources.mSystem));
        }

        if (resourceInfo == null) {
            // Didn't find a match in the framework? Look in the project.
            resourceInfo = getLayoutlibCallback(resources).resolveResourceId(id);
        }

        return resourceInfo;
    }

    private static Pair<String, ResourceValue> getResourceValue(Resources resources, int id) {
        ResourceReference resourceInfo = getResourceInfo(resources, id);

        if (resourceInfo != null) {
            String attributeName = resourceInfo.getName();
            RenderResources renderResources = getContext(resources).getRenderResources();
            ResourceValue value = renderResources.getResolvedResource(resourceInfo);
            if (value == null) {
                // Unable to resolve the attribute, just leave the unresolved value.
                value = new ResourceValueImpl(resourceInfo.getNamespace(),
                        resourceInfo.getResourceType(), attributeName, attributeName);
            }
            return Pair.create(attributeName, value);
        }

        return null;
    }

    @LayoutlibDelegate
    static Drawable getDrawable(Resources resources, int id) {
        return getDrawable(resources, id, null);
    }

    @LayoutlibDelegate
    static Drawable getDrawable(Resources resources, int id, Theme theme) {
        Pair<String, ResourceValue> value = getResourceValue(resources, id);
        if (value != null) {
            String key = value.second.getValue();

            Drawable.ConstantState constantState = key != null ? sDrawableCache.get(key) : null;
            Drawable drawable;
            if (constantState != null) {
                drawable = constantState.newDrawable(resources, theme);
            } else {
                drawable =
                        ResourceHelper.getDrawable(value.second, getContext(resources), theme);

                if (drawable == null) {
                    throwException(resources, id);
                    return null;
                }

                if (key != null) {
                    Drawable.ConstantState state = drawable.getConstantState();
                    if (state != null) {
                        sDrawableCache.put(key, state);
                    }
                }
            }

            return drawable;
        }

        // id was not found or not resolved. Throw a NotFoundException.
        throwException(resources, id);

        // this is not used since the method above always throws
        return null;
    }

    @LayoutlibDelegate
    static int getColor(Resources resources, int id) {
        return getColor(resources, id, null);
    }

    @LayoutlibDelegate
    static int getColor(Resources resources, int id, Theme theme) throws NotFoundException {
        Pair<String, ResourceValue> value = getResourceValue(resources, id);

        if (value != null) {
            ResourceValue resourceValue = value.second;
            try {
                return ResourceHelper.getColor(resourceValue.getValue());
            } catch (NumberFormatException e) {
                ColorStateList stateList = ResourceHelper.getColorStateList(resourceValue,
                        getContext(resources), theme);
                if (stateList != null) {
                    return stateList.getDefaultColor();
                }
                Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_FORMAT, resourceValue.getName() +
                        " is neither a color value nor a color state list", null, null);
                return 0;
            }
        }

        throwException(resources, id);
        return 0;
    }

    @LayoutlibDelegate
    static ColorStateList getColorStateList(Resources resources, int id) throws NotFoundException {
        return getColorStateList(resources, id, null);
    }

    @LayoutlibDelegate
    static ColorStateList getColorStateList(Resources resources, int id, Theme theme)
            throws NotFoundException {
        Pair<String, ResourceValue> resValue = getResourceValue(resources, id);

        if (resValue != null) {
            ColorStateList stateList = ResourceHelper.getColorStateList(resValue.second,
                    getContext(resources), theme);
            if (stateList != null) {
                return stateList;
            }
        }

        // id was not found or not resolved. Throw a NotFoundException.
        throwException(resources, id);

        // this is not used since the method above always throws
        return null;
    }

    @LayoutlibDelegate
    static CharSequence getText(Resources resources, int id, CharSequence def) {
        CharSequence text = getTextInternal(resources, id);
        if (text != null) {
            return text;
        }

        return def;
    }

    @LayoutlibDelegate
    static CharSequence getText(Resources resources, int id) throws NotFoundException {
        CharSequence text = getTextInternal(resources, id);
        if (text != null) {
            return text;
        }

        // id was not found or not resolved. Throw a NotFoundException.
        throwException(resources, id);

        // this is not used since the method above always throws
        return null;
    }

    @Nullable
    private static CharSequence getTextInternal(Resources resources, int id) {
        Pair<String, ResourceValue> value = getResourceValue(resources, id);

        if (value != null) {
            ResourceValue resValue = value.second;
            assert resValue != null;
            return ResourceHelper.getText(resValue);
        }
        return null;
    }

    @LayoutlibDelegate
    static CharSequence[] getTextArray(Resources resources, int id) throws NotFoundException {
        ResourceValue resValue = getArrayResourceValue(resources, id);
        if (resValue == null) {
            // Error already logged by getArrayResourceValue.
            return new CharSequence[0];
        }
        if (resValue instanceof ArrayResourceValue arrayValue) {
            return resolveValues(resources, arrayValue);
        }
        RenderResources renderResources = getContext(resources).getRenderResources();
        return new CharSequence[] { renderResources.resolveResValue(resValue).getValue() };
    }

    @LayoutlibDelegate
    static String[] getStringArray(Resources resources, int id) throws NotFoundException {
        ResourceValue resValue = getArrayResourceValue(resources, id);
        if (resValue == null) {
            // Error already logged by getArrayResourceValue.
            return new String[0];
        }
        if (resValue instanceof ArrayResourceValue arv) {
            return resolveValues(resources, arv);
        }
        return new String[] { resolveReference(resources, resValue) };
    }

    /**
     * Resolves each element in resValue and returns an array of resolved values. The returned array
     * may contain nulls.
     */
    @NonNull
    static String[] resolveValues(@NonNull Resources resources,
            @NonNull ArrayResourceValue resValue) {
        String[] result = new String[resValue.getElementCount()];
        for (int i = 0; i < resValue.getElementCount(); i++) {
            String value = resValue.getElement(i);
            result[i] = resolveReference(resources, value,
                    resValue.getNamespace(), resValue.getNamespaceResolver());
        }
        return result;
    }

    @LayoutlibDelegate
    static int[] getIntArray(Resources resources, int id) throws NotFoundException {
        ResourceValue rv = getArrayResourceValue(resources, id);
        if (rv == null) {
            // Error already logged by getArrayResourceValue.
            return new int[0];
        }
        if (rv instanceof ArrayResourceValue resValue) {
            int n = resValue.getElementCount();
            int[] values = new int[n];
            for (int i = 0; i < n; i++) {
                String element = resolveReference(resources, resValue.getElement(i),
                        resValue.getNamespace(), resValue.getNamespaceResolver());
                if (element != null) {
                    try {
                        if (element.startsWith("#")) {
                            // This integer represents a color (starts with #).
                            values[i] = ResourceHelper.getColor(element);
                        } else {
                            values[i] = getInt(element);
                        }
                    } catch (NumberFormatException e) {
                        Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_FORMAT,
                                "Integer resource array contains non-integer value: \"" + element +
                                        "\"", null, null);
                    } catch (IllegalArgumentException e) {
                        Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_FORMAT,
                                "Integer resource array contains wrong color format: \"" + element +
                                        "\"", null, null);
                    }
                } else {
                    Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_FORMAT,
                            "Integer resource array contains non-integer value: \"" +
                                    resValue.getElement(i) + "\"", null, null);
                }
            }
            return values;
        }

        // This is an older IDE that can only give us the first element of the array.
        String firstValue = resolveReference(resources, rv);
        if (firstValue != null) {
            try {
                return new int[]{getInt(firstValue)};
            } catch (NumberFormatException e) {
                Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_FORMAT,
                        "Integer resource array contains non-integer value: \"" + firstValue + "\"",
                        null, null);
                return new int[1];
            }
        } else {
            Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_FORMAT,
                    "Integer resource array contains non-integer value: \"" +
                            rv.getValue() + "\"", null, null);
            return new int[1];
        }
    }

    /**
     * Try to find the ArrayResourceValue for the given id.
     * <p/>
     * If the ResourceValue found is not of type {@link ResourceType#ARRAY}, the method logs an
     * error and return null. However, if the ResourceValue found has type {@code
     * ResourceType.ARRAY}, but the value is not an instance of {@link ArrayResourceValue}, the
     * method returns the ResourceValue. This happens on older versions of the IDE, which did not
     * parse the array resources properly.
     * <p/>
     *
     * @throws NotFoundException if no resource if found
     */
    @Nullable
    private static ResourceValue getArrayResourceValue(Resources resources, int id)
            throws NotFoundException {
        Pair<String, ResourceValue> v = getResourceValue(resources, id);

        if (v != null) {
            ResourceValue resValue = v.second;

            assert resValue != null;
            final ResourceType type = resValue.getResourceType();
            if (type != ResourceType.ARRAY) {
                Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_RESOLVE,
                        String.format(
                                "Resource with id 0x%1$X is not an array resource, but %2$s",
                                id, type == null ? "null" : type.getDisplayName()),
                        null, null);
                return null;
            }
            if (!(resValue instanceof ArrayResourceValue)) {
                Bridge.getLog().warning(ILayoutLog.TAG_UNSUPPORTED,
                        "Obtaining resource arrays via getTextArray, getStringArray or getIntArray is not fully supported in this version of the IDE.",
                        null, null);
            }
            return resValue;
        }

        // id was not found or not resolved. Throw a NotFoundException.
        throwException(resources, id);

        // this is not used since the method above always throws
        return null;
    }

    @Nullable
    private static String resolveReference(@NonNull Resources resources, @Nullable String value,
            @NonNull ResourceNamespace contextNamespace,
            @NonNull ResourceNamespace.Resolver resolver) {
        if (value != null) {
            ResourceValue resValue = new UnresolvedResourceValue(value, contextNamespace, resolver);
            return resolveReference(resources, resValue);
        }
        return null;
    }

    @Nullable
    private static String resolveReference(@NonNull Resources resources,
            @NonNull ResourceValue value) {
        RenderResources renderResources = getContext(resources).getRenderResources();
        ResourceValue resolvedValue = renderResources.resolveResValue(value);
        return resolvedValue == null ? null : resolvedValue.getValue();
    }

    @LayoutlibDelegate
    static XmlResourceParser getLayout(Resources resources, int id) throws NotFoundException {
        Pair<String, ResourceValue> v = getResourceValue(resources, id);

        if (v != null) {
            ResourceValue value = v.second;

            try {
                BridgeXmlBlockParser parser =
                        ResourceHelper.getXmlBlockParser(getContext(resources), value);
                if (parser != null) {
                    return parser;
                }
            } catch (XmlPullParserException e) {
                Bridge.getLog().error(ILayoutLog.TAG_BROKEN,
                        "Failed to parse " + value.getValue(), e, null, null /*data*/);
                // we'll return null below.
            }
        }

        // id was not found or not resolved. Throw a NotFoundException.
        throwException(resources, id, "layout");

        // this is not used since the method above always throws
        return null;
    }

    @LayoutlibDelegate
    static XmlResourceParser getAnimation(Resources resources, int id) throws NotFoundException {
        Pair<String, ResourceValue> v = getResourceValue(resources, id);

        if (v != null) {
            ResourceValue value = v.second;

            try {
                return ResourceHelper.getXmlBlockParser(getContext(resources), value);
            } catch (XmlPullParserException e) {
                Bridge.getLog().error(ILayoutLog.TAG_BROKEN,
                        "Failed to parse " + value.getValue(), e, null, null /*data*/);
                // we'll return null below.
            }
        }

        // id was not found or not resolved. Throw a NotFoundException.
        throwException(resources, id);

        // this is not used since the method above always throws
        return null;
    }

    @LayoutlibDelegate
    static TypedArray obtainAttributes(Resources resources, AttributeSet set, int[] attrs) {
        BridgeContext context = getContext(resources);
        RenderResources renderResources = context.getRenderResources();
        // Remove all themes, including default, to ensure theme attributes are not resolved
        renderResources.getAllThemes().clear();
        BridgeTypedArray ta = context.internalObtainStyledAttributes(set, attrs, 0, 0);
        // Reset styles to only the default if present
        renderResources.clearStyles();
        return ta;
    }

    @LayoutlibDelegate
    static TypedArray obtainAttributes(Resources resources, Resources.Theme theme, AttributeSet
            set, int[] attrs) {
        return Resources.obtainAttributes_Original(resources, theme, set, attrs);
    }

    @LayoutlibDelegate
    static TypedArray obtainTypedArray(Resources resources, int id) throws NotFoundException {
        BridgeContext context = getContext(resources);
        ResourceReference reference = context.resolveId(id);
        RenderResources renderResources = context.getRenderResources();
        ResourceValue value = renderResources.getResolvedResource(reference);

        if (!(value instanceof ArrayResourceValue arrayValue)) {
            throw new NotFoundException("Array resource ID #0x" + Integer.toHexString(id));
        }

        int length = arrayValue.getElementCount();
        ResourceNamespace namespace = arrayValue.getNamespace();
        BridgeTypedArray typedArray = newTypeArray(resources, length);

        for (int i = 0; i < length; i++) {
            ResourceValue elementValue;
            ResourceUrl resourceUrl = ResourceUrl.parse(arrayValue.getElement(i));
            if (resourceUrl != null) {
                ResourceReference elementRef =
                  resourceUrl.resolve(namespace, arrayValue.getNamespaceResolver());
                elementValue = renderResources.getResolvedResource(elementRef);
            } else {
                elementValue = new ResourceValueImpl(namespace, ResourceType.STRING, "element" + i,
                  arrayValue.getElement(i));
            }
            typedArray.bridgeSetValue(i, elementValue.getName(), namespace, i, elementValue);
        }

        typedArray.sealArray();
        return typedArray;
    }

    @LayoutlibDelegate
    static float getDimension(Resources resources, int id) throws NotFoundException {
        Pair<String, ResourceValue> value = getResourceValue(resources, id);

        if (value != null) {
            ResourceValue resValue = value.second;

            assert resValue != null;
            String v = resValue.getValue();
            if (v != null) {
                if (v.equals(BridgeConstants.MATCH_PARENT) ||
                        v.equals(BridgeConstants.FILL_PARENT)) {
                    return LayoutParams.MATCH_PARENT;
                } else if (v.equals(BridgeConstants.WRAP_CONTENT)) {
                    return LayoutParams.WRAP_CONTENT;
                }
                TypedValue tmpValue = new TypedValue();
                if (ResourceHelper.parseFloatAttribute(
                        value.first, v, tmpValue, true /*requireUnit*/) &&
                        tmpValue.type == TypedValue.TYPE_DIMENSION) {
                    return tmpValue.getDimension(resources.getDisplayMetrics());
                }
            }
        }

        // id was not found or not resolved. Throw a NotFoundException.
        throwException(resources, id);

        // this is not used since the method above always throws
        return 0;
    }

    @LayoutlibDelegate
    static int getDimensionPixelOffset(Resources resources, int id) throws NotFoundException {
        Pair<String, ResourceValue> value = getResourceValue(resources, id);

        if (value != null) {
            ResourceValue resValue = value.second;

            assert resValue != null;
            String v = resValue.getValue();
            if (v != null) {
                TypedValue tmpValue = new TypedValue();
                if (ResourceHelper.parseFloatAttribute(
                        value.first, v, tmpValue, true /*requireUnit*/) &&
                        tmpValue.type == TypedValue.TYPE_DIMENSION) {
                    return TypedValue.complexToDimensionPixelOffset(tmpValue.data,
                            resources.getDisplayMetrics());
                }
            }
        }

        // id was not found or not resolved. Throw a NotFoundException.
        throwException(resources, id);

        // this is not used since the method above always throws
        return 0;
    }

    @LayoutlibDelegate
    static int getDimensionPixelSize(Resources resources, int id) throws NotFoundException {
        Pair<String, ResourceValue> value = getResourceValue(resources, id);

        if (value != null) {
            ResourceValue resValue = value.second;

            assert resValue != null;
            String v = resValue.getValue();
            if (v != null) {
                TypedValue tmpValue = new TypedValue();
                if (ResourceHelper.parseFloatAttribute(
                        value.first, v, tmpValue, true /*requireUnit*/) &&
                        tmpValue.type == TypedValue.TYPE_DIMENSION) {
                    return TypedValue.complexToDimensionPixelSize(tmpValue.data,
                            resources.getDisplayMetrics());
                }
            }
        }

        // id was not found or not resolved. Throw a NotFoundException.
        throwException(resources, id);

        // this is not used since the method above always throws
        return 0;
    }

    @LayoutlibDelegate
    static int getInteger(Resources resources, int id) throws NotFoundException {
        Pair<String, ResourceValue> value = getResourceValue(resources, id);

        if (value != null) {
            ResourceValue resValue = value.second;

            assert resValue != null;
            String v = resValue.getValue();
            if (v != null) {
                try {
                    return getInt(v);
                } catch (NumberFormatException e) {
                    // return exception below
                }
            }
        }

        // id was not found or not resolved. Throw a NotFoundException.
        throwException(resources, id);

        // this is not used since the method above always throws
        return 0;
    }

    @LayoutlibDelegate
    static float getFloat(Resources resources, int id) {
        Pair<String, ResourceValue> value = getResourceValue(resources, id);

        if (value != null) {
            ResourceValue resValue = value.second;

            if (resValue != null) {
                String v = resValue.getValue();
                if (v != null) {
                    try {
                        return Float.parseFloat(v);
                    } catch (NumberFormatException ignore) {
                    }
                }
            }
        }
        return 0;
    }

    @LayoutlibDelegate
    static boolean getBoolean(Resources resources, int id) throws NotFoundException {
        Pair<String, ResourceValue> value = getResourceValue(resources, id);

        if (value != null) {
            ResourceValue resValue = value.second;

            if (resValue != null) {
                String v = resValue.getValue();
                if (v != null) {
                    return Boolean.parseBoolean(v);
                }
            }
        }

        // id was not found or not resolved. Throw a NotFoundException.
        throwException(resources, id);

        // this is not used since the method above always throws
        return false;
    }

    @LayoutlibDelegate
    static String getResourceEntryName(Resources resources, int resid) throws NotFoundException {
        ResourceReference resourceInfo = getResourceInfo(resources, resid);
        if (resourceInfo != null) {
            return resourceInfo.getName();
        }
        throwException(resid, null);
        return null;
    }

    @LayoutlibDelegate
    static String getResourceName(Resources resources, int resid) throws NotFoundException {
        ResourceReference resourceInfo = getResourceInfo(resources, resid);
        if (resourceInfo != null) {
            String packageName = getPackageName(resourceInfo, resources);
            return packageName + ':' + resourceInfo.getResourceType().getName() + '/' +
                    resourceInfo.getName();
        }
        throwException(resid, null);
        return null;
    }

    @LayoutlibDelegate
    static String getResourcePackageName(Resources resources, int resid) throws NotFoundException {
        ResourceReference resourceInfo = getResourceInfo(resources, resid);
        if (resourceInfo != null) {
            return getPackageName(resourceInfo, resources);
        }
        throwException(resid, null);
        return null;
    }

    @LayoutlibDelegate
    static String getResourceTypeName(Resources resources, int resid) throws NotFoundException {
        ResourceReference resourceInfo = getResourceInfo(resources, resid);
        if (resourceInfo != null) {
            return resourceInfo.getResourceType().getName();
        }
        throwException(resid, null);
        return null;
    }

    private static String getPackageName(ResourceReference resourceInfo, Resources resources) {
        String packageName = resourceInfo.getNamespace().getPackageName();
        if (packageName == null) {
            packageName = getLayoutlibCallback(resources).getResourcePackage();
            if (packageName == null) {
                packageName = APP_PREFIX;
            }
        }
        return packageName;
    }

    @LayoutlibDelegate
    static String getString(Resources resources, int id, Object... formatArgs)
            throws NotFoundException {
        String s = getString(resources, id);
        if (s != null) {
            return String.format(s, formatArgs);

        }

        // id was not found or not resolved. Throw a NotFoundException.
        throwException(resources, id);

        // this is not used since the method above always throws
        return null;
    }

    @LayoutlibDelegate
    static String getString(Resources resources, int id) throws NotFoundException {
        Pair<String, ResourceValue> value = getResourceValue(resources, id);

        if (value != null && value.second.getValue() != null) {
            return value.second.getValue();
        }

        // id was not found or not resolved. Throw a NotFoundException.
        throwException(resources, id);

        // this is not used since the method above always throws
        return null;
    }

    @LayoutlibDelegate
    static String getQuantityString(Resources resources, int id, int quantity) throws
            NotFoundException {
        Pair<String, ResourceValue> value = getResourceValue(resources, id);

        if (value != null) {
            if (value.second instanceof PluralsResourceValue pluralsResourceValue) {
                PluralRules pluralRules = PluralRules.forLocale(resources.getConfiguration().getLocales()
                        .get(0));
                String strValue = pluralsResourceValue.getValue(pluralRules.select(quantity));
                if (strValue == null) {
                    strValue = pluralsResourceValue.getValue(PluralRules.KEYWORD_OTHER);
                }

                return strValue;
            }
            else {
                return value.second.getValue();
            }
        }

        // id was not found or not resolved. Throw a NotFoundException.
        throwException(resources, id);

        // this is not used since the method above always throws
        return null;
    }

    @LayoutlibDelegate
    static String getQuantityString(Resources resources, int id, int quantity, Object... formatArgs)
            throws NotFoundException {
        String raw = getQuantityString(resources, id, quantity);
        return String.format(resources.getConfiguration().getLocales().get(0), raw, formatArgs);
    }

    @LayoutlibDelegate
    static CharSequence getQuantityText(Resources resources, int id, int quantity) throws
            NotFoundException {
        return getQuantityString(resources, id, quantity);
    }

    @LayoutlibDelegate
    static Typeface getFont(Resources resources, int id) throws
            NotFoundException {
        Pair<String, ResourceValue> value = getResourceValue(resources, id);
        if (value != null) {
            return ResourceHelper.getFont(value.second, getContext(resources), null);
        }

        throwException(resources, id);

        // this is not used since the method above always throws
        return null;
    }

    @LayoutlibDelegate
    static Typeface getFont(Resources resources, TypedValue outValue, int id) throws
            NotFoundException {
        ResourceValue resVal = getResourceValue(resources, id, outValue);
        if (resVal != null) {
            return ResourceHelper.getFont(resVal, getContext(resources), null);
        }

        throwException(resources, id);
        return null; // This is not used since the method above always throws.
    }

    @LayoutlibDelegate
    static void getValue(Resources resources, int id, TypedValue outValue, boolean resolveRefs)
            throws NotFoundException {
        getResourceValue(resources, id, outValue);
    }

    private static ResourceValue getResourceValue(Resources resources, int id, TypedValue outValue)
            throws NotFoundException {
        Pair<String, ResourceValue> value = getResourceValue(resources, id);

        if (value != null) {
            ResourceValue resVal = value.second;
            String v = resVal != null ? resVal.getValue() : null;

            if (v != null) {
                if (ResourceHelper.parseFloatAttribute(value.first, v, outValue,
                        false /*requireUnit*/)) {
                    return resVal;
                }
                if (resVal instanceof DensityBasedResourceValue) {
                    outValue.density =
                            ((DensityBasedResourceValue) resVal).getResourceDensity().getDpiValue();
                }

                // else it's a string
                outValue.type = TypedValue.TYPE_STRING;
                outValue.string = v;
                return resVal;
            }
        }

        // id was not found or not resolved. Throw a NotFoundException.
        throwException(resources, id);
        return null; // This is not used since the method above always throws.
    }

    @LayoutlibDelegate
    static void getValue(Resources resources, String name, TypedValue outValue, boolean resolveRefs)
            throws NotFoundException {
        throw new UnsupportedOperationException();
    }

    @LayoutlibDelegate
    static void getValueForDensity(Resources resources, int id, int density, TypedValue outValue,
            boolean resolveRefs) throws NotFoundException {
        getValue(resources, id, outValue, resolveRefs);
    }

    @LayoutlibDelegate
    static int getAttributeSetSourceResId(@Nullable AttributeSet set) {
        // Not supported in layoutlib
        return Resources.ID_NULL;
    }

    @LayoutlibDelegate
    static XmlResourceParser getXml(Resources resources, int id) throws NotFoundException {
        Pair<String, ResourceValue> v = getResourceValue(resources, id);

        if (v != null) {
            ResourceValue value = v.second;

            try {
                return ResourceHelper.getXmlBlockParser(getContext(resources), value);
            } catch (XmlPullParserException e) {
                Bridge.getLog().error(ILayoutLog.TAG_BROKEN,
                        "Failed to parse " + value.getValue(), e, null, null /*data*/);
                // we'll return null below.
            }
        }

        // id was not found or not resolved. Throw a NotFoundException.
        throwException(resources, id);

        // this is not used since the method above always throws
        return null;
    }

    @LayoutlibDelegate
    static XmlResourceParser loadXmlResourceParser(Resources resources, int id,
            String type) throws NotFoundException {
        return resources.loadXmlResourceParser_Original(id, type);
    }

    @LayoutlibDelegate
    static XmlResourceParser loadXmlResourceParser(Resources resources, String file, int id,
            int assetCookie, String type) throws NotFoundException {
        return resources.loadXmlResourceParser_Original(file, id, assetCookie, type);
    }

    @LayoutlibDelegate
    public static XmlResourceParser loadXmlResourceParser(Resources resources, String file, int id,
            int assetCookie, String type, boolean usesFeatureFlags) throws NotFoundException {
        // even though we know the XML file to load directly, we still need to resolve the
        // id so that we can know if it's a platform or project resource.
        // (mPlatformResourceFlag will get the result and will be used later).
        Pair<String, ResourceValue> result = getResourceValue(resources, id);

        ResourceNamespace layoutNamespace;
        if (result != null && result.second != null) {
            layoutNamespace = result.second.getNamespace();
        } else {
            // We need to pick something, even though the resource system never heard about a layout
            // with this numeric id.
            layoutNamespace = ResourceNamespace.RES_AUTO;
        }

        try {
            XmlPullParser parser = ParserFactory.create(file);
            return new BridgeXmlBlockParser(parser, getContext(resources), layoutNamespace);
        } catch (XmlPullParserException e) {
            NotFoundException newE = new NotFoundException();
            newE.initCause(e);
            throw newE;
        }
    }

    @LayoutlibDelegate
    static InputStream openRawResource(Resources resources, int id) throws NotFoundException {
        Pair<String, ResourceValue> value = getResourceValue(resources, id);

        if (value != null) {
            String path = value.second.getValue();
            if (path != null) {
                return openRawResource(resources, path);
            }
        }

        // id was not found or not resolved. Throw a NotFoundException.
        throwException(resources, id);

        // this is not used since the method above always throws
        return null;
    }

    @LayoutlibDelegate
    static InputStream openRawResource(Resources resources, int id, TypedValue value)
            throws NotFoundException {
        getValue(resources, id, value, true);

        String path = value.string.toString();
        return openRawResource(resources, path);
    }

    private static InputStream openRawResource(Resources resources, String path)
            throws NotFoundException {
        AssetRepository repository = getAssetRepository(resources);
        try {
            InputStream stream = repository.openNonAsset(0, path, ACCESS_STREAMING);
            if (stream == null) {
                throw new NotFoundException(path);
            }
            if (path.endsWith(".9.png")) {
                stream = new NinePatchInputStream(stream, path);
            }
            return stream;
        } catch (IOException e) {
            NotFoundException exception = new NotFoundException();
            exception.initCause(e);
            throw exception;
        }
    }

    @LayoutlibDelegate
    static AssetFileDescriptor openRawResourceFd(Resources resources, int id)
            throws NotFoundException {
        throw new UnsupportedOperationException();
    }

    @VisibleForTesting
    @Nullable
    static ResourceUrl resourceUrlFromName(
            @NonNull String name, @Nullable String defType, @Nullable String defPackage) {
        int colonIdx = name.indexOf(':');
        int slashIdx = name.indexOf('/');

        if (colonIdx != -1 && slashIdx != -1) {
            // Easy case
            return ResourceUrl.parse(PREFIX_RESOURCE_REF + name);
        }

        if (colonIdx == -1 && slashIdx == -1) {
            if (defType == null) {
                throw new IllegalArgumentException("name does not define a type an no defType was" +
                        " passed");
            }

            // It does not define package or type
            return ResourceUrl.parse(
                    PREFIX_RESOURCE_REF + (defPackage != null ? defPackage + ":" : "") + defType +
                            "/" + name);
        }

        if (colonIdx != -1) {
            if (defType == null) {
                throw new IllegalArgumentException("name does not define a type an no defType was" +
                        " passed");
            }
            // We have package but no type
            String pkg = name.substring(0, colonIdx);
            ResourceType type = ResourceType.fromClassName(defType);
            return type != null ? ResourceUrl.create(pkg, type, name.substring(colonIdx + 1)) :
                    null;
        }

        ResourceType type = ResourceType.fromClassName(name.substring(0, slashIdx));
        if (type == null) {
            return null;
        }
        // We have type but no package
        return ResourceUrl.create(defPackage,
                type,
                name.substring(slashIdx + 1));
    }

    @LayoutlibDelegate
    static int getIdentifier(Resources resources, String name, String defType, String defPackage) {
        if (name == null) {
            return 0;
        }

        ResourceUrl url = resourceUrlFromName(name, defType, defPackage);
        if (url != null) {
            if (ANDROID_PKG.equals(url.namespace)) {
                return Bridge.getResourceId(url.type, url.name);
            }

            if (getLayoutlibCallback(resources).getResourcePackage().equals(url.namespace)) {
                return getLayoutlibCallback(resources).getOrGenerateResourceId(
                        new ResourceReference(ResourceNamespace.RES_AUTO, url.type, url.name));
            }
        }

        return 0;
    }

    /**
     * Builds and throws a {@link Resources.NotFoundException} based on a resource id and a resource
     * type.
     *
     * @param id the id of the resource
     * @param expectedType the type of resource that was expected
     *
     * @throws NotFoundException
     */
    private static void throwException(Resources resources, int id, @Nullable String expectedType)
            throws NotFoundException {
        throwException(id, getResourceInfo(resources, id), expectedType);
    }

    private static void throwException(Resources resources, int id) throws NotFoundException {
        throwException(resources, id, null);
    }

    private static void throwException(int id, @Nullable ResourceReference resourceInfo) {
        throwException(id, resourceInfo, null);
    }
    private static void throwException(int id, @Nullable ResourceReference resourceInfo,
            @Nullable String expectedType) {
        String message;
        if (resourceInfo != null) {
            message = String.format(
                    "Could not find %1$s resource matching value 0x%2$X (resolved name: %3$s) in current configuration.",
                    resourceInfo.getResourceType(), id, resourceInfo.getName());
        } else {
            message = String.format("Could not resolve resource value: 0x%1$X.", id);
        }

        if (expectedType != null) {
            message += " Or the resolved value was not of type " + expectedType + " as expected.";
        }
        throw new NotFoundException(message);
    }

    private static int getInt(String v) throws NumberFormatException {
        int radix = 10;
        if (v.startsWith("0x")) {
            v = v.substring(2);
            radix = 16;
        } else if (v.startsWith("0")) {
            radix = 8;
        }
        return Integer.parseInt(v, radix);
    }

    private static AssetRepository getAssetRepository(Resources resources) {
        BridgeContext context = getContext(resources);
        BridgeAssetManager assetManager = context.getAssets();
        return assetManager.getAssetRepository();
    }
}
