/*
 * Copyright 2011 Luke Usherwood.
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2.1 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
        
package net.bettyluke.tracinstant.ui;

import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.bettyluke.tracinstant.data.Ticket;
import net.bettyluke.tracinstant.prefs.TracInstantProperties;


public class HtmlFormatter {
    
    private static final int MAX_DESCRIPTIONS = 50;

    private static final Pattern BUG_PATTERN = Pattern.compile("#([0-9]{1,8}+)");

    private static final URL STYLESHEET_TRAC_RESOURCE =
        HtmlFormatter.class.getResource("res/trac.css");
    
    private final static String HTML_HEADER =
        "<html><head><link rel=\"stylesheet\" type=\"text/css\" href=\"" +
            STYLESHEET_TRAC_RESOURCE + "\">";
    
    private final static String HTML_END = "</html>";

    /**
     * Remove a style that Java can't display, so that closed tickets display
     * crossed out
     */
    protected static String fixHyperlinks(String text) {
        return text.replaceAll("class=\\\"closed ticket", "class=\"closed");
    }

    protected static String buildDescription(Ticket[] tickets, SearchTerm[] searchTerms) {
        if (tickets.length == 0) {
            return "";
        }
        String body = buildBody(tickets);
        String highlighted = highlightMatches(body, searchTerms);
        return HTML_HEADER + fixHyperlinks(highlighted) + HTML_END;
    }

    private static String buildBody(Ticket[] tickets) {
        StringBuilder body = new StringBuilder("<body style=\"margin:0;\">");
        int count = 0;
        for (Ticket ticket : tickets) {
            if ((++count) > MAX_DESCRIPTIONS) {
                body.append("<br><i>Limit of " + MAX_DESCRIPTIONS + " tickets reached.");
                break;
            }
            String background = (count % 2 == 0) ? "#ffffd0" : "#ffffff";
            body.append("<div style=\"background:" + background + "; padding:3px;\">");
            String heading = makeHyperlinkedHeading(ticket);
            if (heading != null) {
                body.append(heading);
            }
            String description = ticket.getValue("description");
            if (description == null) {
                body.append(
                    "<br><i>Trac query in progress...</i><br> &nbsp;");
                body.append("</div>");
                break;
            }
            body.append(description);
            body.append("</div>");
        }
        body.append("</body>");
        return body.toString();
    }

    private static String highlightMatches(String body, SearchTerm[] searchTerms) {
        
        if (searchTerms.length == 0) {
            return body;
        }
        Pattern superPattern = createSuperPattern(searchTerms);
        
        StringBuilder bb = new StringBuilder();
        int rangeStart = 0, rangeEnd = 0, replacements = 0;
        while (true) {
            rangeEnd = body.indexOf('>', rangeStart) + 1;
            if (rangeEnd == -1) {
                break;
            }
            
            // Append up to end of a tag without modification.
            bb.append(body.substring(rangeStart, rangeEnd));
            rangeStart = rangeEnd;
            
            rangeEnd = body.indexOf('<', rangeStart);
            if (rangeEnd == -1) {
                break;
            }
            
            // Append a text segment with any matching highlighting marked-up
            String text = body.substring(rangeStart, rangeEnd);
            Matcher m = superPattern.matcher(text);
            
            replacements += appendHighlightedText(bb, m, text);

            // Limit to a reasonable number of highlights; not just so that this method
            // doesn't spiral out of control (time, memory) but also the HTML renderer.
            if (replacements > 800) {
                break;
            }
            
            rangeStart = rangeEnd;
        }
        bb.append(body.substring(rangeStart, body.length()));
        return bb.toString();
    }

    /** Like <code>Matcher.replaceAll()</code> but also counting replacements. */
    private static int appendHighlightedText(StringBuilder bb, Matcher m, String text) {
        int replacements = 0;
        if (m.find()) {
            StringBuffer sb = new StringBuffer();
            do {
                m.appendReplacement(sb, 
                    "<font color=\"white\" bgcolor=\"#66dd88\">$0</font>");
                ++ replacements;
            } while (m.find());
            m.appendTail(sb);
            bb.append(sb.toString());
        } else {
            bb.append(text);
        }
        return replacements;
    }

    private static Pattern createSuperPattern(SearchTerm[] searchTerms) {
        assert searchTerms.length > 0;
        StringBuilder sb = new StringBuilder();
        String pipe = "";
        sb.append('(');
        for (SearchTerm term : searchTerms) {
            
            // TODO: create constants for special fields such as "description"
            if (term.field == null ||
                    "description".startsWith(term.field.toLowerCase())) {
                sb.append(pipe).append(term.pattern.toString());
                pipe = "|";
            }
        }
        sb.append(')');
        return Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE);
    }

    private static String makeHyperlinkedHeading(Ticket ticket) {
        String title = ticket.getValue("title");
        if (title == null) {
            return null;
        }
        title = BUG_PATTERN.matcher(title).replaceAll(
            "<a style=\"text-decoration: none;\" " +
            "href=\"" + TracInstantProperties.getURL() + "/ticket/$1\">#$1</a>");
        return "<h2 style=\"color:#770044;\">" + title + "</h2>";
    }
}

@@ -3277,146 +3277,134 @@
     
-%0A        if (searchTerms.length == 0) %7B%0A            return body;%0A        %7D%0A        Pattern superPattern = createSuperPattern(searchTerms);
+Pattern superPattern = createSuperPattern(searchTerms);%0A        if (superPattern == null) %7B%0A            return body;%0A        %7D
 %0A   
@@ -5384,47 +5384,8 @@
 ) %7B%0A
-        assert searchTerms.length %3E 0;%0A
     
@@ -5865,24 +5865,91 @@
 ppend(')');%0A
+        if (sb.length() == 2) %7B%0A            return null;%0A        %7D%0A
         retu

Correct the 'no-highlighting needed' check