/*
 * 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.
 *
 * $Header:$
 */
package org.apache.beehive.netui.core.urltemplates;

import org.apache.beehive.netui.util.internal.InternalStringBuilder;
import org.apache.beehive.netui.util.logging.Logger;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;


/**
 * The class to format a URL defined by url-template-config template
 * given by values for a set of tokens.
 */
public class URLTemplate implements Serializable
{
    private static final Logger _log = Logger.getInstance( URLTemplate.class );

    private static final char BEGIN_TOKEN_QUALIFIER = '{';
    private static final char END_TOKEN_QUALIFIER = '}';

    // The String form of the template.
    private String _template;
    private String _name;

    // Parsed representation of the template... list of literals and tokens.
    private ArrayList/*< TemplateItem >*/ _parsedTemplate = null;

    private boolean _isParsed = false;

    private HashMap/*< String, String >*/ _tokenValuesMap = new HashMap/*< String, String >*/();

    /**
     * Create a URLTemplate from a url-template-config template.
     *
     * @param template the string form of the template from url-template-config.
     */
    public URLTemplate( String template )
    {
        setTemplate( template );
    }

    /**
     * Create a URLTemplate from a url-template-config template.
     *
     * @param template the string form of the template from url-template-config.
     * @param name the name of the template
     */
    public URLTemplate( String template, String name )
    {
        setTemplate( template );
        setName( name );
    }

    /**
     * Copy constructor to create a URLTemplate from an existing URLTemplate.
     *
     * <p> Note that this is not truly a complete copy because the Map
     * of the replacement values for the given tokens is not copied.
     * This copy will just have an empty map of token values so that
     * it is "cleared" and ready to format another URL. </p>
     *
     * @param template the URLTemplate to copy.
     */
    public URLTemplate( URLTemplate template )
    {
        setTemplate( template.getTemplate() );
        setName( template.getName() );
        _parsedTemplate = template._parsedTemplate;
        _isParsed = template._isParsed;
    }

    /**
     * Reset the String form of the template.
     *
     * <p> Should call verify after setting a new template. </p>
     *
     * @param template the string form of the template from url-template-config.
     */
    public void setTemplate( String template )
    {
        if ( template == null || template.length() == 0 )
        {
            throw new IllegalStateException( "Template cannot be null or empty." );
        }

        if ( template.equals( _template ) )
        {
            return;
        }

        _template = template;
        _isParsed = false;
        _parsedTemplate = null;
    }

    /**
     * Retrieve the String form of the template.
     *
     * @return the string form of the template.
     */
    public String getTemplate()
    {
        return _template;
    }

    public void setName( String name )
    {
        _name = name;
    }

    public String getName()
    {
        return _name;
    }

    /**
     * Verification will ensure the URL template conforms to a valid format
     * for known tokens and contains the required tokens. It will also parse
     * the tokens and literal data into a list to improve the replacement
     * performance when constructing the final URL string.
     *
     * <p> Allow clients to define a set of required and known tokens for the
     * template verification. Tokens are expected to be qualified
     * in braces. E.g. {url:path} </p>
     *
     * <p> If the template does not contain the required tokens or if the
     * format of a known token is incorrect, this method will log the error
     * and return false. </p>
     *
     * <p> Should call verify after creating a new template. </p>
     *
     * @param knownTokens the collection of known tokens (Strings) for a valid template.
     * @param requiredTokens the collection of required tokens (Strings) in a valid template.
     * @return true if the template conforms to a valid format, otherwise return false.
     */
    public boolean verify( Collection knownTokens, Collection requiredTokens )
    {
        boolean valid = true;
        // For each known token, make sure there is a leading and trailing brace
        if ( knownTokens != null )
        {
            for ( java.util.Iterator ii = knownTokens.iterator(); ii.hasNext(); )
            {
                String token = ( String ) ii.next();
                if ( token != null && token.length() > 2 )
                {
                    // Strip braces from the known token
                    token = token.substring( 1, token.length() - 1 );
                    int index = _template.indexOf( token );
                    if ( index != -1 )
                    {
                        if ( _template.charAt( index - 1 ) != BEGIN_TOKEN_QUALIFIER
                                || _template.charAt( index + token.length() ) != END_TOKEN_QUALIFIER )
                        {
                            _log.error( "Template token, " + token
                                    + ", is not correctly enclosed with braces in template: " + _template );
                            valid = false;
                        }
                    }
                }
            }
        }

        // Parse the template into tokens and literals
        parseTemplate();

        // Check if the required tokens are present
        if ( requiredTokens != null )
        {
            for ( java.util.Iterator ii = requiredTokens.iterator(); ii.hasNext(); )
            {
                String token = ( String ) ii.next();
                TemplateItem requiredItem = new TemplateItem( token, true );

                if ( !_parsedTemplate.contains( requiredItem ) )
                {
                    _log.error( "Required token, " + token + ", not found in template: " + _template );
                    valid = false;
                }
            }
        }

        return valid;
    }

    private void parseTemplate()
    {
        if ( _isParsed )
        {
            return;
        }

        _parsedTemplate = new ArrayList/*< TemplateItem >*/();
        TemplateTokenizer tokenizer = new TemplateTokenizer( getTemplate() );
        for ( ; tokenizer.hasNext(); )
        {
            boolean isToken = tokenizer.isTokenNext();
            String tokenOrLiteral = ( String ) tokenizer.next();
            if ( tokenOrLiteral.equals( "" ) )
            {
                continue;
            }

            TemplateItem item = new TemplateItem( tokenOrLiteral, isToken );
            _parsedTemplate.add( item );
        }

        _isParsed = true;
    }

    /**
     * Replace a set of tokens in the template with a corresponding set of values.
     * This assumes that there is an ordered one-to-one relationship. Tokens are
     * expected to be qualified in braces. E.g. {url:path}
     */
    public void substitute( Map/*< String, String >*/ tokensAndValues )
    {
        if ( tokensAndValues != null )
        {
            _tokenValuesMap.putAll( tokensAndValues );
        }
    }

    /**
     * Replace a single token in the template with a corresponding String value.
     * Tokens are expected to be qualified in braces. E.g. {url:path}
     */
    public void substitute( String token, String value )
    {
        _tokenValuesMap.put( token, value );
    }

    /**
     * Replace a single token in the template with a corresponding int value.
     * Tokens are expected to be qualified in braces. E.g. {url:port}
     */
    public void substitute( String token, int value )
    {
        String valueStr = Integer.toString( value );
        _tokenValuesMap.put( token, valueStr );
    }

    /**
     * Return the String representation of the URL after replacing
     * the tokens in the template with their associated values. If
     * there is no value for a token, the token is discarded/removed.
     * I.E. It will not be part of the returned String.
     *
     * @return the url
     */
    public String toString()
    {
        return format( true );
    }

    /**
     * Return the String representation of the URL after replacing
     * the tokens in the template with their associated values. If
     * there is no value for a token, the token is discarded/removed.
     * I.E. It will not be part of the returned String.
     *
     * @return the url
     */
    public String format()
    {
        return format( true );
    }

    /**
     * Return the String representation of the URL after replacing
     * the tokens in the template with their associated values.
     * If the boolean argument is <code>true</code>, then the unset
     * template tokens are removed. Otherwise, do not cleanup
     * the unset tokens.
     *
     * @param removeUnsetTokens flag to tell URLTemplate to remove
     *        or leave the unset tokens in the URL.
     * @return the url
     */
    public String format( boolean removeUnsetTokens )
    {
        // template should already have been parsed with a call to
        if ( !_isParsed )
        {
            // Parse the template into tokens and literals
            parseTemplate();
        }

        InternalStringBuilder result = new InternalStringBuilder( _template.length() + 16 );
        for ( java.util.Iterator ii = _parsedTemplate.iterator(); ii.hasNext(); )
        {
            TemplateItem item = ( TemplateItem ) ii.next();
            if ( item.isToken() )
            {
                if ( _tokenValuesMap.containsKey( item.getValue() ) )
                {
                    appendToResult( result, ( String ) _tokenValuesMap.get( item.getValue() ) );
                }
                else
                {
                    // No value for the token.
                    if ( !removeUnsetTokens )
                    {
                        // treat the token as a literal
                        appendToResult( result, item.getValue() );
                    }
                }
            }
            else
            {
                appendToResult( result, item.getValue() );
            }
        }

        if ( result.length() > 0 && result.charAt( result.length() - 1 ) == '?' )
        {
            result.deleteCharAt( result.length() - 1 );
        }

        return result.toString();
    }

    // check to make sure we don't end up with "//" between components
    // of the URL
    protected void appendToResult( InternalStringBuilder result, String value )
    {
        if ( value == null || value.length() == 0 )
        {
            return;
        }

        if ( result.length() > 0 && result.charAt( result.length() - 1 ) == '/'
                && value.charAt( 0 ) == '/' )
        {
            result.deleteCharAt( result.length() - 1 );
        }
        result.append( value );
    }

    protected class TemplateItem implements Serializable
    {
        private String value;
        private boolean isToken = false;

        public TemplateItem( String value, boolean isToken )
        {
            assert value != null : "TemplateItem value cannot be null.";
            this.value = value;
            this.isToken = isToken;
        }

        public String getValue()
        {
            return value;
        }

        public boolean isToken()
        {
            return isToken;
        }

        public boolean equals( Object o )
        {
            if ( this == o )
            {
                return true;
            }
            if ( !( o instanceof TemplateItem ) )
            {
                return false;
            }

            final TemplateItem templateItem = ( TemplateItem ) o;

            if ( isToken != templateItem.isToken() )
            {
                return false;
            }
            if ( !value.equals( templateItem.getValue() ) )
            {
                return false;
            }

            return true;
        }

        public int hashCode()
        {
            int result;
            result = value.hashCode();
            result = 29 * result + ( isToken ? 1 : 0 );
            return result;
        }
    }
}
