/*
 * Copyright (C) 2011 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 com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.io.IAbstractFile;
import com.android.io.IAbstractFolder;
import com.android.resources.ResourceType;
import com.android.utils.ILogger;
import com.google.common.base.Charsets;

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

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;

/**
 * Framework resources repository.
 *
 * This behaves the same as {@link ResourceRepository} except that it differentiates between
 * resources that are public and non public.
 * {@link #getResourceItemsOfType(ResourceType)} and {@link #hasResourcesOfType(ResourceType)} only return
 * public resources. This is typically used to display resource lists in the UI.
 *
 * {@link #getConfiguredResources(com.android.ide.common.resources.configuration.FolderConfiguration)}
 * returns all resources, even the non public ones so that this can be used for rendering.
 */
public class FrameworkResources extends ResourceRepository {

    /**
     * Map of {@link ResourceType} to list of items. It is guaranteed to contain a list for all
     * possible values of ResourceType.
     */
    protected final Map<ResourceType, List<ResourceItem>> mPublicResourceMap =
        new EnumMap<ResourceType, List<ResourceItem>>(ResourceType.class);

    public FrameworkResources(@NonNull IAbstractFolder resFolder) {
        super(resFolder, true /*isFrameworkRepository*/);
    }

    /**
     * Returns a {@link Collection} (always non null, but can be empty) of <b>public</b>
     * {@link ResourceItem} matching a given {@link ResourceType}.
     *
     * @param type the type of the resources to return
     * @return a collection of items, possibly empty.
     */
    @Override
    @NonNull
    public List<ResourceItem> getResourceItemsOfType(@NonNull ResourceType type) {
        return mPublicResourceMap.get(type);
    }

    /**
     * Returns whether the repository has <b>public</b> resources of a given {@link ResourceType}.
     * @param type the type of resource to check.
     * @return true if the repository contains resources of the given type, false otherwise.
     */
    @Override
    public boolean hasResourcesOfType(@NonNull ResourceType type) {
        return !mPublicResourceMap.get(type).isEmpty();
    }

    @Override
    @NonNull
    protected ResourceItem createResourceItem(@NonNull String name) {
        return new FrameworkResourceItem(name);
    }

    /**
     * Reads the public.xml file in data/res/values/ for a given resource folder and builds up
     * a map of public resources.
     *
     * This map is a subset of the full resource map that only contains framework resources
     * that are public.
     *
     * @param logger a logger to report issues to
     */
    public void loadPublicResources(@Nullable ILogger logger) {
        IAbstractFolder valueFolder = getResFolder().getFolder(SdkConstants.FD_RES_VALUES);
        if (!valueFolder.exists()) {
            return;
        }

        IAbstractFile publicXmlFile = valueFolder.getFile("public.xml"); //$NON-NLS-1$
        if (publicXmlFile.exists()) {
            Reader reader = null;
            try {
                reader = new BufferedReader(new InputStreamReader(publicXmlFile.getContents(),
                        Charsets.UTF_8));
                KXmlParser parser = new KXmlParser();
                parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
                parser.setInput(reader);

                ResourceType lastType = null;
                String lastTypeName = "";
                while (true) {
                    int event = parser.next();
                    if (event == XmlPullParser.START_TAG) {
                        // As of API 15 there are a number of "java-symbol" entries here
                        if (!parser.getName().equals("public")) { //$NON-NLS-1$
                            continue;
                        }

                        String name = null;
                        String typeName = null;
                        for (int i = 0, n = parser.getAttributeCount(); i < n; i++) {
                            String attribute = parser.getAttributeName(i);

                            if (attribute.equals("name")) { //$NON-NLS-1$
                                name = parser.getAttributeValue(i);
                                if (typeName != null) {
                                    // Skip id attribute processing
                                    break;
                                }
                            } else if (attribute.equals("type")) { //$NON-NLS-1$
                                typeName = parser.getAttributeValue(i);
                            }
                        }

                        if (name != null && typeName != null) {
                            ResourceType type = null;
                            if (typeName.equals(lastTypeName)) {
                                type = lastType;
                            } else {
                                type = ResourceType.getEnum(typeName);
                                lastType = type;
                                lastTypeName = typeName;
                            }
                            if (type != null) {
                                ResourceItem match = null;
                                Map<String, ResourceItem> map = mResourceMap.get(type);
                                if (map != null) {
                                    match = map.get(name);
                                }

                                if (match != null) {
                                    List<ResourceItem> publicList = mPublicResourceMap.get(type);
                                    if (publicList == null) {
                                        // Pick initial size for the list to hold the public
                                        // resources. We could just use map.size() here,
                                        // but they're usually much bigger; for example,
                                        // in one platform version, there are 1500 drawables
                                        // and 1200 strings but only 175 and 25 public ones
                                        // respectively.
                                        int size;
                                        switch (type) {
                                            case STYLE: size = 500; break;
                                            case ATTR: size = 1050; break;
                                            case DRAWABLE: size = 200; break;
                                            case ID: size = 50; break;
                                            case LAYOUT:
                                            case COLOR:
                                            case STRING:
                                            case ANIM:
                                            case INTERPOLATOR:
                                                size = 30;
                                                break;
                                            default:
                                                size = 10;
                                                break;
                                        }
                                        publicList = new ArrayList<ResourceItem>(size);
                                        mPublicResourceMap.put(type, publicList);
                                    }

                                    publicList.add(match);
                                } else {
                                    // log that there's a public resource that doesn't actually
                                    // exist?
                                }
                            } else {
                                // log that there was a reference to a typo that doesn't actually
                                // exist?
                            }
                        }
                    } else if (event == XmlPullParser.END_DOCUMENT) {
                        break;
                    }
                }
            } catch (Exception e) {
                if (logger != null) {
                    logger.error(e, "Can't read and parse public attribute list");
                }
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (IOException e) {
                        // Nothing to be done here - we don't care if it closed or not.
                    }
                }
            }
        }

        // put unmodifiable list for all res type in the public resource map
        // this will simplify access
        for (ResourceType type : ResourceType.values()) {
            List<ResourceItem> list = mPublicResourceMap.get(type);
            if (list == null) {
                list = Collections.emptyList();
            } else {
                list = Collections.unmodifiableList(list);
            }

            // put the new list in the map
            mPublicResourceMap.put(type, list);
        }
    }
}

