package com.hammurapi.common;

import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;

/**
 * Expands tokens like $[myToken] to token values.
 * Nested tokens are supported, i.e. if myToken value contains $[someOtherToken]
 * then it will also be expanded. $[ can be escaped by adding extra $, e.g. $$[ will be replaced with $[.
 * Token can contain default value separated from the token name by pipe, e.g. $[myToken|tokenDefaultValue].
 * @author Pavel Vlasov
 */
public class TokenExpander {
	
	/**
	 * Source of tokens.
	 * @author Pavel Vlasov
	 */
	public interface TokenSource {
		
		/**
		 * @param name Token name.
		 * @return Token value or null.
		 */
		String getToken(String name);

	}
		
    public static final String TOKEN_CLOSING_CHAR = "}";
	public static final String TOKEN_SECOND_OPENING_CHAR = "{";
	public static final String TOKEN_FIRST_OPENING_CHAR = "$";
	
    private String tokenClosingChar = TOKEN_CLOSING_CHAR;
	private String tokenSecondOpeningChar = TOKEN_SECOND_OPENING_CHAR;
	private String tokenFirstOpeningChar = TOKEN_FIRST_OPENING_CHAR;
	
	private TokenSource tokens;
    
    /** 
     * Creates a new instance of PropertyParser
     * @param properties Properties
     * @param useNameAsDefault If true then property name will be used
     * as property default value. 
     */
    public TokenExpander(TokenSource tokens) {
        if (tokens==null) {
        	throw new NullPointerException("Tokens map is null");
        }
        this.tokens=tokens;
    }    
    
    /** 
     * Creates a new instance of TokenExpander.
     * @param tokens 
     * @param tokenFirstOpeningChar Token first opening char. Default is $.
     * @param tokenSecondOpeningChar Token second opening char. Default is {.
     * @param tokenClosingChar Token closing char. Default is }.
     */
    public TokenExpander(TokenSource tokens, char tokenFirstOpeningChar, char tokenSecondOpeningChar, char tokenClosingChar) {
    	this(tokens);
        this.tokenClosingChar=String.valueOf(tokenClosingChar);
        this.tokenFirstOpeningChar=String.valueOf(tokenFirstOpeningChar);
        this.tokenSecondOpeningChar=String.valueOf(tokenSecondOpeningChar);
    }
            
    /**
     * Property parsing. 
     * Replaces string ${&lt;property name&gt;} with property value.
     * If property value contains ${&lt;other property name&gt;} it
     * will be parsed. 
     */
    public String expandToken(String token, String defaultValue) throws TokenExpansionException {
        return expandToken(token, defaultValue, new HashSet<String>());
    }
    
    /**
     * Parses a string by replacing occurences of ${&lt;property name&gt;} 
     * with property values.
     * If property value contains ${&lt;other property name&gt;} it
     * will be parsed. 
     */
    public String expand(String str) throws TokenExpansionException {
        return parse(str, null);
    }

    private String expandToken(String token, String defaultValue, Set<String> stack) throws TokenExpansionException {
    	String value = tokens.getToken(token);
        if (value==null) {
        	if (defaultValue==null) {
        		throw new TokenExpansionException("Token not found: "+token);
        	} 
        	value = defaultValue;
        }
        
        if (stack.contains(token)) {
        	throw new TokenExpansionException("Circular reference in token substitution, token: "+token);
        }
        
        stack.add(token);
        
        return parse(value, stack);
    }
    
    private String parse(String str, Set<String> stack) throws TokenExpansionException {
        if (str==null) {
        	return null;
        }
        
        StringTokenizer st=new StringTokenizer(str,tokenFirstOpeningChar+tokenSecondOpeningChar+tokenClosingChar,true);
        
        StringBuilder ret=new StringBuilder();
        
        /**
         * Parser state:
         * 0: Text
         * 1: $
         * 2: ${
         */
        final int TEXT = 0;
        final int OPEN_FIRST = 1;
        final int OPEN_SECOND = 2;
                
        int state=0;
        
        String propTxt=null;
        while (st.hasMoreTokens()) {
            String tkn=st.nextToken();
            switch (state) {
                case TEXT:
                    if (tokenFirstOpeningChar.equals(tkn))
                        state=OPEN_FIRST;
                    else
                        ret.append(tkn);
                    break;
                case OPEN_FIRST:
                    if (tokenSecondOpeningChar.equals(tkn))
                        state=OPEN_SECOND;
                    else {
                        state=TEXT;
                        ret.append(tokenFirstOpeningChar);
                        if (tkn.equals(tokenFirstOpeningChar)) {
                        	String next = st.hasMoreTokens() ? st.nextToken() : null;
                        	if (next==null) {
                        		ret.append(tkn);
                        	} else {
                        		if (!next.equals(tokenSecondOpeningChar)) {
                        			ret.append(tkn);
                        		}
                       			ret.append(next);
                        	}
                        } else {
                        	ret.append(tkn);
                        }
                    }
                    break;
                case OPEN_SECOND:
                    if (tokenClosingChar.equals(tkn)) {
                    	int pipeIdx = propTxt==null ? -1 : propTxt.indexOf('|');                    	
                        String propVal=expandToken(pipeIdx==-1 ? propTxt : propTxt.substring(0, pipeIdx), pipeIdx==-1 ? null : propTxt.substring(pipeIdx+1), stack==null ? new HashSet<String>() : stack);
                        ret.append(propVal);
                        propTxt=null;
                        state=0;
                    } else {
                        if (propTxt==null) {
                            propTxt=tkn;
                        } else {
                            propTxt+=tkn;
                        }
                    }
                    break;
                default:
                    throw new IllegalStateException("Illegal parser state: "+state);
            }
        }
        
        if (state==OPEN_SECOND) {
            ret.append(tokenFirstOpeningChar+tokenSecondOpeningChar+propTxt);
        }
        
        return ret.toString();
    }        
}
