/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.maven.reporting;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.maven.doxia.markup.Markup;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
import org.apache.maven.shared.utils.StringUtils;

/**
 * <p>An abstract class to manage report generation, with many helper methods to ease the job: you just need to
 * implement getTitle() and renderBody().</p>
 *
 * <p><strong>TODO</strong> Later it may be appropriate to create something like a VelocityMavenReportRenderer
 * that could take a velocity template and pipe that through Doxia rather than coding them
 * up like this.</p>
 *
 * @author <a href="mailto:jason@maven.org">Jason van Zyl</a>
 * @author <a href="evenisse@apache.org">Emmanuel Venisse</a>
 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
 * @since 2.0
 * @see #getTitle()
 * @see #renderBody()
 */
public abstract class AbstractMavenReportRenderer implements MavenReportRenderer {
    /** The current sink to use */
    protected Sink sink;

    /** The current section number */
    private int section;

    /**
     * Default constructor.
     *
     * @param sink the sink to use.
     */
    public AbstractMavenReportRenderer(Sink sink) {
        this.sink = sink;
    }

    /** {@inheritDoc} */
    @Override
    public void render() {
        sink.head();

        sink.title();
        text(getTitle());
        sink.title_();

        sink.head_();

        sink.body();
        renderBody();
        sink.body_();

        sink.flush();

        sink.close();
    }

    // ----------------------------------------------------------------------
    // Section handler
    // ----------------------------------------------------------------------

    /**
     * Convenience method to wrap section creation in the current sink.
     * An anchor will be derived from the name.
     *
     * @param name the name of this section, could be null.
     * @see #text(String)
     * @see Sink#section(int, org.apache.maven.doxia.sink.SinkEventAttributes)
     * @see Sink#sectionTitle(int, org.apache.maven.doxia.sink.SinkEventAttributes)
     * @see Sink#sectionTitle_(int)
     */
    protected void startSection(String name) {
        startSection(name, name);
    }

    /**
     * Convenience method to wrap section creation in the current sink.
     *
     * @param name the name of this section, could be null.
     * @param anchor the anchor of this section, could be null.
     * @see #text(String)
     * @see Sink#section(int, org.apache.maven.doxia.sink.SinkEventAttributes)
     * @see Sink#sectionTitle(int, org.apache.maven.doxia.sink.SinkEventAttributes)
     * @see Sink#sectionTitle_(int)
     */
    protected void startSection(String name, String anchor) {
        section++;
        sink.section(section, null);
        sink.anchor(anchor);
        sink.anchor_();
        sink.sectionTitle(section, null);
        text(name);
        sink.sectionTitle_(section);
    }

    /**
     * Convenience method to wrap section ending in the current sink.
     *
     * @see Sink#section_(int)
     * @throws IllegalStateException if too many closing sections.
     */
    protected void endSection() {
        sink.section_(section);
        section--;

        if (section < 0) {
            throw new IllegalStateException("Too many closing sections");
        }
    }

    // ----------------------------------------------------------------------
    // Table handler
    // ----------------------------------------------------------------------

    /**
     * Convenience method to wrap the table start in the current sink.
     *
     * @see Sink#table()
     */
    protected void startTable() {
        startTable(null, false);
    }

    /**
     * Convenience method to wrap the table start in the current sink.
     *
     * @param justification the justification of table cells.
     * @param grid whether to draw a grid around cells.
     *
     * @see Sink#table()
     * @see Sink#tableRows(int[],boolean)
     * @since 2.1
     */
    protected void startTable(int[] justification, boolean grid) {
        sink.table();
        sink.tableRows(justification, grid);
    }

    /**
     * Convenience method to wrap the table ending in the current sink.
     *
     * @see Sink#table_()
     */
    protected void endTable() {
        sink.tableRows_();
        sink.table_();
    }

    /**
     * Convenience method to wrap the table header cell start in the current sink.
     *
     * @param text the text to put in this cell, could be null.
     * @see #text(String)
     * @see Sink#tableHeaderCell()
     * @see Sink#tableHeaderCell_()
     */
    protected void tableHeaderCell(String text) {
        sink.tableHeaderCell();

        text(text);

        sink.tableHeaderCell_();
    }

    /**
     * Convenience method to wrap a table cell start in the current sink.
     * <p>The text could be a link patterned text defined by <code>{text, url}</code></p>
     *
     * @param text the text to put in this cell, could be null.
     * @see #linkPatternedText(String)
     * @see #tableCell(String)
     */
    protected void tableCell(String text) {
        tableCell(text, false);
    }

    /**
     * Convenience method to wrap a table cell start in the current sink.
     * <p>The text could be a link patterned text defined by <code>{text, url}</code></p>
     * <p>If <code>asHtml</code> is true, add the text as Html</p>
     *
     * @param text the text to put in this cell, could be null.
     * @param asHtml {@code true} to add the text as Html, {@code false} otherwise.
     * @see #linkPatternedText(String)
     * @see Sink#tableCell()
     * @see Sink#tableCell_()
     * @see Sink#rawText(String)
     */
    protected void tableCell(String text, boolean asHtml) {
        sink.tableCell();

        if (asHtml) {
            sink.rawText(text);
        } else {
            linkPatternedText(text);
        }

        sink.tableCell_();
    }

    /**
     * Convenience method to wrap a table row start in the current sink.
     * <p>The texts in the <code>content</code> could be link patterned texts defined by <code>{text, url}</code></p>
     *
     * @param content an array of text to put in the cells in this row, could be null.
     * @see #tableCell(String)
     * @see Sink#tableRow()
     * @see Sink#tableRow_()
     */
    protected void tableRow(String[] content) {
        sink.tableRow();

        if (content != null) {
            for (int i = 0; i < content.length; i++) {
                tableCell(content[i]);
            }
        }

        sink.tableRow_();
    }

    /**
     * Convenience method to wrap a table header row start in the current sink.
     *
     * @param content an array of text to put in the cells in this row header, could be null.
     * @see #tableHeaderCell(String)
     * @see Sink#tableRow()
     * @see Sink#tableRow_()
     */
    protected void tableHeader(String[] content) {
        sink.tableRow();

        if (content != null) {
            for (int i = 0; i < content.length; i++) {
                tableHeaderCell(content[i]);
            }
        }

        sink.tableRow_();
    }

    /**
     * Convenience method to wrap a table caption in the current sink.
     *
     * @param caption the caption of the table, could be null.
     * @see #text(String)
     * @see Sink#tableCaption()
     * @see Sink#tableCaption_()
     */
    protected void tableCaption(String caption) {
        sink.tableCaption();

        text(caption);

        sink.tableCaption_();
    }

    // ----------------------------------------------------------------------
    // Paragraph handler
    // ----------------------------------------------------------------------

    /**
     * Convenience method to wrap a paragraph in the current sink.
     *
     * @param paragraph the paragraph to add, could be null.
     * @see #text(String)
     * @see Sink#paragraph()
     * @see Sink#paragraph_()
     */
    protected void paragraph(String paragraph) {
        sink.paragraph();

        text(paragraph);

        sink.paragraph_();
    }

    /**
     * Convenience method to wrap a link in the current sink.
     *
     * @param href the link to add, cannot be null.
     * @param name the link name.
     * @see #text(String)
     * @see Sink#link(String)
     * @see Sink#link_()
     */
    protected void link(String href, String name) {
        sink.link(href);

        text(name);

        sink.link_();
    }

    /**
     * Convenience method to wrap a text in the current sink.
     * <p>If text is empty or has a <code>null</code> value, add the <code>"-"</code> character</p>
     *
     * @param text a text, could be null.
     * @see Sink#text(String)
     */
    protected void text(String text) {
        if (text == null || text.isEmpty()) // Take care of spaces
        {
            sink.text("-");
        } else {
            sink.text(text);
        }
    }

    /**
     * Convenience method to wrap a text as verbatim style in the current sink .
     *
     * @param text a text, could be null.
     * @see #text(String)
     * @see Sink#verbatim(org.apache.maven.doxia.sink.SinkEventAttributes)
     * @see Sink#verbatim_()
     */
    protected void verbatimText(String text) {
        sink.verbatim();

        text(text);

        sink.verbatim_();
    }

    /**
     * Convenience method to wrap a text with a given link href as verbatim style in the current sink.
     *
     * @param text a string
     * @param href an href could be null
     * @see #link(String, String)
     * @see #verbatimText(String)
     * @see Sink#verbatim(org.apache.maven.doxia.sink.SinkEventAttributes)
     * @see Sink#verbatim_()
     */
    protected void verbatimLink(String text, String href) {
        if (href == null || href.isEmpty()) {
            verbatimText(text);
        } else {
            sink.verbatim();

            link(href, text);

            sink.verbatim_();
        }
    }

    /**
     * Convenience method to wrap source code as verbatim style in the current sink .
     *
     * @param source a source code, could be null.
     * @see #text(String)
     * @see Sink#verbatim(org.apache.maven.doxia.sink.SinkEventAttributes)
     * @see Sink#verbatim_()
     */
    protected void verbatimSource(String source) {
        sink.verbatim(SinkEventAttributeSet.SOURCE);

        text(source);

        sink.verbatim_();
    }

    /**
     * Convenience method to add a Javascript code in the current sink.
     *
     * @param jsCode a string of Javascript
     * @see Sink#rawText(String)
     */
    protected void javaScript(String jsCode) {
        sink.rawText(Markup.EOL + "<script>" + Markup.EOL + jsCode + Markup.EOL + "</script>" + Markup.EOL);
    }

    /**
     * Convenience method to wrap a patterned text in the current link.
     * <p>The text variable should contained this given pattern <code>{text, url}</code>
     * to handle the link creation.</p>
     *
     * @param text a text with link pattern defined.
     * @see #text(String)
     * @see #link(String, String)
     * @see #applyPattern(String)
     */
    public void linkPatternedText(String text) {
        if (text == null || text.isEmpty()) {
            text(text);
        } else {
            List<String> segments = applyPattern(text);

            if (segments == null) {
                text(text);
            } else {
                for (Iterator<String> it = segments.iterator(); it.hasNext(); ) {
                    String name = it.next();
                    String href = it.next();

                    if (href == null) {
                        text(name);
                    } else {
                        link(href, name);
                    }
                }
            }
        }
    }

    /**
     * Create a link pattern text defined by <code>{text, url}</code>.
     * <p>This created pattern could be used by the method <code>linkPatternedText(String)</code> to
     * handle a text with link.</p>
     *
     * @param text
     * @param href
     * @return a link pattern
     * @see #linkPatternedText(String)
     */
    protected static String createLinkPatternedText(String text, String href) {
        if (text == null) {
            return text;
        }

        if (href == null) {
            return text;
        }

        return '{' + text + ", " + href + '}';
    }

    /**
     * Convenience method to display a <code>Properties</code> object as comma separated String.
     *
     * @param props the properties to display.
     * @return the properties object as comma separated String
     */
    protected static String propertiesToString(Properties props) {
        if (props == null || props.isEmpty()) {
            return "";
        }

        StringBuilder sb = new StringBuilder();

        for (Map.Entry<?, ?> entry : props.entrySet()) {
            if (sb.length() > 0) {
                sb.append(", ");
            }

            sb.append(entry.getKey()).append("=").append(entry.getValue());
        }

        return sb.toString();
    }

    // ----------------------------------------------------------------------
    // Private methods
    // ----------------------------------------------------------------------

    /**
     * The method parses a text and applies the given pattern <code>{text, url}</code> to create
     * a list of text/href.
     *
     * @param text a text with or without the pattern <code>{text, url}</code>
     * @return a map of text/href
     */
    private static List<String> applyPattern(String text) {
        if (text == null || text.isEmpty()) {
            return null;
        }

        // Map defined by key/value name/href
        // If href == null, it means
        List<String> segments = new ArrayList<>();

        // TODO Special case http://jira.codehaus.org/browse/MEV-40
        if (text.indexOf("${") != -1) {
            int lastComma = text.lastIndexOf(",");
            int lastSemi = text.lastIndexOf("}");
            if (lastComma != -1 && lastSemi != -1 && lastComma < lastSemi) {
                segments.add(text.substring(lastComma + 1, lastSemi).trim());
                segments.add(null);
            } else {
                segments.add(text);
                segments.add(null);
            }

            return segments;
        }

        boolean inQuote = false;
        int braceStack = 0;
        int lastOffset = 0;

        for (int i = 0; i < text.length(); i++) {
            char ch = text.charAt(i);

            if (ch == '\'' && !inQuote && braceStack == 0) {
                // handle: ''
                if (i + 1 < text.length() && text.charAt(i + 1) == '\'') {
                    i++;
                    segments.add(text.substring(lastOffset, i));
                    segments.add(null);
                    lastOffset = i + 1;
                } else {
                    inQuote = true;
                }
            } else {
                switch (ch) {
                    case '{':
                        if (!inQuote) {
                            if (braceStack == 0) {
                                if (i != lastOffset) // handle { at first character
                                {
                                    segments.add(text.substring(lastOffset, i));
                                    segments.add(null);
                                }
                                lastOffset = i + 1;
                            }
                            braceStack++;
                        }
                        break;
                    case '}':
                        if (!inQuote) {
                            braceStack--;
                            if (braceStack == 0) {
                                String subString = text.substring(lastOffset, i);
                                lastOffset = i + 1;

                                int lastComma = subString.lastIndexOf(",");
                                if (lastComma != -1) {
                                    segments.add(
                                            subString.substring(0, lastComma).trim());
                                    segments.add(
                                            subString.substring(lastComma + 1).trim());
                                } else {
                                    segments.add(subString);
                                    segments.add(null);
                                }
                            }
                        }
                        break;
                    case '\'':
                        inQuote = false;
                        break;
                    default:
                        break;
                }
            }
        }

        if (!StringUtils.isEmpty(text.substring(lastOffset))) {
            segments.add(text.substring(lastOffset));
            segments.add(null);
        }

        if (braceStack != 0) {
            throw new IllegalArgumentException("Unmatched braces in the pattern.");
        }

        if (inQuote) {
            // throw new IllegalArgumentException( "Unmatched quote in the pattern." );
            // TODO: warning...
        }

        return Collections.unmodifiableList(segments);
    }

    // ----------------------------------------------------------------------
    // Abstract methods
    // ----------------------------------------------------------------------

    @Override
    public abstract String getTitle();

    /**
     * Renderer the body content of the report.
     */
    protected abstract void renderBody();
}
