/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.text;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.ranides.assira.collection.map.Cache;

/**
 *
 * @author ranides
 * @todo (migration) assira (printf)
 */
public class PrintPatternFactory {
    
    final Map<Character, PrintFunction> functions; 
    
    final Map<String, PrintPattern> cache; 
    
    public PrintPatternFactory() {
        this.functions = new HashMap<>();
        this.cache = new Cache<>();
    }
    
    public PrintPatternFactory(Map<Character, PrintFunction> functions) {
        this.functions = new HashMap<>(functions);
        this.cache = new Cache<>();
    }
    
    public PrintPatternFactory configure(char c, PrintFunction function) {
        functions.put(c, function);
        return this;
    }
    
//    public Appendable formatTo(Appendable target, String format, Object... arguments) throws IOException {
//        return compile(format).formatTo(target, arguments);
//    }
//
//    public String format(String format, Object... arguments) {
//        return compile(format).format(arguments);
//    }
    
    @SuppressWarnings({"PMD.NPathComplexity", "PMD.CyclomaticComplexity"})
    public PrintPattern compile(String format) {
        if( cache.containsKey(format)) {
            return cache.get(format);
        }
        // %...conversion
        // %%
        List<Token> tokens = new ArrayList<>();
        int last = 0;
        int n = format.length();
        int param = 0;
        for(int i=0; i<n; i++) {
            if(format.charAt(i) != '%') { 
                continue; 
            }
            if( i+1 < n && format.charAt(i+1)=='%') {
                i++;
                continue;
            }
            if(last<i && last<n && i<=n) {
                tokens.add(new ConstToken(format.substring(last, i)));
            }
            boolean processed = false;
            for(int c=i+1; c<n; c++) {
                if( functions.containsKey( format.charAt(c) ) ) {
                    tokens.add( token(format.substring(i+1, c+1), param++));
                    i = c+1;
                    last = c+1;
                    processed = true;
                    break;
                }
            }
            if(!processed) {
                throw new IllegalArgumentException("unknown token after [" + i + "] \""+clip(format,i)+"\"");
            }
        }
        if(last<n) {
            tokens.add(new ConstToken(format.substring(last)));
        }
        PrintPattern result = new PrintPattern(tokens.toArray(new Token[tokens.size()]));
        cache.put(format, result);
        return result;
    }
    
    private Token token(String format, int index) {
        char type = format.charAt(format.length()-1);
        String params = format.substring(0, format.length()-1);
        return new FunctionToken(functions.get(type), params, index);
    }
    
    private String clip(String text, int position) {
        return text.substring(Math.max(0, position-2), Math.min(text.length(), position+12));
    }
    
    interface Token {
        
        void apply(Appendable target, Object[] arguments) throws IOException;
        
        int estimate(Object[] arguments);
    }
    
    private static class FunctionToken implements Token {
        
        private final PrintFunction function;
        private final PrintParams params;

        public FunctionToken(PrintFunction function, String params, int index) {
            this.function = function;
            this.params = PrintParams.make(params, index);
        }
        
        @Override
            public void apply(Appendable target, Object[] arguments) throws IOException {
                function.apply(target, params, arguments);
            }

        @Override
        public int estimate(Object[] arguments) {
            return function.estimate(params, arguments);
        }

    }
    
    private static class ConstToken implements Token {
        private final String value;

        public ConstToken(String value) {
            this.value = value;
        }
        @Override
        public void apply(Appendable target, Object[] arguments) throws IOException {
            target.append(value);
        }
        @Override
        public int estimate(Object[] arguments) {
            return value.length();
        }
    }
    
}