/*
 * 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;

import static com.android.SdkConstants.CURRENT_PLATFORM;
import static com.android.SdkConstants.DOT_9PNG;
import static com.android.SdkConstants.DOT_PNG;
import static com.android.SdkConstants.PLATFORM_LINUX;
import static com.android.SdkConstants.UTF_8;
import static com.android.tools.lint.detector.api.LintUtils.endsWith;
import static java.io.File.separatorChar;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.checks.AccessibilityDetector;
import com.android.tools.lint.checks.AlwaysShowActionDetector;
import com.android.tools.lint.checks.AndroidAutoDetector;
import com.android.tools.lint.checks.AndroidTvDetector;
import com.android.tools.lint.checks.AnnotationDetector;
import com.android.tools.lint.checks.ApiDetector;
import com.android.tools.lint.checks.AppCompatCallDetector;
import com.android.tools.lint.checks.AppIndexingApiDetector;
import com.android.tools.lint.checks.ByteOrderMarkDetector;
import com.android.tools.lint.checks.CleanupDetector;
import com.android.tools.lint.checks.CommentDetector;
import com.android.tools.lint.checks.DetectMissingPrefix;
import com.android.tools.lint.checks.DosLineEndingDetector;
import com.android.tools.lint.checks.DuplicateResourceDetector;
import com.android.tools.lint.checks.GradleDetector;
import com.android.tools.lint.checks.GridLayoutDetector;
import com.android.tools.lint.checks.HardcodedValuesDetector;
import com.android.tools.lint.checks.IncludeDetector;
import com.android.tools.lint.checks.InefficientWeightDetector;
import com.android.tools.lint.checks.JavaPerformanceDetector;
import com.android.tools.lint.checks.ManifestDetector;
import com.android.tools.lint.checks.MissingClassDetector;
import com.android.tools.lint.checks.MissingIdDetector;
import com.android.tools.lint.checks.NamespaceDetector;
import com.android.tools.lint.checks.ObsoleteLayoutParamsDetector;
import com.android.tools.lint.checks.ParcelDetector;
import com.android.tools.lint.checks.PropertyFileDetector;
import com.android.tools.lint.checks.PxUsageDetector;
import com.android.tools.lint.checks.ReadParcelableDetector;
import com.android.tools.lint.checks.RtlDetector;
import com.android.tools.lint.checks.ScrollViewChildDetector;
import com.android.tools.lint.checks.SecurityDetector;
import com.android.tools.lint.checks.SignatureOrSystemDetector;
import com.android.tools.lint.checks.SupportAnnotationDetector;
import com.android.tools.lint.checks.TextFieldDetector;
import com.android.tools.lint.checks.TextViewDetector;
import com.android.tools.lint.checks.TitleDetector;
import com.android.tools.lint.checks.TranslationDetector;
import com.android.tools.lint.checks.TypoDetector;
import com.android.tools.lint.checks.TypographyDetector;
import com.android.tools.lint.checks.UseCompoundDrawableDetector;
import com.android.tools.lint.checks.UselessViewDetector;
import com.android.tools.lint.checks.Utf8Detector;
import com.android.tools.lint.checks.WrongCallDetector;
import com.android.tools.lint.checks.WrongCaseDetector;
import com.android.tools.lint.detector.api.Issue;
import com.android.utils.SdkUtils;
import com.google.common.annotations.Beta;
import com.google.common.collect.Sets;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import com.google.common.io.Files;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

/** A reporter is an output generator for lint warnings
 * <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 abstract class Reporter {
    protected final LintCliClient mClient;
    protected final File mOutput;
    protected String mTitle = "Lint Report";
    protected boolean mSimpleFormat;
    protected boolean mBundleResources;
    protected Map<String, String> mUrlMap;
    protected File mResources;
    protected final Map<File, String> mResourceUrl = new HashMap<File, String>();
    protected final Map<String, File> mNameToFile = new HashMap<String, File>();
    protected boolean mDisplayEmpty = true;

    /**
     * Write the given warnings into the report
     *
     * @param errorCount the number of errors
     * @param warningCount the number of warnings
     * @param issues the issues to be reported
     * @throws IOException if an error occurs
     */
    public abstract void write(int errorCount, int warningCount, List<Warning> issues)
            throws IOException;

    protected Reporter(LintCliClient client, File output) {
        mClient = client;
        mOutput = output;
    }

    /**
     * Sets the report title
     *
     * @param title the title of the report
     */
    public void setTitle(String title) {
        mTitle = title;
    }

    /** @return the title of the report */
    public String getTitle() {
        return mTitle;
    }

    /**
     * Sets whether the report should bundle up resources along with the HTML report.
     * This implies a non-simple format (see {@link #setSimpleFormat(boolean)}).
     *
     * @param bundleResources if true, copy images into a directory relative to
     *            the report
     */
    public void setBundleResources(boolean bundleResources) {
        mBundleResources = bundleResources;
        mSimpleFormat = false;
    }

    /**
     * Sets whether the report should use simple formatting (meaning no JavaScript,
     * embedded images, etc).
     *
     * @param simpleFormat whether the formatting should be simple
     */
    public void setSimpleFormat(boolean simpleFormat) {
        mSimpleFormat = simpleFormat;
    }

    /**
     * Returns whether the report should use simple formatting (meaning no JavaScript,
     * embedded images, etc).
     *
     * @return whether the report should use simple formatting
     */
    public boolean isSimpleFormat() {
        return mSimpleFormat;
    }


    String getUrl(File file) {
        if (mBundleResources && !mSimpleFormat) {
            String url = getRelativeResourceUrl(file);
            if (url != null) {
                return url;
            }
        }

        if (mUrlMap != null) {
            String path = file.getAbsolutePath();
            // Perform the comparison using URLs such that we properly escape spaces etc.
            String pathUrl = encodeUrl(path);
            for (Map.Entry<String, String> entry : mUrlMap.entrySet()) {
                String prefix = entry.getKey();
                String prefixUrl = encodeUrl(prefix);
                if (pathUrl.startsWith(prefixUrl)) {
                    String relative = pathUrl.substring(prefixUrl.length());
                    return entry.getValue() + relative;
                }
            }
        }

        if (file.isAbsolute()) {
            String relativePath = getRelativePath(mOutput.getParentFile(), file);
            if (relativePath != null) {
                relativePath = relativePath.replace(separatorChar, '/');
                return encodeUrl(relativePath);
            }
        }

        try {
            return SdkUtils.fileToUrlString(file);
        } catch (MalformedURLException e) {
            return null;
        }
    }

    /** Encodes the given String as a safe URL substring, escaping spaces etc */
    static String encodeUrl(String url) {
        try {
            url = url.replace('\\', '/');
            return URLEncoder.encode(url, UTF_8).replace("%2F", "/");         //$NON-NLS-1$
        } catch (UnsupportedEncodingException e) {
            // This shouldn't happen for UTF-8
            System.err.println("Invalid string " + e.getLocalizedMessage());
            return url;
        }
    }

    /** Set mapping of path prefixes to corresponding URLs in the HTML report */
    public void setUrlMap(@Nullable Map<String, String> urlMap) {
        mUrlMap = urlMap;
    }

    /** Gets a pointer to the local resource directory, if any */
    File getResourceDir() {
        if (mResources == null && mBundleResources) {
            mResources = computeResourceDir();
            if (mResources == null) {
                mBundleResources = false;
            }
        }

        return mResources;
    }

    /** Finds/creates the local resource directory, if possible */
    File computeResourceDir() {
        String fileName = mOutput.getName();
        int dot = fileName.indexOf('.');
        if (dot != -1) {
            fileName = fileName.substring(0, dot);
        }

        File resources = new File(mOutput.getParentFile(), fileName + "_files"); //$NON-NLS-1$
        if (!resources.exists() && !resources.mkdir()) {
            resources = null;
        }

        return resources;
    }

    /** Returns a URL to a local copy of the given file, or null */
    protected String getRelativeResourceUrl(File file) {
        String resource = mResourceUrl.get(file);
        if (resource != null) {
            return resource;
        }

        String name = file.getName();
        if (!endsWith(name, DOT_PNG) || endsWith(name, DOT_9PNG)) {
            return null;
        }

        // Attempt to make local copy
        File resourceDir = getResourceDir();
        if (resourceDir != null) {
            String base = file.getName();

            File path = mNameToFile.get(base);
            if (path != null && !path.equals(file)) {
                // That filename already exists and is associated with a different path:
                // make a new unique version
                for (int i = 0; i < 100; i++) {
                    base = '_' + base;
                    path = mNameToFile.get(base);
                    if (path == null || path.equals(file)) {
                        break;
                    }
                }
            }

            File target = new File(resourceDir, base);
            try {
                Files.copy(file, target);
            } catch (IOException e) {
                return null;
            }
            return resourceDir.getName() + '/' + encodeUrl(base);
        }
        return null;
    }

    /** Returns a URL to a local copy of the given resource, or null. There is
     * no filename conflict resolution. */
    protected String addLocalResources(URL url) throws IOException {
        // Attempt to make local copy
        File resourceDir = computeResourceDir();
        if (resourceDir != null) {
            String base = url.getFile();
            base = base.substring(base.lastIndexOf('/') + 1);
            mNameToFile.put(base, new File(url.toExternalForm()));

            File target = new File(resourceDir, base);
            Closer closer = Closer.create();
            try {
                FileOutputStream output = closer.register(new FileOutputStream(target));
                InputStream input = closer.register(url.openStream());
                ByteStreams.copy(input, output);
            } catch (Throwable e) {
                //noinspection ThrowableResultOfMethodCallIgnored
                closer.rethrow(e);
            } finally {
                closer.close();
            }
            return resourceDir.getName() + '/' + encodeUrl(base);
        }
        return null;
    }

    // Based on similar code in com.intellij.openapi.util.io.FileUtilRt
    @Nullable
    static String getRelativePath(File base, File file) {
        if (base == null || file == null) {
            return null;
        }
        if (!base.isDirectory()) {
            base = base.getParentFile();
            if (base == null) {
                return null;
            }
        }
        if (base.equals(file)) {
            return ".";
        }

        final String filePath = file.getAbsolutePath();
        String basePath = base.getAbsolutePath();

        // TODO: Make this return null if we go all the way to the root!

        basePath = !basePath.isEmpty() && basePath.charAt(basePath.length() - 1) == separatorChar
                ? basePath : basePath + separatorChar;

        // Whether filesystem is case sensitive. Technically on OSX you could create a
        // sensitive one, but it's not the default.
        boolean caseSensitive = CURRENT_PLATFORM == PLATFORM_LINUX;
        Locale l = Locale.getDefault();
        String basePathToCompare = caseSensitive ? basePath : basePath.toLowerCase(l);
        String filePathToCompare = caseSensitive ? filePath : filePath.toLowerCase(l);
        if (basePathToCompare.equals(!filePathToCompare.isEmpty()
                && filePathToCompare.charAt(filePathToCompare.length() - 1) == separatorChar
                ? filePathToCompare : filePathToCompare + separatorChar)) {
            return ".";
        }
        int len = 0;
        int lastSeparatorIndex = 0;
        // bug in inspection; see http://youtrack.jetbrains.com/issue/IDEA-118971
        //noinspection ConstantConditions
        while (len < filePath.length() && len < basePath.length()
                && filePathToCompare.charAt(len) == basePathToCompare.charAt(len)) {
            if (basePath.charAt(len) == separatorChar) {
                lastSeparatorIndex = len;
            }
            len++;
        }
        if (len == 0) {
            return null;
        }

        StringBuilder relativePath = new StringBuilder();
        for (int i = len; i < basePath.length(); i++) {
            if (basePath.charAt(i) == separatorChar) {
                relativePath.append("..");
                relativePath.append(separatorChar);
            }
        }
        relativePath.append(filePath.substring(lastSeparatorIndex + 1));
        return relativePath.toString();
    }

    /**
     * Returns whether this report should display info (such as a path to the report) if
     * no issues were found
     */
    public boolean isDisplayEmpty() {
        return mDisplayEmpty;
    }

    /**
     * Sets whether this report should display info (such as a path to the report) if
     * no issues were found
     */
    public void setDisplayEmpty(boolean displayEmpty) {
        mDisplayEmpty = displayEmpty;
    }

    private static Set<Issue> sAdtFixes;
    private static Set<Issue> sStudioFixes;

    /** Tools known to have quickfixes for lint */
    enum QuickfixHandler {
        /** Android Studio or IntelliJ */
        STUDIO,
        /** Eclipse */
        ADT;

        public boolean hasAutoFix(Issue issue) {
            return Reporter.hasAutoFix(this, issue);
        }
    }

    /**
     * Returns true if the given issue has an automatic IDE fix.
     *
     * @param tool the name of the tool to be checked
     * @param issue the issue to be checked
     * @return true if the given tool is known to have an automatic fix for the
     *         given issue
     */
    public static boolean hasAutoFix(@NonNull QuickfixHandler tool, Issue issue) {
        if (tool == QuickfixHandler.ADT) {
            if (sAdtFixes == null) {
                sAdtFixes = Sets.newHashSet(
                        InefficientWeightDetector.INEFFICIENT_WEIGHT,
                        AccessibilityDetector.ISSUE,
                        InefficientWeightDetector.BASELINE_WEIGHTS,
                        HardcodedValuesDetector.ISSUE,
                        UselessViewDetector.USELESS_LEAF,
                        UselessViewDetector.USELESS_PARENT,
                        PxUsageDetector.PX_ISSUE,
                        TextFieldDetector.ISSUE,
                        SecurityDetector.EXPORTED_SERVICE,
                        DetectMissingPrefix.MISSING_NAMESPACE,
                        ScrollViewChildDetector.ISSUE,
                        ObsoleteLayoutParamsDetector.ISSUE,
                        TypographyDetector.DASHES,
                        TypographyDetector.ELLIPSIS,
                        TypographyDetector.FRACTIONS,
                        TypographyDetector.OTHER,
                        TypographyDetector.QUOTES,
                        UseCompoundDrawableDetector.ISSUE,
                        ApiDetector.UNSUPPORTED,
                        ApiDetector.INLINED,
                        TypoDetector.ISSUE,
                        ManifestDetector.ALLOW_BACKUP,
                        MissingIdDetector.ISSUE,
                        TranslationDetector.MISSING,
                        DosLineEndingDetector.ISSUE
                );
            }
            return sAdtFixes.contains(issue);
        } else if (tool == QuickfixHandler.STUDIO) {
            // List generated by AndroidLintInspectionToolProviderTest in tools/adt/idea;
            // set LIST_ISSUES_WITH_QUICK_FIXES to true
            if (sStudioFixes == null) {
                sStudioFixes = Sets.newHashSet(
                        AccessibilityDetector.ISSUE,
                        AlwaysShowActionDetector.ISSUE,
                        AndroidAutoDetector.INVALID_USES_TAG_ISSUE,
                        AndroidTvDetector.MISSING_BANNER,
                        AndroidTvDetector.MISSING_LEANBACK_SUPPORT,
                        AndroidTvDetector.PERMISSION_IMPLIES_UNSUPPORTED_HARDWARE,
                        AndroidTvDetector.UNSUPPORTED_TV_HARDWARE,
                        AnnotationDetector.SWITCH_TYPE_DEF,
                        ApiDetector.INLINED,
                        ApiDetector.OVERRIDE,
                        ApiDetector.UNSUPPORTED,
                        ApiDetector.UNUSED,
                        AppCompatCallDetector.ISSUE,
                        AppIndexingApiDetector.ISSUE_APP_INDEXING,
                        AppIndexingApiDetector.ISSUE_APP_INDEXING_API,
                        AppIndexingApiDetector.ISSUE_URL_ERROR,
                        ByteOrderMarkDetector.BOM,
                        CleanupDetector.SHARED_PREF,
                        CommentDetector.STOP_SHIP,
                        DetectMissingPrefix.MISSING_NAMESPACE,
                        DuplicateResourceDetector.TYPE_MISMATCH,
                        GradleDetector.COMPATIBILITY,
                        GradleDetector.DEPENDENCY,
                        GradleDetector.DEPRECATED,
                        GradleDetector.NOT_INTERPOLATED,
                        GradleDetector.PLUS,
                        GradleDetector.REMOTE_VERSION,
                        GradleDetector.STRING_INTEGER,
                        GridLayoutDetector.ISSUE,
                        IncludeDetector.ISSUE,
                        InefficientWeightDetector.BASELINE_WEIGHTS,
                        InefficientWeightDetector.INEFFICIENT_WEIGHT,
                        InefficientWeightDetector.ORIENTATION,
                        JavaPerformanceDetector.USE_VALUE_OF,
                        ManifestDetector.ALLOW_BACKUP,
                        ManifestDetector.APPLICATION_ICON,
                        ManifestDetector.MIPMAP,
                        ManifestDetector.MOCK_LOCATION,
                        ManifestDetector.TARGET_NEWER,
                        MissingClassDetector.INNERCLASS,
                        MissingIdDetector.ISSUE,
                        NamespaceDetector.RES_AUTO,
                        ObsoleteLayoutParamsDetector.ISSUE,
                        ParcelDetector.ISSUE,
                        PropertyFileDetector.ESCAPE,
                        PropertyFileDetector.HTTP,
                        PxUsageDetector.DP_ISSUE,
                        PxUsageDetector.PX_ISSUE,
                        ReadParcelableDetector.ISSUE,
                        RtlDetector.COMPAT,
                        ScrollViewChildDetector.ISSUE,
                        SecurityDetector.EXPORTED_SERVICE,
                        SignatureOrSystemDetector.ISSUE,
                        SupportAnnotationDetector.CHECK_PERMISSION,
                        SupportAnnotationDetector.CHECK_RESULT,
                        SupportAnnotationDetector.MISSING_PERMISSION,
                        TextFieldDetector.ISSUE,
                        TextViewDetector.SELECTABLE,
                        TitleDetector.ISSUE,
                        TypoDetector.ISSUE,
                        TypographyDetector.DASHES,
                        TypographyDetector.ELLIPSIS,
                        TypographyDetector.FRACTIONS,
                        TypographyDetector.OTHER,
                        TypographyDetector.QUOTES,
                        UselessViewDetector.USELESS_LEAF,
                        Utf8Detector.ISSUE,
                        WrongCallDetector.ISSUE,
                        WrongCaseDetector.WRONG_CASE
                );
            }
            return sStudioFixes.contains(issue);
        }

        return false;
    }
}
