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

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.io.Serializable;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.ranides.assira.generic.CompareUtils;
import net.ranides.assira.generic.HashUtils;
import net.ranides.assira.reflection.ResolveException;

/**
 *
 * Składnia wzorców jest identyczna jak składnia z MessageFormat.
 * Podajemy kolejno:
 *      { ArgumentIndex, Format, Pattern  }
 * 
 * ARGUMENT:
 *      ArgumentIndex jest naszym wyrażeniem
 *      . służy jako separator
 *      [ ] służy jako indeks (transformowane na separator)
 * 
 * FORMATS:
 *  {number} DecimalFormat | integer | currency | percent
 * 
 *  {metric} DecimalFormat
 *  {metric/e} DecimalFormat
 *  {metric/si} DecimalFormat
 *  
 *  {binary} DecimalFormat
 *  {binary/e} DecimalFormat
 *  {binary/iec} DecimalFormat
 *  {binary/knuth} DecimalFormat
 *
 *  {date} SimpleDateFormat | short | medium | long | full | iso | gnu
 *
 *  {time} SimpleDateFormat | short | medium | long | full | iso | gnu
 *
 *  {choice,ChoiceFormat}
 *
 *  {hex} array | matrix
 * 
 * @author Ranides Atterwim <ranides@gmail.com>
 */
@SuppressFBWarnings("SE_BAD_FIELD")
public class ResolveFormat implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    private static final Pattern  REGEX_SPECIAL = Pattern.compile("([\\\\*+\\[\\]()\\$.?\\^|])");
    
    private static final Pattern RE_COMPILE = Pattern.compile("\\{(.+?)(?:,(.+?))?(?:,(.+?))?\\}");
    
    private final ResolveDialect dialect;
    
    private final String pattern;
    
    private final Token[] tokens;
    
    private final Token[] parsers;
    
    private final Pattern scanre;
    
    ResolveFormat(ResolveDialect dialect, String pattern) {
        this.dialect = dialect;
        this.pattern = pattern;

        List<Token> itokens = new ArrayList<>();
        List<Token> iparsers = new ArrayList<>();
        Matcher hit = RE_COMPILE.matcher(pattern);
        int last = 0;
        while(hit.find()) {
            if(hit.start()-last > 0) {
                String text = pattern.substring(last,hit.start());
                itokens.add(new FString(text));
            }
            Token token = dialect.get(hit.group(1), hit.group(2), hit.group(3));
            itokens.add(token);
            iparsers.add(token);
            
            last = hit.end();
        }
        if(last<pattern.length()) {
            String text = pattern.substring(last);
            itokens.add(new FString(text));
        }
        this.tokens = itokens.toArray(new Token[itokens.size()]);
        this.parsers = iparsers.toArray(new Token[iparsers.size()]);
        
        int[] index = new int[]{0};
        
        String p = REGEX_SPECIAL.matcher(pattern).replaceAll("\\\\$1");
        this.scanre = Pattern.compile(StringUtils.replace(p, RE_COMPILE, h -> {
            return parsers[index[0]++].regex();
        }));
    }
    
    public static ResolveFormat compile(String pattern) {
        return ResolveDialect.getDefault().compile(pattern);
    }
    
    public static ResolveFormat compile(String dialect, String pattern) {
        return ResolveDialect.getInstance(dialect).compile(pattern);
    }
    
    public String pattern() {
        return pattern;
    }
    
    public ResolveDialect dialect() {
        return dialect;
    }
    
    public String format(Object context) {
        try {
            return format(new StrBuilder(), context).toString();
        } catch(IOException cause) {
            throw new AssertionError("unreachable code", cause);
        }
    }
    
    public String vformat(Object... arguments) {
        return format((Object)arguments);
    }
    
    public <T extends Appendable> T format(T output, Object context) throws IOException {
        for(Token token : tokens) {
            token.append(output, context);
        }
        return output;
    }
    
    public <T extends Appendable> T vformat(T output, Object... arguments) throws IOException {
        return format(output, (Object)arguments);
    }
    
    public void parse(String input, Object context) {
        Matcher hit = scanre.matcher(input);
        if(!hit.matches()) {
            throw new IllegalArgumentException("Expression does not match pattern: \"" + pattern + "\" expression: \"" + input + "\"nregex: " + scanre.pattern());
        }
        try {
            int i=0;
            for(Token p : parsers) {
                p.parse(hit.group(++i), context);
            }
        } catch(ParseException cause) {
            throw new ResolveException(cause);
        }
    }
    
    private Object writeReplace() {
        return new SResolveFormat(dialect.name(), pattern);
    }

    @Override
    public int hashCode() {
        return HashUtils.hashPair(dialect, pattern);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final ResolveFormat other = (ResolveFormat) obj;
        return 
            CompareUtils.equals(this.dialect, other.dialect) &&
            CompareUtils.equals(this.pattern, other.pattern);
    }
       
    
    
    interface Token {

        String regex();
        
        void append(Appendable target, Object context) throws IOException;

        void parse(String source, Object context) throws ParseException;

    }
    
    private static final class FString implements ResolveFormat.Token {
        
        private final String value;

        public FString(String value) {
            this.value = value;
        }

        @Override
        public void append(Appendable target, Object context) throws IOException {
            target.append(value);
        }
        
        @Override
        public String regex() {
            throw new UnsupportedOperationException("Unreachable code.");
        }
            
        @Override
        public void parse(String source, Object context) {
            throw new UnsupportedOperationException("Unreachable code.");
        }
                
    }
    
    @SuppressWarnings("PMD")
    private static class SResolveFormat implements Serializable {

        private static final long serialVersionUID = 1L;

        private final String dialect;
        private final String pattern;

        public SResolveFormat(String dialect, String pattern) {
            this.dialect = dialect;
            this.pattern = pattern;
        }

        private Object readResolve() {
            return compile(dialect, pattern);
        }

    }
    
}
