/*
 * Copyright (C) 2012 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.res2;

import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX;
import static com.android.SdkConstants.ATTR_FORMAT;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_TYPE;
import static com.android.SdkConstants.TAG_EAT_COMMENT;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.SdkConstants.TAG_SKIP;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceType;
import com.android.utils.PositionXmlParser;
import com.android.utils.XmlUtils;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.ParserConfigurationException;

/**
 * Parser for "values" files.
 *
 * This parses the file and returns a list of {@link ResourceItem} object.
 */
class ValueResourceParser2 {

    @NonNull
    private final File mFile;

    @Nullable
    private final String mLibraryName;

    private boolean mTrackSourcePositions = true;

    /**
     * Creates the parser for a given file.
     * @param file the file to parse.
     */
    ValueResourceParser2(@NonNull File file, @Nullable String libraryName) {
        mFile = file;
        mLibraryName = libraryName;
    }

    /**
     * Set whether or not to use a source position-tracking XML parser.
     */
    void setTrackSourcePositions(boolean value) {
        mTrackSourcePositions = value;
    }

    /**
     * Parses the file and returns a list of {@link ResourceItem} objects.
     * @return a list of resources.
     *
     * @throws MergingException if a merging exception happens
     */
    @NonNull
    List<ResourceItem> parseFile() throws MergingException {
        Document document = parseDocument(mFile, mTrackSourcePositions);

        // get the root node
        Node rootNode = document.getDocumentElement();
        if (rootNode == null) {
            return Collections.emptyList();
        }
        NodeList nodes = rootNode.getChildNodes();

        final int count = nodes.getLength();
        // list containing the result
        List<ResourceItem> resources = Lists.newArrayListWithExpectedSize(count);
        // Multimap to detect dups
        Map<ResourceType, Set<String>> map = Maps.newEnumMap(ResourceType.class);

        for (int i = 0, n = nodes.getLength(); i < n; i++) {
            Node node = nodes.item(i);

            if (node.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }

            ResourceItem resource = getResource(node, mFile, mLibraryName);
            if (resource != null) {
                // check this is not a dup
                checkDuplicate(resource, map, mFile);

                resources.add(resource);

                if (resource.getType() == ResourceType.DECLARE_STYLEABLE) {
                    // Need to also create ATTR items for its children
                    addStyleableItems(node, resources, map, mFile, mLibraryName);
                }
            }
        }

        return resources;
    }

    /**
     * Returns a new ResourceItem object for a given node.
     * @param node the node representing the resource.
     * @return a ResourceItem object or null.
     */
    static ResourceItem getResource(@NonNull Node node, @Nullable File from, @Nullable String libraryName)
            throws MergingException {
        ResourceType type = getType(node, from);
        String name = getName(node);

        if (name != null) {
            if (type != null) {
                ValueResourceNameValidator.validate(name, type, from);
                return new ResourceItem(name, type, node, libraryName);
            }
        } else if (type == ResourceType.PUBLIC) {
            // Allow a <public /> node with no name: this means all resources are private
            return new ResourceItem("", type, node, libraryName);
        }

        return null;
    }

    /**
     * Returns the type of the ResourceItem based on a node's attributes.
     * @param node the node
     * @return the ResourceType or null if it could not be inferred.
     */
    static ResourceType getType(@NonNull Node node, @Nullable File from) throws MergingException {
        String nodeName = node.getLocalName();
        String typeString = null;

        if (TAG_ITEM.equals(nodeName)) {
            Attr attribute = (Attr) node.getAttributes().getNamedItemNS(null, ATTR_TYPE);
            if (attribute != null) {
                typeString = attribute.getValue();
            }
        } else if (TAG_EAT_COMMENT.equals(nodeName) || TAG_SKIP.equals(nodeName)) {
            return null;
        } else {
            // the type is the name of the node.
            typeString = nodeName;
        }

        if (typeString != null) {
            ResourceType type = ResourceType.getEnum(typeString);
            if (type != null) {
                return type;
            }
            throw MergingException.withMessage("Unsupported type '%s'", typeString).withFile(from)
                    .build();
        }

        throw MergingException.withMessage("Unsupported node '%s'", nodeName).withFile(from).build();
    }

    /**
     * Returns the name of the resource based a node's attributes.
     * @param node the node.
     * @return the name or null if it could not be inferred.
     */
    static String getName(@NonNull Node node) {
        Attr attribute = (Attr) node.getAttributes().getNamedItemNS(null, ATTR_NAME);

        if (attribute != null) {
            return attribute.getValue();
        }

        return null;
    }

    /**
     * Loads the DOM for a given file and returns a {@link Document} object.
     * @param file the file to parse
     * @param trackPositions should track XML node positions
     * @return a Document object.
     * @throws MergingException if a merging exception happens
     */
    @NonNull
    static Document parseDocument(@NonNull File file, boolean trackPositions) throws MergingException {
        try {
            if (trackPositions) {
                return PositionXmlParser.parse(new BufferedInputStream(new FileInputStream(file)));
            }
            else {
                return XmlUtils.parseUtfXmlFile(file, true);
            }
        } catch (SAXException e) {
            throw MergingException.wrapException(e).withFile(file).build();
        } catch (ParserConfigurationException e) {
            throw MergingException.wrapException(e).withFile(file).build();
        } catch (IOException e) {
            throw MergingException.wrapException(e).withFile(file).build();
        }
    }

    /**
     * Adds any declare styleable attr items below the given declare styleable nodes
     * into the given list
     *
     * @param styleableNode the declare styleable node
     * @param list the list to add items into
     * @param map map of existing items to detect dups.
     */
    static void addStyleableItems(@NonNull Node styleableNode,
                                  @NonNull List<ResourceItem> list,
                                  @Nullable Map<ResourceType, Set<String>> map,
                                  @Nullable File from,
                                  @Nullable String libraryName)
            throws MergingException {
        assert styleableNode.getNodeName().equals(ResourceType.DECLARE_STYLEABLE.getName());
        NodeList nodes = styleableNode.getChildNodes();

        for (int i = 0, n = nodes.getLength(); i < n; i++) {
            Node node = nodes.item(i);

            if (node.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }

            ResourceItem resource = getResource(node, from, libraryName);
            if (resource != null) {
                assert resource.getType() == ResourceType.ATTR;

                // is the attribute in the android namespace?
                if (!resource.getName().startsWith(ANDROID_NS_NAME_PREFIX)) {
                    if (hasFormatAttribute(node) || XmlUtils.hasElementChildren(node)) {
                        checkDuplicate(resource, map, from);
                        resource.setIgnoredFromDiskMerge(true);
                        list.add(resource);
                    }
                }
            }
        }
    }

    private static void checkDuplicate(@NonNull ResourceItem resource,
                                       @Nullable Map<ResourceType, Set<String>> map,
                                       @Nullable File from)
            throws MergingException {
        if (map == null) {
            return;
        }

        String name = resource.getName();
        Set<String> set = map.get(resource.getType());
        if (set == null) {
            set = Sets.newHashSet(name);
            map.put(resource.getType(), set);
        } else {
            if (set.contains(name) && resource.getType() != ResourceType.PUBLIC) {
                throw MergingException.withMessage(
                        "Found item %s/%s more than one time",
                        resource.getType().getDisplayName(), name).withFile(from).build();
            }

            set.add(name);
        }
    }

    private static boolean hasFormatAttribute(Node node) {
        return node.getAttributes().getNamedItemNS(null, ATTR_FORMAT) != null;
    }
}
