package org.apache.maven.tools.plugin.javadoc;

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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.StringTokenizer;

import javax.swing.text.AttributeSet;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.SimpleAttributeSet;

import com.sun.javadoc.Tag;
import com.sun.tools.doclets.Taglet;

/**
 * Abstract <code>Taglet</code> for <a href="http://maven.codehaus.org/"/>Maven</a> Mojo annotations.
 * <br/>
 * A Mojo annotation is defined like the following:
 * <pre>
 * &#64;annotation &lt;annotationValue&gt; &lt;parameterName="parameterValue"&gt;
 * </pre>
 *
 * @see <a href="package-summary.html#package_description">package-summary.html</a>
 *
 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
 * @version $Id: AbstractMojoTaglet.java 1405630 2012-11-04 20:38:18Z rfscholte $
 */
public abstract class AbstractMojoTaglet
    implements Taglet
{
    /** {@inheritDoc} */
    public String toString( Tag tag )
    {
        if ( tag == null )
        {
            return null;
        }

        String tagValue = getTagValue( tag );
        MutableAttributeSet tagAttributes = getTagAttributes( tag );

        StringBuilder sb = new StringBuilder();

        appendTag( sb, tag, tagAttributes, tagValue );

        return sb.toString();
    }

    /** {@inheritDoc} */
    public String toString( Tag[] tags )
    {
        if ( tags.length == 0 )
        {
            return null;
        }

        StringBuilder sb = new StringBuilder();
        for ( int i = 0; i < tags.length; i++ )
        {
            String tagValue = getTagValue( tags[i] );
            MutableAttributeSet tagAttributes = getTagAttributes( tags[i] );

            appendTag( sb, tags[i], tagAttributes, tagValue );
        }

        return sb.toString();
    }

    /**
     * @return the header, i.e. the message, to display
     */
    public abstract String getHeader();

    /**
     * @return the given annotation value, or <code>null</code> if the given Mojo annotation/tag does't allow
     * annotation value.
     * <br/>
     * <b>Note</b>: the value could be a pattern value, i.e.: <code>*</code> for every values, <code>a|b|c</code>
     * for <code>a OR b OR c</code>.
     */
    public abstract String getAllowedValue();

    /**
     * @return an array of the allowed parameter names for the given Mojo annotation/tag, or <code>null</code>
     * if the annotation/tag doesn't allow parameter.
     */
    public abstract String[] getAllowedParameterNames();

    /**
     * @return <code>true</code> if taglet has annotation value, <code>false</code> otherwise.
     * @see #getAllowedValue()
     */
    public boolean hasAnnotationValue()
    {
        return getAllowedValue() != null;
    }

    /**
     * @return <code>true</code> if taglet has parameters, <code>false</code> otherwise.
     * @see #getAllowedParameterNames()
     */
    public boolean hasAnnotationParameters()
    {
        return getAllowedParameterNames() != null;
    }

    /**
     * @param tag not null.
     * @return a not null String or <code>null</code> if no annotation value was found.
     */
    private String getTagValue( Tag tag )
    {
        if ( tag == null )
        {
            throw new IllegalArgumentException( "tag should be not null" );
        }

        String text = tag.text();
        if ( isEmpty( text ) )
        {
            // using pattern: @annotation
            return null;
        }

        String tagValue = null;
        StringTokenizer token = new StringTokenizer( text, " " );
        while ( token.hasMoreTokens() )
        {
            String nextToken = token.nextToken();

            if ( nextToken.indexOf( '=' ) == -1 )
            {
                // using pattern: @annotation <annotationValue>
                tagValue = nextToken;
            }
        }

        return tagValue;
    }

    /**
     * @param tag not null.
     * @return a not null MutableAttributeSet.
     */
    private MutableAttributeSet getTagAttributes( Tag tag )
    {
        if ( tag == null )
        {
            throw new IllegalArgumentException( "tag should be not null" );
        }

        String text = tag.text();

        StringTokenizer token = new StringTokenizer( text, " " );
        MutableAttributeSet tagAttributes = new SimpleAttributeSet();
        while ( token.hasMoreTokens() )
        {
            String nextToken = token.nextToken();

            if ( nextToken.indexOf( '=' ) == -1 )
            {
                // using pattern: @annotation <annotationValue>
                continue;
            }

            StringTokenizer token2 = new StringTokenizer( nextToken, "=" );
            if ( token2.countTokens() != 2 )
            {
                System.err.println( "The annotation '" + tag.name() + "' has no name/value pairs parameter: "
                    + tag.name() + " " + text + " (" + tag.position().file() + ":" + tag.position().line() + ":"
                    + tag.position().column() + ")" );
                tagAttributes.addAttribute( token2.nextToken(), "" );
                continue;
            }

            String name = token2.nextToken();
            String value = token2.nextToken().replaceAll( "\"", "" );

            if ( getAllowedParameterNames() != null && !Arrays.asList( getAllowedParameterNames() ).contains( name ) )
            {
                System.err.println( "The annotation '" + tag.name() + "' has wrong parameter name: " + tag.name() + " "
                    + text + " (" + tag.position().file() + ":" + tag.position().line() + ":" + tag.position().column()
                    + ")" );
            }

            tagAttributes.addAttribute( name, value );
        }

        return tagAttributes;
    }

    /**
     * Append a tag
     *
     * @param sb not null
     * @param tag not null
     * @param tagAttributes not null
     * @param tagValue not null
     */
    private void appendTag( StringBuilder sb, Tag tag, MutableAttributeSet tagAttributes, String tagValue )
    {
        if ( !hasAnnotationParameters() )
        {
            if ( tagAttributes.getAttributeCount() > 0 )
            {
                System.err.println( "The annotation '@" + getName() + "' should have no attribute ("
                    + tag.position().file() + ":" + tag.position().line() + ":" + tag.position().column() + ")" );
            }

            if ( hasAnnotationValue() )
            {
                sb.append( "<DT><B>" ).append( getHeader() ).append( ":</B></DT>" );
                if ( isEveryValues( getAllowedValue() ) )
                {
                    if ( isNotEmpty( tagValue ) )
                    {
                        sb.append( "<DD>" ).append( tagValue ).append( "</DD>" );
                    }
                    else
                    {
                        System.err.println( "The annotation '@" + getName() + "' is specified to have a value but "
                            + "no value is defined (" + tag.position().file() + ":" + tag.position().line() + ":"
                            + tag.position().column() + ")" );
                        sb.append( "<DD>" ).append( "NOT DEFINED" ).append( "</DD>" );
                    }
                }
                else
                {
                    List<String> l = getOnlyValues( getAllowedValue() );
                    if ( isNotEmpty( tagValue ) )
                    {
                        if ( l.contains( tagValue ) )
                        {
                            sb.append( "<DD>" ).append( tagValue ).append( "</DD>" );
                        }
                        else
                        {
                            System.err.println( "The annotation '@" + getName() + "' is specified to be a value of "
                                + l + " (" + tag.position().file() + ":" + tag.position().line() + ":"
                                + tag.position().column() + ")" );
                            sb.append( "<DD>" ).append( tagValue ).append( "</DD>" );
                        }
                    }
                    else
                    {
                        sb.append( "<DD>" ).append( l.get( 0 ) ).append( "</DD>" );
                    }
                }
            }
            else
            {
                if ( isNotEmpty( tagValue ) )
                {
                    System.err.println( "The annotation '@" + getName() + "' should have no value ("
                        + tag.position().file() + ":" + tag.position().line() + ":" + tag.position().column() + ")" );
                }
                sb.append( "<DT><B>" ).append( getHeader() ).append( "</B></DT>" );
                sb.append( "<DD></DD>" );
            }
        }
        else
        {
            if ( hasAnnotationValue() )
            {
                sb.append( "<DT><B>" ).append( getHeader() ).append( ":</B></DT>" );
                if ( isEveryValues( getAllowedValue() ) )
                {
                    if ( isNotEmpty( tagValue ) )
                    {
                        sb.append( "<DD>" ).append( tagValue );
                    }
                    else
                    {
                        System.err.println( "The annotation '@" + getName() + "' is specified to have a value but "
                            + "no value is defined (" + tag.position().file() + ":" + tag.position().line() + ":"
                            + tag.position().column() + ")" );
                        sb.append( "<DD>" ).append( "NOT DEFINED" );
                    }
                }
                else
                {
                    List<String> l = getOnlyValues( getAllowedValue() );
                    if ( isNotEmpty( tagValue ) )
                    {
                        if ( l.contains( tagValue ) )
                        {
                            sb.append( "<DD>" ).append( tagValue );
                        }
                        else
                        {
                            System.err.println( "The annotation '@" + getName() + "' is specified to be a value in "
                                + l + " (" + tag.position().file() + ":" + tag.position().line() + ":"
                                + tag.position().column() + ")" );
                            sb.append( "<DD>" ).append( tagValue );
                        }
                    }
                    else
                    {
                        sb.append( "<DD>" ).append( l.get( 0 ) );
                    }
                }
            }
            else
            {
                if ( isNotEmpty( tagValue ) )
                {
                    System.err.println( "The annotation '@" + getName() + "' should have no value ("
                        + tag.position().file() + ":" + tag.position().line() + ":" + tag.position().column() + ")" );
                }
                sb.append( "<DT><B>" ).append( getHeader() ).append( ":</B></DT>" );
                sb.append( "<DD>" );
            }

            appendAnnotationParameters( sb, tagAttributes );
            sb.append( "</DD>" );
        }
    }

    /**
     * Append the annotation parameters as a definition list.
     *
     * @param sb not null
     * @param att not null
     */
    private static void appendAnnotationParameters( StringBuilder sb, MutableAttributeSet att )
    {
        sb.append( "<DL>" );

        Enumeration<?> names = att.getAttributeNames();
        while ( names.hasMoreElements() )
        {
            Object key = names.nextElement();
            Object value = att.getAttribute( key );

            if ( value instanceof AttributeSet )
            {
                // ignored
            }
            else
            {
                sb.append( "<DT><B>" ).append( key ).append( ":</B></DT>" );
                sb.append( "<DD>" ).append( value ).append( "</DD>" );
            }
        }

        sb.append( "</DL>" );
    }

    /**
     * @param text not null
     * @return <code>true</code> if text contains <code>*</code>, <code>false</code> otherwise.
     */
    private static boolean isEveryValues( String text )
    {
        return text.trim().equals( "*" );
    }

    /**
     * Splits the provided text into a array, using pipe as the separator.
     *
     * @param text not null
     * @return a list of parsed Strings or <code>Collections.EMPTY_LIST</code>.
     * By convention, the default value is the first element.
     */
    private static List<String> getOnlyValues( String text )
    {
        if ( text.indexOf( '|' ) == -1 )
        {
            return Collections.emptyList();
        }

        List<String> l = new ArrayList<String>();
        StringTokenizer token = new StringTokenizer( text, "|" );
        while ( token.hasMoreTokens() )
        {
            l.add( token.nextToken() );
        }

        return l;
    }

    /**
     * <p>Checks if a String is non <code>null</code> and is
     * not empty (<code>length > 0</code>).</p>
     *
     * @param str the String to check
     * @return true if the String is non-null, and not length zero
     */
    private static boolean isNotEmpty( String str )
    {
        return ( str != null && str.length() > 0 );
    }

    /**
     * <p>Checks if a (trimmed) String is <code>null</code> or empty.</p>
     *
     * @param str the String to check
     * @return <code>true</code> if the String is <code>null</code>, or
     *  length zero once trimmed
     */
    private static boolean isEmpty( String str )
    {
        return ( str == null || str.trim().length() == 0 );
    }
}
