/*
 * 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 static com.android.tools.lint.detector.api.TextFormat.RAW;

import com.android.annotations.NonNull;
import com.android.tools.lint.client.api.Configuration;
import com.google.common.annotations.Beta;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;


/**
 * An issue is a potential bug in an Android application. An issue is discovered
 * by a {@link Detector}, and has an associated {@link Severity}.
 * <p>
 * Issues and detectors are separate classes because a detector can discover
 * multiple different issues as it's analyzing code, and we want to be able to
 * different severities for different issues, the ability to suppress one but
 * not other issues from the same detector, and so on.
 * <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 final class Issue implements Comparable<Issue> {
    private final String mId;
    private final String mBriefDescription;
    private final String mExplanation;
    private final Category mCategory;
    private final int mPriority;
    private final Severity mSeverity;
    private Object mMoreInfoUrls;
    private boolean mEnabledByDefault = true;
    private Implementation mImplementation;

    // Use factory methods
    private Issue(
            @NonNull String id,
            @NonNull String shortDescription,
            @NonNull String explanation,
            @NonNull Category category,
            int priority,
            @NonNull Severity severity,
            @NonNull Implementation implementation) {
        assert !shortDescription.isEmpty();
        assert !explanation.isEmpty();

        mId = id;
        mBriefDescription = shortDescription;
        mExplanation = explanation;
        mCategory = category;
        mPriority = priority;
        mSeverity = severity;
        mImplementation = implementation;
    }

    /**
     * Creates a new issue. The description strings can use some simple markup;
     * see the {@link TextFormat#RAW} documentation
     * for details.
     *
     * @param id the fixed id of the issue
     * @param briefDescription short summary (typically 5-6 words or less), typically
     *                         describing the <b>problem</b> rather than the <b>fix</b>
     *                         (e.g. "Missing minSdkVersion")
     * @param explanation a full explanation of the issue, with suggestions for
     *            how to fix it
     * @param category the associated category, if any
     * @param priority the priority, a number from 1 to 10 with 10 being most
     *            important/severe
     * @param severity the default severity of the issue
     * @param implementation the default implementation for this issue
     * @return a new {@link Issue}
     */
    @NonNull
    public static Issue create(
            @NonNull String id,
            @NonNull String briefDescription,
            @NonNull String explanation,
            @NonNull Category category,
            int priority,
            @NonNull Severity severity,
            @NonNull Implementation implementation) {
        return new Issue(id, briefDescription, explanation, category, priority,
                severity, implementation);
    }

    /**
     * For compatibility with older custom rules)
     *
     * @deprecated Use {@link #create(String, String, String, Category, int, Severity, Implementation)} instead
     */
    @NonNull
    @Deprecated
    public static Issue create(
            @NonNull String id,
            @NonNull String briefDescription,
            @SuppressWarnings("UnusedParameters") @NonNull String description,
            @NonNull String explanation,
            @NonNull Category category,
            int priority,
            @NonNull Severity severity,
            @NonNull Implementation implementation) {
        return new Issue(id, briefDescription, explanation, category, priority,
                severity, implementation);
    }

    /**
     * Returns the unique id of this issue. These should not change over time
     * since they are used to persist the names of issues suppressed by the user
     * etc. It is typically a single camel-cased word.
     *
     * @return the associated fixed id, never null and always unique
     */
    @NonNull
    public String getId() {
        return mId;
    }

    /**
     * Briefly (in a couple of words) describes these errors
     *
     * @return a brief summary of the issue, never null, never empty
     */
    @NonNull
    public String getBriefDescription(@NonNull TextFormat format) {
        return RAW.convertTo(mBriefDescription, format);
    }

    /**
     * Describes the error found by this rule, e.g.
     * "Buttons must define contentDescriptions". Preferably the explanation
     * should also contain a description of how the problem should be solved.
     * Additional info can be provided via {@link #getMoreInfo()}.
     *
     * @param format the format to write the format as
     * @return an explanation of the issue, never null, never empty
     */
    @NonNull
    public String getExplanation(@NonNull TextFormat format) {
        return RAW.convertTo(mExplanation, format);
    }

    /**
     * The primary category of the issue
     *
     * @return the primary category of the issue, never null
     */
    @NonNull
    public Category getCategory() {
        return mCategory;
    }

    /**
     * Returns a priority, in the range 1-10, with 10 being the most severe and
     * 1 the least
     *
     * @return a priority from 1 to 10
     */
    public int getPriority() {
        return mPriority;
    }

    /**
     * Returns the default severity of the issues found by this detector (some
     * tools may allow the user to specify custom severities for detectors).
     * <p>
     * Note that even though the normal way for an issue to be disabled is for
     * the {@link Configuration} to return {@link Severity#IGNORE}, there is a
     * {@link #isEnabledByDefault()} method which can be used to turn off issues
     * by default. This is done rather than just having the severity as the only
     * attribute on the issue such that an issue can be configured with an
     * appropriate severity (such as {@link Severity#ERROR}) even when issues
     * are disabled by default for example because they are experimental or not
     * yet stable.
     *
     * @return the severity of the issues found by this detector
     */
    @NonNull
    public Severity getDefaultSeverity() {
        return mSeverity;
    }

    /**
     * Returns a link (a URL string) to more information, or null
     *
     * @return a link to more information, or null
     */
    @NonNull
    public List<String> getMoreInfo() {
        if (mMoreInfoUrls == null) {
            return Collections.emptyList();
        } else if (mMoreInfoUrls instanceof String) {
            return Collections.singletonList((String) mMoreInfoUrls);
        } else {
            assert mMoreInfoUrls instanceof List;
            //noinspection unchecked
            return (List<String>) mMoreInfoUrls;
        }
    }

    /**
     * Adds a more info URL string
     *
     * @param moreInfoUrl url string
     * @return this, for constructor chaining
     */
    @NonNull
    public Issue addMoreInfo(@NonNull String moreInfoUrl) {
        // Nearly all issues supply at most a single URL, so don't bother with
        // lists wrappers for most of these issues
        if (mMoreInfoUrls == null) {
            mMoreInfoUrls = moreInfoUrl;
        } else if (mMoreInfoUrls instanceof String) {
            String existing = (String) mMoreInfoUrls;
            List<String> list = new ArrayList<String>(2);
            list.add(existing);
            list.add(moreInfoUrl);
            mMoreInfoUrls = list;
        } else {
            assert mMoreInfoUrls instanceof List;
            //noinspection unchecked
            ((List<String>) mMoreInfoUrls).add(moreInfoUrl);
        }
        return this;
    }

    /**
     * Returns whether this issue should be enabled by default, unless the user
     * has explicitly disabled it.
     *
     * @return true if this issue should be enabled by default
     */
    public boolean isEnabledByDefault() {
        return mEnabledByDefault;
    }

    /**
     * Returns the implementation for the given issue
     *
     * @return the implementation for this issue
     */
    @NonNull
    public Implementation getImplementation() {
        return mImplementation;
    }

    /**
     * Sets the implementation for the given issue. This is typically done by
     * IDEs that can offer a replacement for a given issue which performs better
     * or in some other way works better within the IDE.
     *
     * @param implementation the new implementation to use
     */
    public void setImplementation(@NonNull Implementation implementation) {
        mImplementation = implementation;
    }

    /**
     * Sorts the detectors alphabetically by id. This is intended to make it
     * convenient to store settings for detectors in a fixed order. It is not
     * intended as the order to be shown to the user; for that, a tool embedding
     * lint might consider the priorities, categories, severities etc of the
     * various detectors.
     *
     * @param other the {@link Issue} to compare this issue to
     */
    @Override
    public int compareTo(@NonNull Issue other) {
        return getId().compareTo(other.getId());
    }

    /**
     * Sets whether this issue is enabled by default.
     *
     * @param enabledByDefault whether the issue should be enabled by default
     * @return this, for constructor chaining
     */
    @NonNull
    public Issue setEnabledByDefault(boolean enabledByDefault) {
        mEnabledByDefault = enabledByDefault;
        return this;
    }

    @Override
    public String toString() {
        return mId;
    }
}
