/*
 * 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.DOT_JPG;
import static com.android.SdkConstants.DOT_PNG;
import static com.android.SdkConstants.VALUE_FALSE;
import static com.android.tools.lint.detector.api.LintUtils.endsWith;
import static com.android.tools.lint.detector.api.TextFormat.HTML;
import static com.android.tools.lint.detector.api.TextFormat.RAW;

import com.android.tools.lint.checks.BuiltinIssueRegistry;
import com.android.tools.lint.client.api.Configuration;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Position;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.TextFormat;
import com.android.utils.SdkUtils;
import com.google.common.annotations.Beta;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closeables;
import com.google.common.io.Files;

import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A reporter which emits lint results into an HTML report.
 * <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 HtmlReporter extends Reporter {
    public static boolean INLINE_RESOURCES =
            !VALUE_FALSE.equals(System.getProperty("lint.inline-resources"));
    private static final boolean USE_HOLO_STYLE = true;
    @SuppressWarnings("ConstantConditions")
    private static final String CSS = USE_HOLO_STYLE
            ? "hololike.css" : "default.css"; //$NON-NLS-1$ //$NON-NLS-2$

    /**
     * Maximum number of warnings allowed for a single issue type before we
     * split up and hide all but the first {@link #SHOWN_COUNT} items.
     */
    private static final int SPLIT_LIMIT = 8;
    /**
     * When a warning has at least {@link #SPLIT_LIMIT} items, then we show the
     * following number of items before the "Show more" button/link.
     */
    private static final int SHOWN_COUNT = SPLIT_LIMIT - 3;

    protected final Writer mWriter;
    private String mStripPrefix;
    private String mFixUrl;

    /**
     * Creates a new {@link HtmlReporter}
     *
     * @param client the associated client
     * @param output the output file
     * @throws IOException if an error occurs
     */
    public HtmlReporter(LintCliClient client, File output) throws IOException {
        super(client, output);
        mWriter = new BufferedWriter(Files.newWriter(output, Charsets.UTF_8));
    }

    @Override
    public void write(int errorCount, int warningCount, List<Warning> issues) throws IOException {
        Map<Issue, String> missing = computeMissingIssues(issues);

        mWriter.write(
                "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n" + //$NON-NLS-1$
                "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n" +      //$NON-NLS-1$
                "<head>\n" +                                             //$NON-NLS-1$
                "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />" + //$NON-NLS-1$
                "<title>" + mTitle + "</title>\n");                      //$NON-NLS-1$//$NON-NLS-2$

        writeStyleSheet();

        if (!mSimpleFormat) {
            // JavaScript for collapsing/expanding long lists
            mWriter.write(
                "<script language=\"javascript\" type=\"text/javascript\"> \n" + //$NON-NLS-1$
                "<!--\n" +                                               //$NON-NLS-1$
                "function reveal(id) {\n" +                              //$NON-NLS-1$
                "if (document.getElementById) {\n" +                     //$NON-NLS-1$
                "document.getElementById(id).style.display = 'block';\n" +       //$NON-NLS-1$
                "document.getElementById(id+'Link').style.display = 'none';\n" + //$NON-NLS-1$
                "}\n" +                                                  //$NON-NLS-1$
                "}\n" +                                                  //$NON-NLS-1$
                "//--> \n" +                                             //$NON-NLS-1$
                "</script>\n");                                          //$NON-NLS-1$
        }

        mWriter.write(
                "</head>\n" +                                            //$NON-NLS-1$
                "<body>\n" +                                             //$NON-NLS-1$
                "<h1>" +                                                 //$NON-NLS-1$
                mTitle +
                "</h1>\n" +                                              //$NON-NLS-1$
                "<div class=\"titleSeparator\"></div>\n");               //$NON-NLS-1$

        mWriter.write(String.format("Check performed at %1$s.",
                new Date().toString()));
        mWriter.write("<br/>\n");                                        //$NON-NLS-1$
        mWriter.write(String.format("%1$d errors and %2$d warnings found:",
                errorCount, warningCount));
        mWriter.write("<br/><br/>\n");                                   //$NON-NLS-1$

        Issue previousIssue = null;
        if (!issues.isEmpty()) {
            List<List<Warning>> related = new ArrayList<List<Warning>>();
            List<Warning> currentList = null;
            for (Warning warning : issues) {
                if (warning.issue != previousIssue) {
                    previousIssue = warning.issue;
                    currentList = new ArrayList<Warning>();
                    related.add(currentList);
                }
                assert currentList != null;
                currentList.add(warning);
            }

            writeOverview(related, missing.size());

            Category previousCategory = null;
            for (List<Warning> warnings : related) {
                Warning first = warnings.get(0);
                Issue issue = first.issue;

                if (issue.getCategory() != previousCategory) {
                    previousCategory = issue.getCategory();
                    mWriter.write("\n<a name=\"");                       //$NON-NLS-1$
                    mWriter.write(issue.getCategory().getFullName());
                    mWriter.write("\"></a>\n");                          //$NON-NLS-1$
                    mWriter.write("<div class=\"category\"><a href=\"#\" title=\"Return to top\">");           //$NON-NLS-1$
                    mWriter.write(issue.getCategory().getFullName());
                    mWriter.write("</a><div class=\"categorySeparator\"></div>\n");//$NON-NLS-1$
                    mWriter.write("</div>\n");                           //$NON-NLS-1$
                }

                mWriter.write("<a name=\"" + issue.getId() + "\"></a>\n"); //$NON-NLS-1$ //$NON-NLS-2$
                mWriter.write("<div class=\"issue\">\n");                //$NON-NLS-1$

                // Explain this issue
                mWriter.write("<div class=\"id\"><a href=\"#\" title=\"Return to top\">");                     //$NON-NLS-1$
                mWriter.write(issue.getId());
                mWriter.write(": ");                                     //$NON-NLS-1$
                mWriter.write(issue.getBriefDescription(HTML));
                mWriter.write("</a><div class=\"issueSeparator\"></div>\n"); //$NON-NLS-1$
                mWriter.write("</div>\n");                               //$NON-NLS-1$

                mWriter.write("<div class=\"warningslist\">\n");         //$NON-NLS-1$
                boolean partialHide = !mSimpleFormat && warnings.size() > SPLIT_LIMIT;

                int count = 0;
                for (Warning warning : warnings) {
                    if (partialHide && count == SHOWN_COUNT) {
                        String id = warning.issue.getId() + "Div";       //$NON-NLS-1$
                        mWriter.write("<button id=\"");                  //$NON-NLS-1$
                        mWriter.write(id);
                        mWriter.write("Link\" onclick=\"reveal('");      //$NON-NLS-1$
                        mWriter.write(id);
                        mWriter.write("');\" />");                       //$NON-NLS-1$
                        mWriter.write(String.format("+ %1$d More Occurrences...",
                                warnings.size() - SHOWN_COUNT));
                        mWriter.write("</button>\n");                    //$NON-NLS-1$
                        mWriter.write("<div id=\"");                     //$NON-NLS-1$
                        mWriter.write(id);
                        mWriter.write("\" style=\"display: none\">\n");  //$NON-NLS-1$
                    }
                    count++;
                    String url = null;
                    if (warning.path != null) {
                        url = writeLocation(warning.file, warning.path, warning.line);
                        mWriter.write(':');
                        mWriter.write(' ');
                    }

                    // Is the URL for a single image? If so, place it here near the top
                    // of the error floating on the right. If there are multiple images,
                    // they will instead be placed in a horizontal box below the error
                    boolean addedImage = false;
                    if (url != null && warning.location != null
                            && warning.location.getSecondary() == null) {
                        addedImage = addImage(url, warning.location);
                    }
                    mWriter.write("<span class=\"message\">");           //$NON-NLS-1$
                    mWriter.append(RAW.convertTo(warning.message, HTML));
                    mWriter.write("</span>");                            //$NON-NLS-1$
                    if (addedImage) {
                        mWriter.write("<br clear=\"right\"/>");          //$NON-NLS-1$
                    } else {
                        mWriter.write("<br />");                         //$NON-NLS-1$
                    }

                    // Insert surrounding code block window
                    if (warning.line >= 0 && warning.fileContents != null) {
                        mWriter.write("<pre class=\"errorlines\">\n");   //$NON-NLS-1$
                        appendCodeBlock(warning.fileContents, warning.line, warning.offset);
                        mWriter.write("\n</pre>");                       //$NON-NLS-1$
                    }
                    mWriter.write('\n');
                    if (warning.location != null && warning.location.getSecondary() != null) {
                        mWriter.write("<ul>");
                        Location l = warning.location.getSecondary();
                        int otherLocations = 0;
                        while (l != null) {
                            String message = l.getMessage();
                            if (message != null && !message.isEmpty()) {
                                Position start = l.getStart();
                                int line = start != null ? start.getLine() : -1;
                                String path = mClient.getDisplayPath(warning.project, l.getFile());
                                writeLocation(l.getFile(), path, line);
                                mWriter.write(':');
                                mWriter.write(' ');
                                mWriter.write("<span class=\"message\">");           //$NON-NLS-1$
                                mWriter.append(RAW.convertTo(message, HTML));
                                mWriter.write("</span>");                            //$NON-NLS-1$
                                mWriter.write("<br />");                         //$NON-NLS-1$

                                String name = l.getFile().getName();
                                if (!(endsWith(name, DOT_PNG) || endsWith(name, DOT_JPG))) {
                                    String s = mClient.readFile(l.getFile());
                                    if (!s.isEmpty()) {
                                        mWriter.write("<pre class=\"errorlines\">\n");   //$NON-NLS-1$
                                        int offset = start != null ? start.getOffset() : -1;
                                        appendCodeBlock(s, line, offset);
                                        mWriter.write("\n</pre>");                       //$NON-NLS-1$
                                    }
                                }
                            } else {
                                otherLocations++;
                            }

                            l = l.getSecondary();
                        }
                        mWriter.write("</ul>");
                        if (otherLocations > 0) {
                            String id = "Location" + count + "Div";          //$NON-NLS-1$
                            mWriter.write("<button id=\"");                  //$NON-NLS-1$
                            mWriter.write(id);
                            mWriter.write("Link\" onclick=\"reveal('");      //$NON-NLS-1$
                            mWriter.write(id);
                            mWriter.write("');\" />"); //$NON-NLS-1$
                            mWriter.write(String.format("+ %1$d Additional Locations...",
                                    otherLocations));
                            mWriter.write("</button>\n");                    //$NON-NLS-1$
                            mWriter.write("<div id=\"");                     //$NON-NLS-1$
                            mWriter.write(id);
                            mWriter.write("\" style=\"display: none\">\n");  //$NON-NLS-1$

                            mWriter.write("Additional locations: ");
                            mWriter.write("<ul>\n"); //$NON-NLS-1$
                            l = warning.location.getSecondary();
                            while (l != null) {
                                Position start = l.getStart();
                                int line = start != null ? start.getLine() : -1;
                                String path = mClient.getDisplayPath(warning.project, l.getFile());
                                mWriter.write("<li> "); //$NON-NLS-1$
                                writeLocation(l.getFile(), path, line);
                                mWriter.write("\n");  //$NON-NLS-1$
                                l = l.getSecondary();
                            }
                            mWriter.write("</ul>\n"); //$NON-NLS-1$

                            mWriter.write("</div><br/><br/>\n"); //$NON-NLS-1$
                        }
                    }

                    // Place a block of images?
                    if (!addedImage && url != null && warning.location != null
                            && warning.location.getSecondary() != null) {
                        addImage(url, warning.location);
                    }

                    if (warning.isVariantSpecific()) {
                        mWriter.write("\n");
                        mWriter.write("Applies to variants: ");
                        mWriter.write(Joiner.on(", ").join(warning.getIncludedVariantNames()));
                        mWriter.write("<br/>\n");
                        mWriter.write("Does <b>not</b> apply to variants: ");
                        mWriter.write(Joiner.on(", ").join(warning.getExcludedVariantNames()));
                        mWriter.write("<br/>\n");
                    }
                }
                if (partialHide) { // Close up the extra div
                    mWriter.write("</div>\n");                           //$NON-NLS-1$
                }

                mWriter.write("</div>\n");                               //$NON-NLS-1$
                writeIssueMetadata(issue, first.severity, null);

                mWriter.write("</div>\n");                               //$NON-NLS-1$
            }

            if (!mClient.isCheckingSpecificIssues()) {
                writeMissingIssues(missing);
            }

            writeSuppressInfo();
        } else {
            mWriter.write("Congratulations!");
        }
        mWriter.write("\n</body>\n</html>");                             //$NON-NLS-1$
        mWriter.close();

        if (!mClient.getFlags().isQuiet()
                && (mDisplayEmpty || errorCount > 0 || warningCount > 0)) {
            String url = SdkUtils.fileToUrlString(mOutput.getAbsoluteFile());
            System.out.println(String.format("Wrote HTML report to %1$s", url));
        }
    }

    private void writeIssueMetadata(Issue issue, Severity severity, String disabledBy)
            throws IOException {
        mWriter.write("<div class=\"metadata\">");               //$NON-NLS-1$

        if (mClient.getRegistry() instanceof BuiltinIssueRegistry) {
            boolean adtHasFix = QuickfixHandler.ADT.hasAutoFix(issue);
            boolean studioHasFix = QuickfixHandler.STUDIO.hasAutoFix(issue);
            if (adtHasFix || studioHasFix) {
                String adt = "Eclipse/ADT";
                String studio = "Android Studio/IntelliJ";
                String tools = adtHasFix && studioHasFix
                        ? (studio + " & " + adt) : studioHasFix ? studio : adt;
                mWriter.write("Note: This issue has an associated quickfix operation in " + tools);
                if (!INLINE_RESOURCES) {
                    mWriter.write(getFixIcon());
                } else if (mFixUrl != null) {
                    mWriter.write("&nbsp;<img alt=\"Fix\" border=\"0\" align=\"top\" src=\""); //$NON-NLS-1$
                    mWriter.write(mFixUrl);
                    mWriter.write("\" />\n");                            //$NON-NLS-1$
                }

                mWriter.write("<br>\n");
            }
        }

        if (disabledBy != null) {
            mWriter.write(String.format("Disabled By: %1$s<br/>\n", disabledBy));
        }

        mWriter.write("Priority: ");
        mWriter.write(String.format("%1$d / 10", issue.getPriority()));
        mWriter.write("<br/>\n");                                //$NON-NLS-1$
        mWriter.write("Category: ");
        mWriter.write(issue.getCategory().getFullName());
        mWriter.write("</div>\n");                               //$NON-NLS-1$

        mWriter.write("Severity: ");
        if (severity == Severity.ERROR || severity == Severity.FATAL) {
            mWriter.write("<span class=\"error\">");             //$NON-NLS-1$
        } else if (severity == Severity.WARNING) {
            mWriter.write("<span class=\"warning\">");           //$NON-NLS-1$
        } else {
            mWriter.write("<span>");                             //$NON-NLS-1$
        }
        appendEscapedText(severity.getDescription());
        mWriter.write("</span>");                                //$NON-NLS-1$

        mWriter.write("<div class=\"summary\">\n");              //$NON-NLS-1$
        mWriter.write("Explanation: ");
        String description = issue.getBriefDescription(HTML);
        mWriter.write(description);
        if (!description.isEmpty()
                && Character.isLetter(description.charAt(description.length() - 1))) {
            mWriter.write('.');
        }
        mWriter.write("</div>\n");                               //$NON-NLS-1$
        mWriter.write("<div class=\"explanation\">\n");          //$NON-NLS-1$
        String explanationHtml = issue.getExplanation(HTML);
        mWriter.write(explanationHtml);
        mWriter.write("\n</div>\n");                             //$NON-NLS-1$;
        List<String> moreInfo = issue.getMoreInfo();
        mWriter.write("<br/>");                                  //$NON-NLS-1$
        mWriter.write("<div class=\"moreinfo\">");               //$NON-NLS-1$
        mWriter.write("More info: ");
        int count = moreInfo.size();
        if (count > 1) {
            mWriter.write("<ul>");                               //$NON-NLS-1$
        }
        for (String uri : moreInfo) {
            if (count > 1) {
                mWriter.write("<li>");                           //$NON-NLS-1$
            }
            mWriter.write("<a href=\"");                         //$NON-NLS-1$
            mWriter.write(uri);
            mWriter.write("\">"    );                            //$NON-NLS-1$
            mWriter.write(uri);
            mWriter.write("</a>\n");                             //$NON-NLS-1$
        }
        if (count > 1) {
            mWriter.write("</ul>");                              //$NON-NLS-1$
        }
        mWriter.write("</div>");                                 //$NON-NLS-1$

        mWriter.write("<br/>");                                  //$NON-NLS-1$
        mWriter.write(String.format(
                "To suppress this error, use the issue id \"%1$s\" as explained in the " +
                "%2$sSuppressing Warnings and Errors%3$s section.",
                issue.getId(),
                "<a href=\"#SuppressInfo\">", "</a>"));          //$NON-NLS-1$ //$NON-NLS-2$
        mWriter.write("<br/>\n");
    }

    private void writeSuppressInfo() throws IOException {
        //getSuppressHelp
        mWriter.write("\n<a name=\"SuppressInfo\"></a>\n");      //$NON-NLS-1$
        mWriter.write("<div class=\"category\">");               //$NON-NLS-1$
        mWriter.write("Suppressing Warnings and Errors");
        mWriter.write("<div class=\"categorySeparator\"></div>\n");//$NON-NLS-1$
        mWriter.write("</div>\n");                               //$NON-NLS-1$
        mWriter.write(TextFormat.RAW.convertTo(Main.getSuppressHelp(), TextFormat.HTML));
        mWriter.write('\n');
    }

    protected Map<Issue, String> computeMissingIssues(List<Warning> warnings) {
        Set<Project> projects = new HashSet<Project>();
        Set<Issue> seen = new HashSet<Issue>();
        for (Warning warning : warnings) {
            projects.add(warning.project);
            seen.add(warning.issue);
        }
        Configuration cliConfiguration = mClient.getConfiguration();
        Map<Issue, String> map = Maps.newHashMap();
        for (Issue issue : mClient.getRegistry().getIssues()) {
            if (!seen.contains(issue)) {
                if (mClient.isSuppressed(issue)) {
                    map.put(issue, "Command line flag");
                    continue;
                }

                if (!issue.isEnabledByDefault() && !mClient.isAllEnabled()) {
                    map.put(issue, "Default");
                    continue;
                }

                if (cliConfiguration != null && !cliConfiguration.isEnabled(issue)) {
                    map.put(issue, "Command line supplied --config lint.xml file");
                    continue;
                }

                // See if any projects disable this warning
                for (Project project : projects) {
                    if (!project.getConfiguration(null).isEnabled(issue)) {
                        map.put(issue, "Project lint.xml file");
                        break;
                    }
                }
            }
        }

        return map;
    }

    private void writeMissingIssues(Map<Issue, String> missing) throws IOException {
        mWriter.write("\n<a name=\"MissingIssues\"></a>\n");        //$NON-NLS-1$
        mWriter.write("<div class=\"category\">");                  //$NON-NLS-1$
        mWriter.write("Disabled Checks");
        mWriter.write("<div class=\"categorySeparator\"></div>\n"); //$NON-NLS-1$
        mWriter.write("</div>\n");                                  //$NON-NLS-1$

        mWriter.write(
                "The following issues were not run by lint, either " +
                "because the check is not enabled by default, or because " +
                "it was disabled with a command line flag or via one or " +
                "more lint.xml configuration files in the project directories.");
        mWriter.write("\n<br/><br/>\n"); //$NON-NLS-1$

        List<Issue> list = new ArrayList<Issue>(missing.keySet());
        Collections.sort(list);


        for (Issue issue : list) {
            mWriter.write("<a name=\"" + issue.getId() + "\"></a>\n"); //$NON-NLS-1$ //$NON-NLS-2$
            mWriter.write("<div class=\"issue\">\n");                  //$NON-NLS-1$

            // Explain this issue
            mWriter.write("<div class=\"id\">");                       //$NON-NLS-1$
            mWriter.write(issue.getId());
            mWriter.write("<div class=\"issueSeparator\"></div>\n");   //$NON-NLS-1$
            mWriter.write("</div>\n");                                 //$NON-NLS-1$
            String disabledBy = missing.get(issue);
            writeIssueMetadata(issue, issue.getDefaultSeverity(), disabledBy);
            mWriter.write("</div>\n");                                 //$NON-NLS-1$
        }
    }

    protected void writeStyleSheet() throws IOException {
        if (USE_HOLO_STYLE) {
            mWriter.write(
                "<link rel=\"stylesheet\" type=\"text/css\" " +          //$NON-NLS-1$
                "href=\"http://fonts.googleapis.com/css?family=Roboto\" />\n" );//$NON-NLS-1$
        }

        URL cssUrl = HtmlReporter.class.getResource(CSS);
        if (mSimpleFormat || INLINE_RESOURCES) {
            // Inline the CSS
            mWriter.write("<style>\n");                                   //$NON-NLS-1$
            InputStream input = cssUrl.openStream();
            byte[] bytes = ByteStreams.toByteArray(input);
            try {
                Closeables.close(input, true /* swallowIOException */);
            } catch (IOException e) {
                // cannot happen
            }
            String css = new String(bytes, Charsets.UTF_8);
            mWriter.write(css);
            mWriter.write("</style>\n");                                  //$NON-NLS-1$
        } else {
            String ref = addLocalResources(cssUrl);
            if (ref != null) {
                mWriter.write(
                "<link rel=\"stylesheet\" type=\"text/css\" href=\""     //$NON-NLS-1$
                            + ref + "\" />\n");                          //$NON-NLS-1$
            }
        }
    }

    private void writeOverview(List<List<Warning>> related, int missingCount)
            throws IOException {
        // Write issue id summary
        mWriter.write("<table class=\"overview\">\n");                          //$NON-NLS-1$

        String errorUrl = null;
        String warningUrl = null;
        if (!INLINE_RESOURCES && !mSimpleFormat) {
            errorUrl = addLocalResources(getErrorIconUrl());
            warningUrl = addLocalResources(getWarningIconUrl());
            mFixUrl = addLocalResources(HtmlReporter.class.getResource("lint-run.png")); //$NON-NLS-1$)
        }

        Category previousCategory = null;
        for (List<Warning> warnings : related) {
            Issue issue = warnings.get(0).issue;

            boolean isError = false;
            for (Warning warning : warnings) {
                if (warning.severity == Severity.ERROR || warning.severity == Severity.FATAL) {
                    isError = true;
                    break;
                }
            }

            if (issue.getCategory() != previousCategory) {
                mWriter.write("<tr><td></td><td class=\"categoryColumn\">");
                previousCategory = issue.getCategory();
                String categoryName = issue.getCategory().getFullName();
                mWriter.write("<a href=\"#");                        //$NON-NLS-1$
                mWriter.write(categoryName);
                mWriter.write("\">");                                //$NON-NLS-1$
                mWriter.write(categoryName);
                mWriter.write("</a>\n");                             //$NON-NLS-1$
                mWriter.write("</td></tr>");                         //$NON-NLS-1$
                mWriter.write("\n");                                 //$NON-NLS-1$
            }
            mWriter.write("<tr>\n");                                 //$NON-NLS-1$

            // Count column
            mWriter.write("<td class=\"countColumn\">");             //$NON-NLS-1$
            mWriter.write(Integer.toString(warnings.size()));
            mWriter.write("</td>");                                  //$NON-NLS-1$

            mWriter.write("<td class=\"issueColumn\">");             //$NON-NLS-1$

            if (INLINE_RESOURCES) {
                String markup = isError ? getErrorIcon() : getWarningIcon();
                mWriter.write(markup);
                mWriter.write('\n');
            } else {
                String imageUrl = isError ? errorUrl : warningUrl;
                if (imageUrl != null) {
                    mWriter.write("<img border=\"0\" align=\"top\" src=\""); //$NON-NLS-1$
                    mWriter.write(imageUrl);
                    mWriter.write("\" alt=\"");
                    mWriter.write(isError ? "Error" : "Warning");
                    mWriter.write("\" />\n");                            //$NON-NLS-1$
                }
            }

            mWriter.write("<a href=\"#");                            //$NON-NLS-1$
            mWriter.write(issue.getId());
            mWriter.write("\">");                                    //$NON-NLS-1$
            mWriter.write(issue.getId());
            mWriter.write(": ");                                     //$NON-NLS-1$
            mWriter.write(issue.getBriefDescription(HTML));
            mWriter.write("</a>\n");                                 //$NON-NLS-1$

            mWriter.write("</td></tr>\n");
        }

        if (missingCount > 0 && !mClient.isCheckingSpecificIssues()) {
            mWriter.write("<tr><td></td>");                          //$NON-NLS-1$
            mWriter.write("<td class=\"categoryColumn\">");          //$NON-NLS-1$
            mWriter.write("<a href=\"#MissingIssues\">");            //$NON-NLS-1$
            mWriter.write(String.format("Disabled Checks (%1$d)",
                    missingCount));

            mWriter.write("</a>\n");                                 //$NON-NLS-1$
            mWriter.write("</td></tr>");                             //$NON-NLS-1$
        }

        mWriter.write("</table>\n");                                 //$NON-NLS-1$
        mWriter.write("<br/>");                                      //$NON-NLS-1$
    }

    private String writeLocation(File file, String path, int line) throws IOException {
        String url;
        mWriter.write("<span class=\"location\">");      //$NON-NLS-1$

        url = getUrl(file);
        if (url != null) {
            mWriter.write("<a href=\"");                 //$NON-NLS-1$
            mWriter.write(url);
            mWriter.write("\">");                        //$NON-NLS-1$
        }

        String displayPath = stripPath(path);
        if (url != null && url.startsWith("../") && new File(displayPath).isAbsolute()) {
            displayPath = url;
        }
        mWriter.write(displayPath);
        //noinspection VariableNotUsedInsideIf
        if (url != null) {
            mWriter.write("</a>");                       //$NON-NLS-1$
        }
        if (line >= 0) {
            // 0-based line numbers, but display 1-based
            mWriter.write(':');
            mWriter.write(Integer.toString(line + 1));
        }
        mWriter.write("</span>");                        //$NON-NLS-1$
        return url;
    }

    private boolean addImage(String url, Location location) throws IOException {
        if (url != null && endsWith(url, DOT_PNG)) {
            if (location.getSecondary() != null) {
                // Emit many images
                // Add in linked images as well
                List<String> urls = new ArrayList<String>();
                while (location != null && location.getFile() != null) {
                    String imageUrl = getUrl(location.getFile());
                    if (imageUrl != null
                            && endsWith(imageUrl, DOT_PNG)) {
                        urls.add(imageUrl);
                    }
                    location = location.getSecondary();
                }
                if (!urls.isEmpty()) {
                    // Sort in order
                    Collections.sort(urls, new Comparator<String>() {
                        @Override
                        public int compare(String s1, String s2) {
                            return getDpiRank(s1) - getDpiRank(s2);
                        }
                    });
                    mWriter.write("<table>");                            //$NON-NLS-1$
                    mWriter.write("<tr>");                               //$NON-NLS-1$
                    for (String linkedUrl : urls) {
                        // Image series: align top
                        mWriter.write("<td>");                           //$NON-NLS-1$
                        mWriter.write("<a href=\"");                     //$NON-NLS-1$
                        mWriter.write(linkedUrl);
                        mWriter.write("\">");                            //$NON-NLS-1$
                        mWriter.write("<img border=\"0\" align=\"top\" src=\"");      //$NON-NLS-1$
                        mWriter.write(linkedUrl);
                        mWriter.write("\" /></a>\n");                    //$NON-NLS-1$
                        mWriter.write("</td>");                          //$NON-NLS-1$
                    }
                    mWriter.write("</tr>");                              //$NON-NLS-1$

                    mWriter.write("<tr>");                               //$NON-NLS-1$
                    for (String linkedUrl : urls) {
                        mWriter.write("<th>");                           //$NON-NLS-1$
                        int index = linkedUrl.lastIndexOf("drawable-");  //$NON-NLS-1$
                        if (index != -1) {
                            index += "drawable-".length();               //$NON-NLS-1$
                            int end = linkedUrl.indexOf('/', index);
                            if (end != -1) {
                                mWriter.write(linkedUrl.substring(index, end));
                            }
                        }
                        mWriter.write("</th>");                          //$NON-NLS-1$
                    }
                    mWriter.write("</tr>\n");                            //$NON-NLS-1$

                    mWriter.write("</table>\n");                         //$NON-NLS-1$
                }
            } else {
                // Just this image: float to the right
                mWriter.write("<img class=\"embedimage\" align=\"right\" src=\""); //$NON-NLS-1$
                mWriter.write(url);
                mWriter.write("\" />");                                  //$NON-NLS-1$
            }

            return true;
        }

        return false;
    }

    /** Provide a sorting rank for a url */
    private static int getDpiRank(String url) {
        if (url.contains("-xhdpi")) {                                   //$NON-NLS-1$
            return 0;
        } else if (url.contains("-hdpi")) {                             //$NON-NLS-1$
            return 1;
        } else if (url.contains("-mdpi")) {                             //$NON-NLS-1$
            return 2;
        } else if (url.contains("-ldpi")) {                             //$NON-NLS-1$
            return 3;
        } else {
            return 4;
        }
    }

    private void appendCodeBlock(String contents, int lineno, int offset)
            throws IOException {
        int max = lineno + 3;
        int min = lineno - 3;
        for (int l = min; l < max; l++) {
            if (l >= 0) {
                int lineOffset = LintCliClient.getLineOffset(contents, l);
                if (lineOffset == -1) {
                    break;
                }

                mWriter.write(String.format("<span class=\"lineno\">%1$4d</span> ", (l + 1))); //$NON-NLS-1$

                String line = LintCliClient.getLineOfOffset(contents, lineOffset);
                if (offset != -1 && lineOffset <= offset && lineOffset+line.length() >= offset) {
                    // Text nodes do not always have correct lines/offsets
                    //assert l == lineno;

                    // This line contains the beginning of the offset
                    // First print everything before
                    int delta = offset - lineOffset;
                    appendEscapedText(line.substring(0, delta));
                    mWriter.write("<span class=\"errorspan\">");         //$NON-NLS-1$
                    appendEscapedText(line.substring(delta));
                    mWriter.write("</span>");                            //$NON-NLS-1$
                } else if (offset == -1 && l == lineno) {
                    mWriter.write("<span class=\"errorline\">");         //$NON-NLS-1$
                    appendEscapedText(line);
                    mWriter.write("</span>");                            //$NON-NLS-1$
                } else {
                    appendEscapedText(line);
                }
                if (l < max - 1) {
                    mWriter.write("\n");                                 //$NON-NLS-1$
                }
            }
        }
    }

    protected void appendEscapedText(String textValue) throws IOException {
        for (int i = 0, n = textValue.length(); i < n; i++) {
            char c = textValue.charAt(i);
            if (c == '<') {
                mWriter.write("&lt;");                                   //$NON-NLS-1$
            } else if (c == '&') {
                mWriter.write("&amp;");                                  //$NON-NLS-1$
            } else if (c == '\n') {
                mWriter.write("<br/>\n");
            } else {
                if (c > 255) {
                    mWriter.write("&#");                                 //$NON-NLS-1$
                    mWriter.write(Integer.toString(c));
                    mWriter.write(';');
                } else {
                    mWriter.write(c);
                }
            }
        }
    }

    private String stripPath(String path) {
        if (mStripPrefix != null && path.startsWith(mStripPrefix)
                && path.length() > mStripPrefix.length()) {
            int index = mStripPrefix.length();
            if (path.charAt(index) == File.separatorChar) {
                index++;
            }
            return path.substring(index);
        }

        return path;
    }

    /** Sets path prefix to strip from displayed file names */
    void setStripPrefix(String prefix) {
        mStripPrefix = prefix;
    }

    static URL getWarningIconUrl() {
        return HtmlReporter.class.getResource("lint-warning.png");   //$NON-NLS-1$
    }

    static URL getErrorIconUrl() {
        return HtmlReporter.class.getResource("lint-error.png");     //$NON-NLS-1$
    }

    static String getErrorIcon() {
        return "<img border=\"0\" align=\"top\" width=\"15\" height=\"15\" alt=\"Error\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAB00lEQVR42nWTS0hbQRiF587MzkUXooi6UHAjhNKNSkGhCDXxkZhUIwWhBLoRsQpi3bXmIboSV2aliI+WKqLUtqsuSxclrhRBUMnVmIpa2oIkQon+zhlr9F7jwOEw/znfLO6dYcy2Arys6AUv6x7klTNh4ViFY485u2+N8Uc8yB1DH0Vt6ki2UkZ20LkS/Eh6CXPk6FnAKVHNJ3nViind9E/6tTKto3TxaU379Qw5euhn4QXxOGzKFjqT7Vmlwx8IC357jh76GvzC64pj4mn6VLbRbf0Nvdcw3J6hr7gS9o3XDxwIN/0RPot+h95pGG7P0AfH1oVz6UR4ya5foXkNw3Pl4Ngub/p6yD1k13FoTsPwXDk4ti89SwnuJrtigYiGY4FhypWDY2aeb0CJ4rzZou9GPc0Y1drtGfrgWLzweUm8uPNsx2ikrHgjHT6LUOrzD/rpDpIlU0JfcaX6d8UfdoW38/20ZbiuxF10MHL1tRNvp2/mSuihn70kZl2/MJ+8Xtkq8NOm4VRqoIUKLy0Hx2mx3PN/5iTk6KFvuaJmyxux3zE8tFPTm9p84KMNdcAGa9COvZqnkaN37wNJvpooSvZFexIvx2b3OkdX4dgne6N3XtUl5wqoyBY2uZQAAAAASUVORK5CYII=\" />";
    }

    static String getWarningIcon() {
        return "<img border=\"0\" align=\"top\" width=\"16\" height=\"15\" alt=\"Warning\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAPCAQAAABHeoekAAAA3klEQVR42nWPsWoCQRCGVyJiF9tAsNImbcDKR/ABBEurYCsBsfQRQiAPYGPyAAnYWQULS9MErNU2Vsr/ObMX7g6O+xd2/5n5dmY3hFQEBVVpuCsVT/yoUl6u4XotBz4E4qR2YYyH6ugEWY8comR/t+tvPPJtSLPYvhvvTswtbdCmCOwjMHXAzjP9kB/ByB7nejbgy43WVPF3WNG+p9+kzkozdhGAQdZh7BlHdGTL3z98pp6Um7okKdvHNuIzWk+9xN+yINOcHps0OnAfuOOoHJH3pmHghhYP2VJcaXx7BaKz9YB2HVrDAAAAAElFTkSuQmCC\"/>";
    }

    static String getFixIcon() {
        return "<img border=\"0\" align=\"top\" width=\"16\" height=\"16\" alt=\"Fix\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA1ElEQVR42s2Szw7BQBCH+wTezAM4u7h7BBKEa3v1AK7iKvoAuDg5uSoaqTR2u90xs9P1J2FLHNjky29mu/Nt09Tz/mb1gxF8wlPBKoG3cAoiCbAVTCQe69bcN8+dgp1k9oTgpL4+bYIXVKCNEqfgIJk4w0RirGmIhklCe07BeBPCEQ9ZOsUwpd17KRiuQ3O4u/DhpMDkfU8kquQNesVQdVIzSX2KQ2l+wykQeKAx4w9GSf05532LU5BpZrD0rzUhLVAiwAtAaYbqXDPKpkvw1a/8s3UBSc/bWGUWa6wAAAAASUVORK5CYII=\"/>";
    }
}
