/*
 * 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.detector.api;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.client.api.LintDriver;
import com.android.tools.lint.client.api.XmlParser;
import com.google.common.annotations.Beta;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

import java.io.File;

/**
 * A {@link Context} used when checking XML files.
 * <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
public class XmlContext extends ResourceContext {
    static final String SUPPRESS_COMMENT_PREFIX = "<!--suppress "; //$NON-NLS-1$

    /** The XML parser */
    private final XmlParser mParser;
    /** The XML document */
    public Document document;

    /**
     * Construct a new {@link XmlContext}
     *
     * @param driver the driver running through the checks
     * @param project the project containing the file being checked
     * @param main the main project if this project is a library project, or
     *            null if this is not a library project. The main project is
     *            the root project of all library projects, not necessarily the
     *            directly including project.
     * @param file the file being checked
     * @param folderType the {@link ResourceFolderType} of this file, if any
     */
    public XmlContext(
            @NonNull LintDriver driver,
            @NonNull Project project,
            @Nullable Project main,
            @NonNull File file,
            @Nullable ResourceFolderType folderType,
            @NonNull XmlParser parser) {
        super(driver, project, main, file, folderType);
        mParser = parser;
    }

    /**
     * Returns the location for the given node, which may be an element or an attribute.
     *
     * @param node the node to look up the location for
     * @return the location for the node
     */
    @NonNull
    public Location getLocation(@NonNull Node node) {
        return mParser.getLocation(this, node);
    }

    /**
     * Returns the location for name-portion of the given element or attribute.
     *
     * @param node the node to look up the location for
     * @return the location for the node
     */
    @NonNull
    public Location getNameLocation(@NonNull Node node) {
        return mParser.getNameLocation(this, node);
    }

    /**
     * Returns the location for value-portion of the given attribute
     *
     * @param node the node to look up the location for
     * @return the location for the node
     */
    @NonNull
    public Location getValueLocation(@NonNull Attr node) {
        return mParser.getValueLocation(this, node);
    }

    /**
     * Creates a new location within an XML text node
     *
     * @param textNode the text node
     * @param begin the start offset within the text node (inclusive)
     * @param end the end offset within the text node (exclusive)
     * @return a new location
     */
    @NonNull
    public Location getLocation(@NonNull Node textNode, int begin, int end) {
        assert textNode.getNodeType() == Node.TEXT_NODE;
        return mParser.getLocation(this, textNode, begin, end);
    }

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

    /**
     * Reports an issue applicable to a given DOM node. The DOM node is used as the
     * scope to check for suppress lint annotations.
     *
     * @param issue the issue to report
     * @param scope the DOM node scope the error applies to. The lint infrastructure
     *    will check whether there are suppress directives on this node (or its enclosing
     *    nodes) and if so suppress the warning without involving the client.
     * @param location the location of the issue, or null if not known
     * @param message the message for this warning
     */
    public void report(
            @NonNull Issue issue,
            @Nullable Node scope,
            @NonNull Location location,
            @NonNull String message) {
        if (scope != null && mDriver.isSuppressed(this, issue, scope)) {
            return;
        }
        super.report(issue, location, message);
    }

    /**
     * Report an error.
     * Like {@link #report(Issue, org.w3c.dom.Node, Location, String)} but with
     * a now-unused data parameter at the end.
     *
     * @deprecated Use {@link #report(Issue, org.w3c.dom.Node, Location, String)} instead;
     *    this method is here for custom rule compatibility
     */
    @SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
    @Deprecated
    public void report(
            @NonNull Issue issue,
            @Nullable Node scope,
            @NonNull Location location,
            @NonNull String message,
            @SuppressWarnings("UnusedParameters") @Nullable Object data) {
        report(issue, scope, location, message);
    }

    @Override
    public void report(
            @NonNull Issue issue,
            @NonNull Location location,
            @NonNull String message) {
        // Warn if clients use the non-scoped form? No, there are cases where an
        //  XML detector's error isn't applicable to one particular location (or it's
        //  not feasible to compute it cheaply)
        //mDriver.getClient().log(null, "Warning: Issue " + issue
        //        + " was reported without a scope node: Can't be suppressed.");

        // For now just check the document root itself
        if (document != null && mDriver.isSuppressed(this, issue, document)) {
            return;
        }

        super.report(issue, location, message);
    }

    @Override
    @Nullable
    protected String getSuppressCommentPrefix() {
        return SUPPRESS_COMMENT_PREFIX;
    }

    public boolean isSuppressedWithComment(@NonNull Node node, @NonNull Issue issue) {
        // Check whether there is a comment marker
        String contents = getContents();
        assert contents != null; // otherwise we wouldn't be here

        int start = mParser.getNodeStartOffset(this, node);
        if (start != -1) {
            return isSuppressedWithComment(start, issue);
        }

        return false;
    }

    @NonNull
    public Location.Handle createLocationHandle(@NonNull Node node) {
        return mParser.createLocationHandle(this, node);
    }
}
