package org.codehaus.plexus.interpolation.multi;

/*
 * Copyright 2001-2009 Codehaus Foundation.
 *
 * 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.
 */

import org.codehaus.plexus.interpolation.InterpolationCycleException;
import org.codehaus.plexus.interpolation.InterpolationException;
import org.codehaus.plexus.interpolation.InterpolationPostProcessor;
import org.codehaus.plexus.interpolation.Interpolator;
import org.codehaus.plexus.interpolation.RecursionInterceptor;
import org.codehaus.plexus.interpolation.SimpleRecursionInterceptor;
import org.codehaus.plexus.interpolation.ValueSource;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class MultiDelimiterStringSearchInterpolator
    implements Interpolator
{

    private static final int MAX_TRIES = 10;
    
    private Map existingAnswers = new HashMap();

    private List<ValueSource> valueSources = new ArrayList<ValueSource>();

    private List postProcessors = new ArrayList();

    private boolean cacheAnswers = false;

    private LinkedHashSet<DelimiterSpecification> delimiters = new LinkedHashSet<DelimiterSpecification>();

    private String escapeString;

    public MultiDelimiterStringSearchInterpolator()
    {
        delimiters.add( DelimiterSpecification.DEFAULT_SPEC );
    }
    
    public MultiDelimiterStringSearchInterpolator addDelimiterSpec( String delimiterSpec )
    {
        if ( delimiterSpec == null )
        {
            return this;
        }
        delimiters.add( DelimiterSpecification.parse( delimiterSpec ) );
        return this;
    }
    
    public boolean removeDelimiterSpec( String delimiterSpec )
    {
        if ( delimiterSpec == null )
        {
            return false;
        }        
        return delimiters.remove( DelimiterSpecification.parse( delimiterSpec ) );
    }
    
    public MultiDelimiterStringSearchInterpolator withValueSource( ValueSource vs )
    {
        addValueSource( vs );
        return this;
    }
    
    public MultiDelimiterStringSearchInterpolator withPostProcessor( InterpolationPostProcessor postProcessor )
    {
        addPostProcessor( postProcessor );
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public void addValueSource( ValueSource valueSource )
    {
        valueSources.add( valueSource );
    }

    /**
     * {@inheritDoc}
     */
    public void removeValuesSource( ValueSource valueSource )
    {
        valueSources.remove( valueSource );
    }

    /**
     * {@inheritDoc}
     */
    public void addPostProcessor( InterpolationPostProcessor postProcessor )
    {
        postProcessors.add( postProcessor );
    }

    /**
     * {@inheritDoc}
     */
    public void removePostProcessor( InterpolationPostProcessor postProcessor )
    {
        postProcessors.remove( postProcessor );
    }

    public String interpolate( String input, String thisPrefixPattern )
        throws InterpolationException
    {
        return interpolate( input, new SimpleRecursionInterceptor() );
    }

    public String interpolate( String input, String thisPrefixPattern, RecursionInterceptor recursionInterceptor )
        throws InterpolationException
    {
        return interpolate( input, recursionInterceptor );
    }

    public String interpolate( String input )
        throws InterpolationException
    {
        return interpolate( input, new SimpleRecursionInterceptor() );
    }

    /**
     * Entry point for recursive resolution of an expression and all of its nested expressions.
     * 
     * TODO: Ensure unresolvable expressions don't trigger infinite recursion.
     */
    public String interpolate( String input, RecursionInterceptor recursionInterceptor )
        throws InterpolationException
    {
        try
        {
            return interpolate( input, recursionInterceptor, new HashSet() );
        }
        finally
        {
            if ( !cacheAnswers )
            {
                existingAnswers.clear();
            }
        }
    }

    private String interpolate( String input, RecursionInterceptor recursionInterceptor, Set<String> unresolvable )
        throws InterpolationException
    {
        if ( input == null )
        {
            // return empty String to prevent NPE too
            return "";
        }
        StringBuilder result = new StringBuilder( input.length() * 2 );

        String lastResult = input;
        int tries = 0;
        do
        {
            tries++;
            if ( result.length() > 0 )
            {
                lastResult = result.toString();
                result.setLength( 0 );
            }
            
            int startIdx = -1;
            int endIdx = -1;
            
            DelimiterSpecification selectedSpec = null;
            while( ( selectedSpec = select( input, endIdx ) ) != null )
            {
                String startExpr = selectedSpec.getBegin();
                String endExpr = selectedSpec.getEnd();
                
                startIdx = selectedSpec.getNextStartIndex();
                result.append( input, endIdx + 1, startIdx );

                endIdx = input.indexOf( endExpr, startIdx + 1 );
                if ( endIdx < 0 )
                {
                    break;
                }

                String wholeExpr = input.substring( startIdx, endIdx + endExpr.length() );
                String realExpr = wholeExpr.substring( startExpr.length(), wholeExpr.length() - endExpr.length() );

                if ( startIdx >= 0 && escapeString != null && escapeString.length() > 0 )
                {
                    int startEscapeIdx = ( startIdx == 0 ) ? 0 : startIdx - escapeString.length();
                    if ( startEscapeIdx >= 0 )
                    {
                        String escape = input.substring( startEscapeIdx, startIdx );
                        if ( escape != null && escapeString.equals( escape ) )
                        {
                            result.append( wholeExpr );
                            if ( startEscapeIdx > 0 )
                            {
                                --startEscapeIdx;
                            }
                            result.replace( startEscapeIdx, startEscapeIdx + escapeString.length(), "" );
                            continue;
                        }
                    }
                }

                boolean resolved = false;
                if ( !unresolvable.contains( wholeExpr ) )
                {
                    if ( realExpr.startsWith( "." ) )
                    {
                        realExpr = realExpr.substring( 1 );
                    }

                    if ( recursionInterceptor.hasRecursiveExpression( realExpr ) )
                    {
                        throw new InterpolationCycleException( recursionInterceptor, realExpr, wholeExpr );
                    }

                    recursionInterceptor.expressionResolutionStarted( realExpr );

                    Object value = existingAnswers.get( realExpr );
                    Object bestAnswer = null;
                    for ( ValueSource vs : valueSources )
                    {
                        if (value != null ) break;

                        value = vs.getValue( realExpr );

                        if ( value != null && value.toString().contains( wholeExpr ) )
                        {
                            bestAnswer = value;
                            value = null;
                        }
                    }

                    // this is the simplest recursion check to catch exact recursion
                    // (non synonym), and avoid the extra effort of more string
                    // searching.
                    if ( value == null && bestAnswer != null )
                    {
                        throw new InterpolationCycleException( recursionInterceptor, realExpr, wholeExpr );
                    }

                    if ( value != null )
                    {
                        value = interpolate( String.valueOf( value ), recursionInterceptor, unresolvable );

                        if ( postProcessors != null && !postProcessors.isEmpty() )
                        {
                            for ( Object postProcessor1 : postProcessors )
                            {
                                InterpolationPostProcessor postProcessor = (InterpolationPostProcessor) postProcessor1;
                                Object newVal = postProcessor.execute( realExpr, value );
                                if ( newVal != null )
                                {
                                    value = newVal;
                                    break;
                                }
                            }
                        }

                        // could use:
                        // result = matcher.replaceFirst( stringValue );
                        // but this could result in multiple lookups of stringValue, and replaceAll is not correct behaviour
                        result.append( String.valueOf( value ) );
                        resolved = true;
                    }
                    else
                    {
                        unresolvable.add( wholeExpr );
                    }

                    recursionInterceptor.expressionResolutionFinished( realExpr );
                }

                if ( !resolved )
                {
                    result.append( wholeExpr );
                }

                if ( endIdx > -1 )
                {
                    endIdx += endExpr.length() - 1;
                }
            }

            if ( endIdx == -1 && startIdx > -1 )
            {
                result.append( input, startIdx, input.length() );
            }
            else if ( endIdx < input.length() )
            {
                result.append( input, endIdx + 1, input.length() );
            }
        }
        while( !lastResult.equals( result.toString() ) && tries < MAX_TRIES );

        return result.toString();
    }

    private DelimiterSpecification select( String input, int lastEndIdx )
    {
        DelimiterSpecification selected = null;

        for ( DelimiterSpecification spec : delimiters )
        {
            spec.clearNextStart();

            if ( selected == null )
            {
                int idx = input.indexOf( spec.getBegin(), lastEndIdx + 1 );
                if ( idx > -1 )
                {
                    spec.setNextStartIndex( idx );
                    selected = spec;
                }
            }
        }
        
        return selected;
    }

    /**
     * Return any feedback messages and errors that were generated - but suppressed - during the interpolation process.
     * Since unresolvable expressions will be left in the source string as-is, this feedback is optional, and will only
     * be useful for debugging interpolation problems.
     * 
     * @return a {@link List} that may be interspersed with {@link String} and {@link Throwable} instances.
     */
    public List getFeedback()
    {
        List messages = new ArrayList();
        for ( ValueSource vs : valueSources )
        {
            List feedback = vs.getFeedback();
            if ( feedback != null && !feedback.isEmpty() )
            {
                messages.addAll( feedback );
            }
        }

        return messages;
    }

    /**
     * Clear the feedback messages from previous interpolate(..) calls.
     */
    public void clearFeedback()
    {
        for ( ValueSource vs : valueSources )
        {
            vs.clearFeedback();
        }
    }

    public boolean isCacheAnswers()
    {
        return cacheAnswers;
    }

    public void setCacheAnswers( boolean cacheAnswers )
    {
        this.cacheAnswers = cacheAnswers;
    }

    public void clearAnswers()
    {
        existingAnswers.clear();
    }

    public String getEscapeString()
    {
        return escapeString;
    }

    public void setEscapeString( String escapeString )
    {
        this.escapeString = escapeString;
    }

    public MultiDelimiterStringSearchInterpolator escapeString( String escapeString )
    {
        this.escapeString = escapeString;
        return this;
    }
    
    public MultiDelimiterStringSearchInterpolator setDelimiterSpecs( LinkedHashSet<String> specs )
    {
        delimiters.clear();
        for ( String spec : specs )
        {
            if ( spec == null )
            {
                continue;
            }
            delimiters.add( DelimiterSpecification.parse( spec ) );
        }
        
        return this;
    }

}
