/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
 *
 * 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.eclipse.adt.internal.refactorings.extractstring;

import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors;

import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jface.text.IDocument;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

/**
 * An helper utility to get IDs out of an Android XML resource file.
 */
@SuppressWarnings("restriction")
class XmlStringFileHelper {

    /** A temporary cache of R.string IDs defined by a given xml file. The key is the
     * project path of the file, the data is a set of known string Ids for that file.
     *
     * Map type: map [String filename] => map [String id => String value].
     */
    private HashMap<String, Map<String, String>> mResIdCache =
        new HashMap<String, Map<String, String>>();

    public XmlStringFileHelper() {
    }

    /**
     * Utility method used by the wizard to retrieve the actual value definition of a given
     * string ID.
     *
     * @param project The project contain the XML file.
     * @param xmlFileWsPath The project path of the XML file, e.g. "/res/values/strings.xml".
     *          The given file may or may not exist.
     * @param stringId The string ID to find.
     * @return The value string if the ID is defined, null otherwise.
     */
    public String valueOfStringId(IProject project, String xmlFileWsPath, String stringId) {
        Map<String, String> cache = getResIdsForFile(project, xmlFileWsPath);
        return cache.get(stringId);
    }

    /**
     * Utility method that retrieves all the *string* IDs defined in the given Android resource
     * file. The instance maintains an internal cache so a given file is retrieved only once.
     * Callers should consider the set to be read-only.
     *
     * @param project The project contain the XML file.
     * @param xmlFileWsPath The project path of the XML file, e.g. "/res/values/strings.xml".
     *          The given file may or may not exist.
     * @return The map of string IDs => values defined in the given file. Cached. Never null.
     */
    public Map<String, String> getResIdsForFile(IProject project, String xmlFileWsPath) {
        Map<String, String> cache = mResIdCache.get(xmlFileWsPath);
        if (cache == null) {
            cache = internalGetResIdsForFile(project, xmlFileWsPath);
            mResIdCache.put(xmlFileWsPath, cache);
        }
        return cache;
    }

    /**
     * Extract all the defined string IDs from a given file using XPath.
     * @param project The project contain the XML file.
     * @param xmlFileWsPath The project path of the file to parse. It may not exist.
     * @return The map of all string IDs => values defined in the file.
     *   The returned set is always non null. It is empty if the file does not exist.
     */
    private Map<String, String> internalGetResIdsForFile(IProject project, String xmlFileWsPath) {

        TreeMap<String, String> ids = new TreeMap<String, String>();

        // Access the project that contains the resource that contains the compilation unit
        IResource resource = project.getFile(xmlFileWsPath);

        if (resource != null && resource.exists() && resource.getType() == IResource.FILE) {
            IStructuredModel smodel = null;

            try {
                IFile file = (IFile) resource;
                IModelManager modelMan = StructuredModelManager.getModelManager();
                smodel = modelMan.getExistingModelForRead(file);
                if (smodel == null) {
                    smodel = modelMan.getModelForRead(file);
                }

                if (smodel instanceof IDOMModel) {
                    IDOMDocument doc = ((IDOMModel) smodel).getDocument();

                    // We want all the IDs in an XML structure like this:
                    // <resources>
                    //    <string name="ID">something</string>
                    // </resources>

                    Node root = findChild(doc, null, ResourcesDescriptors.ROOT_ELEMENT);
                    if (root != null) {
                        for (Node strNode = findChild(root, null,
                                                      ResourcesDescriptors.STRING_ELEMENT);
                             strNode != null;
                             strNode = findChild(null, strNode,
                                                 ResourcesDescriptors.STRING_ELEMENT)) {
                            NamedNodeMap attrs = strNode.getAttributes();
                            Node nameAttr = attrs.getNamedItem(ResourcesDescriptors.NAME_ATTR);
                            if (nameAttr != null) {
                                String id = nameAttr.getNodeValue();

                                // Find the TEXT node right after the element.
                                // Whitespace matters so we don't try to normalize it.
                                String text = "";                       //$NON-NLS-1$
                                for (Node txtNode = strNode.getFirstChild();
                                        txtNode != null && txtNode.getNodeType() == Node.TEXT_NODE;
                                        txtNode = txtNode.getNextSibling()) {
                                    text += txtNode.getNodeValue();
                                }

                                ids.put(id, text);
                            }
                        }
                    }
                }

            } catch (Throwable e) {
                AdtPlugin.log(e, "GetResIds failed in %1$s", xmlFileWsPath); //$NON-NLS-1$
            } finally {
                if (smodel != null) {
                    smodel.releaseFromRead();
                }
            }
        }

        return ids;
    }

    /**
     * Utility method that finds the next node of the requested element name.
     *
     * @param parent The parent node. If not null, will to start searching its children.
     *               Set to null when iterating through children.
     * @param lastChild The last child returned. Use null when visiting a parent the first time.
     * @param elementName The element name of the node to find.
     * @return The next children or sibling nide with the requested element name or null.
     */
    private Node findChild(Node parent, Node lastChild, String elementName) {
        if (lastChild == null && parent != null) {
            lastChild = parent.getFirstChild();
        } else if (lastChild != null) {
            lastChild = lastChild.getNextSibling();
        }

        for ( ; lastChild != null ; lastChild = lastChild.getNextSibling()) {
            if (lastChild.getNodeType() == Node.ELEMENT_NODE &&
                    lastChild.getNamespaceURI() == null &&  // resources don't have any NS URI
                    elementName.equals(lastChild.getLocalName())) {
                return lastChild;
            }
        }

        return null;
    }

}
