/*
 * Copyright (C) 2013 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.ide.common.resources;

import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
import static com.android.SdkConstants.PREFIX_THEME_REF;
import static com.android.ide.common.resources.ResourceResolver.MAX_RESOURCE_INDIRECTION;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.rendering.api.*;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.resources.ResourceType;

import java.util.List;

/**
 * Like {@link ResourceResolver} but for a single item, so it does not need the full resource maps
 * to be resolved up front. Typically used for cases where we may not have fully configured
 * resource maps and we need to look up a specific value, such as in Android Studio where
 * a color reference is found in an XML style file, and we want to resolve it in order to
 * display the final resolved color in the editor margin.
 */
public class ResourceItemResolver extends RenderResources {
    private final FolderConfiguration mConfiguration;
    private final LayoutLog mLogger;
    private final ResourceProvider mResourceProvider;
    private ResourceRepository mFrameworkResources;
    private ResourceResolver mResolver;
    private AbstractResourceRepository myAppResources;
    @Nullable private List<ResourceValue> mLookupChain;

    public ResourceItemResolver(
            @NonNull FolderConfiguration configuration,
            @NonNull ResourceProvider resourceProvider,
            @Nullable LayoutLog logger) {
        mConfiguration = configuration;
        mResourceProvider = resourceProvider;
        mLogger = logger;
        mResolver = resourceProvider.getResolver(false);
    }

    public ResourceItemResolver(
            @NonNull FolderConfiguration configuration,
            @NonNull ResourceRepository frameworkResources,
            @NonNull AbstractResourceRepository appResources,
            @Nullable LayoutLog logger) {
        mConfiguration = configuration;
        mResourceProvider = null;
        mLogger = logger;
        mFrameworkResources = frameworkResources;
        myAppResources = appResources;
    }

    @Override
    @Nullable
    public ResourceValue resolveResValue(@Nullable ResourceValue resValue) {
        if (mResolver != null) {
            return mResolver.resolveResValue(resValue);
        }
        if (mLookupChain != null) {
            mLookupChain.add(resValue);
        }
        return resolveResValue(resValue, 0);
    }

    @Nullable
    private ResourceValue resolveResValue(@Nullable ResourceValue resValue, int depth) {
        if (resValue == null) {
            return null;
        }

        String value = resValue.getValue();
        if (value == null || resValue instanceof ArrayResourceValue) {
            // If there's no value or this an array resource (eg. <string-array>), return.
            return resValue;
        }

        // else attempt to find another ResourceValue referenced by this one.
        ResourceValue resolvedResValue = findResValue(value, resValue.isFramework());

        // if the value did not reference anything, then we simply return the input value
        if (resolvedResValue == null) {
            return resValue;
        }

        // detect potential loop due to mishandled namespace in attributes
        if (resValue == resolvedResValue || depth >= MAX_RESOURCE_INDIRECTION) {
            if (mLogger != null) {
                mLogger.error(LayoutLog.TAG_BROKEN,
                        String.format(
                                "Potential stack overflow trying to resolve '%s': cyclic resource definitions? Render may not be accurate.",
                                value),
                        null);
            }
            return resValue;
        }

        // otherwise, we attempt to resolve this new value as well
        return resolveResValue(resolvedResValue, depth + 1);
    }

    @Override
    @Nullable
    public ResourceValue findResValue(@Nullable String reference, boolean inFramework) {
        if (mResolver != null) {
            return mResolver.findResValue(reference, inFramework);
        }

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

        if (mLookupChain != null && !mLookupChain.isEmpty() &&
                reference.startsWith(PREFIX_RESOURCE_REF)) {
            ResourceValue prev = mLookupChain.get(mLookupChain.size() - 1);
            if (!reference.equals(prev.getValue())) {
                ResourceValue next = new ResourceValue(prev.getResourceType(), prev.getName(),
                        prev.isFramework(), prev.getLibraryName());
                next.setValue(reference);
                mLookupChain.add(next);
            }
        }

        ResourceUrl resource = ResourceUrl.parse(reference);
        if (resource != null && resource.hasValidName()) {
            if (resource.theme) {
                // Do theme lookup? We can't do that here; requires full global analysis, so just use
                // a real resource resolver
                ResourceResolver resolver = getFullResolver();
                if (resolver != null) {
                    return resolver.findResValue(reference, inFramework);
                } else {
                    return null;
                }
            } else if (reference.startsWith(PREFIX_RESOURCE_REF)) {
                return findResValue(resource.type, resource.name, inFramework || resource.framework);
            }
        }

        // Looks like the value didn't reference anything. Return null.
        return null;
    }

    private ResourceValue findResValue(ResourceType resType, String resName, boolean framework) {
        // map of ResourceValue for the given type
        // if allowed, search in the project resources first.
        if (!framework) {
            if (myAppResources == null) {
                assert mResourceProvider != null;
                myAppResources = mResourceProvider.getAppResources();
                if (myAppResources == null) {
                    return null;
                }
            }
            ResourceValue item = null;
            item = myAppResources.getConfiguredValue(resType, resName, mConfiguration);
            if (item != null) {
                if (mLookupChain != null) {
                    mLookupChain.add(item);
                }
                return item;
            }
        } else {
            if (mFrameworkResources == null) {
                assert mResourceProvider != null;
                mFrameworkResources = mResourceProvider.getFrameworkResources();
                if (mFrameworkResources == null) {
                    return null;
                }
            }
            // now search in the framework resources.
            if (mFrameworkResources.hasResourceItem(resType, resName)) {
                ResourceItem item = mFrameworkResources.getResourceItem(resType, resName);
                ResourceValue value = item.getResourceValue(resType, mConfiguration, true);
                if (value != null && mLookupChain != null) {
                    mLookupChain.add(value);
                }
                return value;
            }
        }

        // didn't find the resource anywhere.
        if (mLogger != null) {
            mLogger.warning(LayoutLog.TAG_RESOURCES_RESOLVE,
                    "Couldn't resolve resource @" +
                            (framework ? "android:" : "") + resType + "/" + resName,
                    new ResourceValue(resType, resName, framework));
        }
        return null;
    }

    @Override
    public StyleResourceValue getCurrentTheme() {
        ResourceResolver resolver = getFullResolver();
        if (resolver != null) {
            return resolver.getCurrentTheme();
        }

        return null;
    }

    @Override
    public ResourceValue resolveValue(ResourceType type, String name, String value,
            boolean isFrameworkValue) {
        if (value == null) {
            return null;
        }

        // get the ResourceValue referenced by this value
        ResourceValue resValue = findResValue(value, isFrameworkValue);

        // if resValue is null, but value is not null, this means it was not a reference.
        // we return the name/value wrapper in a ResourceValue. the isFramework flag doesn't
        // matter.
        if (resValue == null) {
            return new ResourceValue(type, name, value, isFrameworkValue);
        }

        // we resolved a first reference, but we need to make sure this isn't a reference also.
        return resolveResValue(resValue);
    }

    // For theme lookup, we need to delegate to a full resource resolver

    @Override
    public StyleResourceValue getTheme(String name, boolean frameworkTheme) {
        assert false; // This method shouldn't be called on this resolver
        return super.getTheme(name, frameworkTheme);
    }

    @Override
    public boolean themeIsParentOf(StyleResourceValue parentTheme, StyleResourceValue childTheme) {
        assert false; // This method shouldn't be called on this resolver
        return super.themeIsParentOf(parentTheme, childTheme);
    }

    @SuppressWarnings("deprecation")
    @Override
    public ResourceValue findItemInTheme(String itemName) {
        ResourceResolver resolver = getFullResolver();
        return resolver != null ? resolver.findItemInTheme(itemName) : null;
    }

    @Override
    public ResourceValue findItemInTheme(String attrName, boolean isFrameworkAttr) {
        ResourceResolver resolver = getFullResolver();
        return resolver != null ? resolver.findItemInTheme(attrName, isFrameworkAttr) : null;
    }

    @SuppressWarnings("deprecation")
    @Override
    public ItemResourceValue findItemInStyle(StyleResourceValue style, String attrName) {
        ResourceResolver resolver = getFullResolver();
        return resolver != null ? resolver.findItemInStyle(style, attrName) : null;
    }

    @Override
    public ItemResourceValue findItemInStyle(StyleResourceValue style, String attrName,
            boolean isFrameworkAttr) {
        ResourceResolver resolver = getFullResolver();
        return resolver != null ? resolver.findItemInStyle(style, attrName, isFrameworkAttr) : null;
    }

    @Override
    public StyleResourceValue getParent(StyleResourceValue style) {
        ResourceResolver resolver = getFullResolver();
        return resolver != null ? resolver.getParent(style) : null;
    }

    private ResourceResolver getFullResolver() {
        if (mResolver == null) {
            if (mResourceProvider == null) {
                return null;
            }
            mResolver = mResourceProvider.getResolver(true);
            if (mResolver != null) {
                if (mLookupChain != null) {
                    mResolver = mResolver.createRecorder(mLookupChain);
                }
            }

        }
        return mResolver;
    }

    /**
     * Optional method to set a list the resolver should record all value resolutions
     * into. Useful if you want to find out the resolution chain for a resource,
     * e.g. {@code @color/buttonForeground ⇒ @color/foreground ⇒ @android:color/black }.
     * <p>
     * There is no getter. Clients setting this list should look it up themselves.
     * Note also that if this resolver has to delegate to a full resource resolver,
     * e.g. to follow theme attributes, those resolutions will not be recorded.
     *
     * @param lookupChain the list to set, or null
     */
    public void setLookupChainList(@Nullable List<ResourceValue> lookupChain) {
        mLookupChain = lookupChain;
    }

    /** Returns the lookup chain being used by this resolver */
    @Nullable
    public List<ResourceValue> getLookupChain() {
        return mLookupChain;
    }

    /**
     * Returns a display string for a resource lookup
     *
     * @param type the resource type
     * @param name the resource name
     * @param isFramework whether the item is in the framework
     * @param lookupChain the list of resolved items to display
     * @return the display string
     */
    public static String getDisplayString(
            @NonNull ResourceType type,
            @NonNull String name,
            boolean isFramework,
            @NonNull List<ResourceValue> lookupChain) {
        String url = ResourceUrl.create(type, name, isFramework, false).toString();
        return getDisplayString(url, lookupChain);
    }

    /**
     * Returns a display string for a resource lookup
     * @param url the resource url, such as {@code @string/foo}
     * @param lookupChain the list of resolved items to display
     * @return the display string
     */
    @NonNull
    public static String getDisplayString(
            @NonNull String url,
            @NonNull List<ResourceValue> lookupChain) {
        StringBuilder sb = new StringBuilder();
        sb.append(url);
        String prev = url;
        for (ResourceValue element : lookupChain) {
            if (element == null) {
                continue;
            }
            String value = element.getValue();
            if (value == null) {
                continue;
            }
            String text = value;
            if (text.equals(prev)) {
                continue;
            }

            sb.append(" => ");

            // Strip paths
            if (!(text.startsWith(PREFIX_THEME_REF) || text.startsWith(PREFIX_RESOURCE_REF))) {
                int end = Math.max(text.lastIndexOf('/'), text.lastIndexOf('\\'));
                if (end != -1) {
                    text = text.substring(end + 1);
                }
            }
            sb.append(text);

            prev = value;
        }

        return sb.toString();
    }

    /**
     * Interface implemented by clients of the {@link ResourceItemResolver} which allows
     * it to lazily look up the project resources, the framework resources and optionally
     * to provide a fully configured resource resolver, if any
     */
    public interface ResourceProvider {
        @Nullable ResourceResolver getResolver(boolean createIfNecessary);
        @Nullable ResourceRepository getFrameworkResources();
        @Nullable AbstractResourceRepository getAppResources();
    }
}
