/*
 * 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.tools.lint.client.api;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.XmlScanner;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.ResourceContext;
import com.android.tools.lint.detector.api.XmlContext;
import com.google.common.annotations.Beta;

import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.RandomAccess;

/**
 * Specialized visitor for running detectors on resources: typically XML documents,
 * but also binary resources.
 * <p>
 * It operates in two phases:
 * <ol>
 *   <li> First, it computes a set of maps where it generates a map from each
 *        significant element name, and each significant attribute name, to a list
 *        of detectors to consult for that element or attribute name.
 *        The set of element names or attribute names (or both) that a detector
 *        is interested in is provided by the detectors themselves.
 *   <li> Second, it iterates over the document a single time. For each element and
 *        attribute it looks up the list of interested detectors, and runs them.
 * </ol>
 * It also notifies all the detectors before and after the document is processed
 * such that they can do pre- and post-processing.
 * <p>
 * <b>NOTE: This is not a public or final API; if you rely on this be prepared
 * to adjust your code for the next tools release.</b>
 */
@Beta
class ResourceVisitor {
    private final Map<String, List<Detector.XmlScanner>> mElementToCheck =
            new HashMap<String, List<Detector.XmlScanner>>();
    private final Map<String, List<Detector.XmlScanner>> mAttributeToCheck =
            new HashMap<String, List<Detector.XmlScanner>>();
    private final List<Detector.XmlScanner> mDocumentDetectors =
            new ArrayList<Detector.XmlScanner>();
    private final List<Detector.XmlScanner> mAllElementDetectors =
            new ArrayList<Detector.XmlScanner>();
    private final List<Detector.XmlScanner> mAllAttributeDetectors =
            new ArrayList<Detector.XmlScanner>();
    private final List<? extends Detector> mAllDetectors;
    private final List<? extends Detector> mBinaryDetectors;
    private final XmlParser mParser;

    // Really want this:
    //<T extends List<Detector> & Detector.XmlScanner> XmlVisitor(IDomParser parser,
    //    T xmlDetectors) {
    // but it makes client code tricky and ugly.
    ResourceVisitor(
            @NonNull XmlParser parser,
            @NonNull List<? extends Detector> xmlDetectors,
            @Nullable List<Detector> binaryDetectors) {
        mParser = parser;
        mAllDetectors = xmlDetectors;
        mBinaryDetectors = binaryDetectors;

        // TODO: Check appliesTo() for files, and find a quick way to enable/disable
        // rules when running through a full project!
        for (Detector detector : xmlDetectors) {
            Detector.XmlScanner xmlDetector = (XmlScanner) detector;
            Collection<String> attributes = xmlDetector.getApplicableAttributes();
            if (attributes == XmlScanner.ALL) {
                mAllAttributeDetectors.add(xmlDetector);
            }  else if (attributes != null) {
                for (String attribute : attributes) {
                    List<Detector.XmlScanner> list = mAttributeToCheck.get(attribute);
                    if (list == null) {
                        list = new ArrayList<Detector.XmlScanner>();
                        mAttributeToCheck.put(attribute, list);
                    }
                    list.add(xmlDetector);
                }
            }
            Collection<String> elements = xmlDetector.getApplicableElements();
            if (elements == XmlScanner.ALL) {
                mAllElementDetectors.add(xmlDetector);
            } else if (elements != null) {
                for (String element : elements) {
                    List<Detector.XmlScanner> list = mElementToCheck.get(element);
                    if (list == null) {
                        list = new ArrayList<Detector.XmlScanner>();
                        mElementToCheck.put(element, list);
                    }
                    list.add(xmlDetector);
                }
            }

            if ((attributes == null || (attributes.isEmpty()
                    && attributes != XmlScanner.ALL))
                  && (elements == null || (elements.isEmpty()
                  && elements != XmlScanner.ALL))) {
                mDocumentDetectors.add(xmlDetector);
            }
        }
    }

    void visitFile(@NonNull XmlContext context, @NonNull File file) {
        assert LintUtils.isXmlFile(file);

        try {
            if (context.document == null) {
                context.document = mParser.parseXml(context);
                if (context.document == null) {
                    // No need to log this; the parser should be reporting
                    // a full warning (such as IssueRegistry#PARSER_ERROR)
                    // with details, location, etc.
                    return;
                }
                if (context.document.getDocumentElement() == null) {
                    // Ignore empty documents
                    return;
                }
            }

            for (Detector check : mAllDetectors) {
                check.beforeCheckFile(context);
            }

            for (Detector.XmlScanner check : mDocumentDetectors) {
                check.visitDocument(context, context.document);
            }

            if (!mElementToCheck.isEmpty() || !mAttributeToCheck.isEmpty()
                    || !mAllAttributeDetectors.isEmpty() || !mAllElementDetectors.isEmpty()) {
                visitElement(context, context.document.getDocumentElement());
            }

            for (Detector check : mAllDetectors) {
                check.afterCheckFile(context);
            }
        } finally {
            if (context.document != null) {
                mParser.dispose(context, context.document);
                context.document = null;
            }
        }
    }

    private void visitElement(@NonNull XmlContext context, @NonNull Element element) {
        List<Detector.XmlScanner> elementChecks = mElementToCheck.get(element.getTagName());
        if (elementChecks != null) {
            assert elementChecks instanceof RandomAccess;
            for (XmlScanner check : elementChecks) {
                check.visitElement(context, element);
            }
        }
        if (!mAllElementDetectors.isEmpty()) {
            for (XmlScanner check : mAllElementDetectors) {
                check.visitElement(context, element);
            }
        }

        if (!mAttributeToCheck.isEmpty() || !mAllAttributeDetectors.isEmpty()) {
            NamedNodeMap attributes = element.getAttributes();
            for (int i = 0, n = attributes.getLength(); i < n; i++) {
                Attr attribute = (Attr) attributes.item(i);
                String name = attribute.getLocalName();
                if (name == null) {
                    name = attribute.getName();
                }
                List<Detector.XmlScanner> list = mAttributeToCheck.get(name);
                if (list != null) {
                    for (XmlScanner check : list) {
                        check.visitAttribute(context, attribute);
                    }
                }
                if (!mAllAttributeDetectors.isEmpty()) {
                    for (XmlScanner check : mAllAttributeDetectors) {
                        check.visitAttribute(context, attribute);
                    }
                }
            }
        }

        // Visit children
        NodeList childNodes = element.getChildNodes();
        for (int i = 0, n = childNodes.getLength(); i < n; i++) {
            Node child = childNodes.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                visitElement(context, (Element) child);
            }
        }

        // Post hooks
        if (elementChecks != null) {
            for (XmlScanner check : elementChecks) {
                check.visitElementAfter(context, element);
            }
        }
        if (!mAllElementDetectors.isEmpty()) {
            for (XmlScanner check : mAllElementDetectors) {
                check.visitElementAfter(context, element);
            }
        }
    }

    @NonNull
    public XmlParser getParser() {
        return mParser;
    }

    public void visitBinaryResource(@NonNull ResourceContext context) {
        if (mBinaryDetectors == null) {
            return;
        }
        for (Detector check : mBinaryDetectors) {
            check.beforeCheckFile(context);
            check.checkBinaryResource(context);
            check.afterCheckFile(context);
        }
    }
}
