/*
 * @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.math.BigDecimal;
import java.math.BigInteger;
import java.text.*;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.ranides.assira.collection.maps.Cache;
import net.ranides.assira.collection.maps.HashMap;
import net.ranides.assira.generic.CompareUtils;
import net.ranides.assira.generic.HashUtils;
import net.ranides.assira.generic.ValueUtils;
import net.ranides.assira.reflection.IAttribute;
import net.ranides.assira.reflection.IClass;
import net.ranides.assira.reflection.ResolvePattern;
import net.ranides.assira.reflection.ResolveStrategy;
import net.ranides.assira.text.FormatNumber.UnitPrefix;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public final class ResolveDialect {
    
    private static final char[] HC_TABLE = {
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
    };
    
    private static final class Li { // NOPMD - lazy init idiom
        
        public static final Cache<String, ResolveDialect> DIALECTS = new Cache<>(32,4);
        
        public static final ResolveDialect DEFAULT = new ResolveDialect(Locale.getDefault().getCountry());
    
    }
    
    private final String name;
    
    private final Cache<String, ResolveFormat> cache = new Cache<>(64,8);
    
    private final Map<TokenKey,Function<ResolvePattern,ResolveFormat.Token>> mDefined = new HashMap<>();
    
    private final Map<String,BiFunction<ResolvePattern,String,ResolveFormat.Token>> mPattern = new HashMap<>();
    
    
    ResolveDialect(String name) {
        this.name = name;
        Locale locale = new Locale(name);
        DecimalFormatSymbols us = DecimalFormatSymbols.getInstance(Locale.US);
        
        putFormat("", "", r -> new FGeneric(r));
        
        // number       : 2 + 3      5
        // date         : 2 + 8     10
        // time         : 2 + 8     10
        // hex          : 1 + 2      3
        // metric       : 1 + 4      5
        // binary       : 1 + 6      7
        //              :           40 x language
        
        putNumber(us, locale);
        putDate(locale);
        putTime(locale);
        putHex();
        putMetric(us);
        putBinary(us);
        putChoice();
    }

    private void putNumber(DecimalFormatSymbols us, Locale locale) {
        putFormat("number",          (r,p) -> new FNumber(r, new DecimalFormat(p,us)));
        putFormat("number", "",          r -> new FNumber(r, DecimalFormat.getInstance(locale)));
        putFormat("number", "integer",   r -> new FNumber(r, DecimalFormat.getIntegerInstance(locale)));
        putFormat("number", "c++",       r -> new FNumber(r, getDecimalFormat(Locale.US, '.','\'')));
        putFormat("number", "java",      r -> new FNumber(r, getDecimalFormat(Locale.US, '.','_')));
        putFormat("number", "currency",  r -> new FNumber(r, getCurrencyFormat(locale)));
        putFormat("number", "percent",   r -> new FNumber(r, DecimalFormat.getPercentInstance(locale)));
    }

    private void putDate(Locale locale) {
        putFormat("date",            (r,p) -> new FDateTime(r, new SimpleDateFormat(p,locale)));
        putFormat("date", "",            r -> new FDateTime(r, DateFormat.getDateInstance(DateFormat.DEFAULT,locale)));
        putFormat("date", "short",       r -> new FDateTime(r, DateFormat.getDateInstance(DateFormat.SHORT,locale)));
        putFormat("date", "medium",      r -> new FDateTime(r, DateFormat.getDateInstance(DateFormat.MEDIUM,locale)));
        putFormat("date", "long",        r -> new FDateTime(r, DateFormat.getDateInstance(DateFormat.LONG,locale)));
        putFormat("date", "full",        r -> new FDateTime(r, DateFormat.getDateInstance(DateFormat.FULL,locale)));
        putFormat("date", "iso",         r -> new FDateTime8(r, "yyyy-MM-dd'T'HH:mmX"));
        putFormat("date", "gnu",         r -> new FDateTime(r, new SimpleDateFormat("yyyyMMddHHmm",Locale.ROOT)));
        putFormat("date", "trace",       r -> new FDateTime(r, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS",Locale.ROOT)));
        putFormat("date", "local",       r -> new FDateTime(r, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss",Locale.ROOT)));
    }
    
    private void putTime(Locale locale) {
        putFormat("time",            (r,p) -> new FDateTime(r, new SimpleDateFormat(p,locale)));
        putFormat("time", "",            r -> new FDateTime(r, DateFormat.getTimeInstance(DateFormat.DEFAULT,locale)));
        putFormat("time", "short",       r -> new FDateTime(r, DateFormat.getTimeInstance(DateFormat.SHORT,locale)));
        putFormat("time", "medium",      r -> new FDateTime(r, DateFormat.getTimeInstance(DateFormat.MEDIUM,locale)));
        putFormat("time", "long",        r -> new FDateTime(r, DateFormat.getTimeInstance(DateFormat.LONG,locale)));
        putFormat("time", "full",        r -> new FDateTime(r, DateFormat.getTimeInstance(DateFormat.FULL,locale)));
        putFormat("time", "iso",         r -> new FDateTime8(r, "HH:mm:ssX"));
        putFormat("time", "gnu",         r -> new FDateTime(r, new SimpleDateFormat("HHmmss",Locale.ROOT)));
        putFormat("time", "trace",       r -> new FDateTime(r, new SimpleDateFormat("HH:mm:ss.SSS",Locale.ROOT)));
        putFormat("time", "local",       r -> new FDateTime(r, new SimpleDateFormat("HH:mm:ss",Locale.ROOT)));
    }
    
    private void putHex() {
        putFormat("hex", "",             r -> new FHexArray(r));
        putFormat("hex", "array",        r -> new FHexArray(r));
        putFormat("hex", "stream",       r -> new FHexStream(r));
    }
    
    private void putMetric(DecimalFormatSymbols us) {
        putFormat("metric",          (r,p) -> new FEngineer(r, new DecimalFormat(p,us), FormatNumber.MP));
        putFormat("metric/si",       (r,p) -> new FEngineer(r, new DecimalFormat(p,us), FormatNumber.MP));
        putFormat("metric/e",        (r,p) -> new FEngineer(r, new DecimalFormat(p,us), FormatNumber.ME));
        putFormat("metric", "",          r -> new FEngineer(r, DecimalFormat.getInstance(Locale.US), FormatNumber.MP));
        putFormat("metric/si", "",       r -> new FEngineer(r, DecimalFormat.getInstance(Locale.US), FormatNumber.MP));
        putFormat("metric/e", "",        r -> new FEngineer(r, DecimalFormat.getInstance(Locale.US), FormatNumber.ME));
    }

    private void putBinary(DecimalFormatSymbols us) {
        putFormat("binary",          (r,p) -> new FEngineer(r, new DecimalFormat(p,us), FormatNumber.BP));
        putFormat("binary/iec",      (r,p) -> new FEngineer(r, new DecimalFormat(p,us), FormatNumber.BP));
        putFormat("binary/e",        (r,p) -> new FEngineer(r, new DecimalFormat(p,us), FormatNumber.BE));
        putFormat("binary/knuth",    (r,p) -> new FEngineer(r, new DecimalFormat(p,us), FormatNumber.BK));
        putFormat("binary", "",          r -> new FEngineer(r, DecimalFormat.getInstance(Locale.US), FormatNumber.BP));
        putFormat("binary/iec", "",      r -> new FEngineer(r, DecimalFormat.getInstance(Locale.US), FormatNumber.BP));
        putFormat("binary/e", "",        r -> new FEngineer(r, DecimalFormat.getInstance(Locale.US), FormatNumber.BE));
        putFormat("binary/knuth", "",    r -> new FEngineer(r, DecimalFormat.getInstance(Locale.US), FormatNumber.BK));
    }
    
    private void putChoice() {
        putFormat("choice",          (r,p) -> new FChoice(r, new ChoiceFormat(p)));
    }
    

    public static ResolveDialect getDefault() {
        return Li.DEFAULT;
    }
    
    public static ResolveDialect getInstance(String dialect) {
        return Li.DIALECTS.get(dialect, () -> new ResolveDialect(dialect));
    }
    
    public String name() {
        return name;
    }
    
    public ResolveFormat compile(String pattern) {
        return cache.get(pattern, ()->new ResolveFormat(this, pattern));
    }

    @Override
    public int hashCode() {
        return name.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final ResolveDialect that = (ResolveDialect) obj;
        return CompareUtils.equals(this.name, that.name);
    }
    
    
    private void putFormat(String format, String pattern, Function<ResolvePattern,ResolveFormat.Token> token) {
        mDefined.put(new TokenKey(format, pattern), token);
    }
    
    private void putFormat(String format, BiFunction<ResolvePattern,String,ResolveFormat.Token> token) {
        mPattern.put(format, token);
    }
    
    ResolveFormat.Token get(String expression, String format, String pattern) {
        format = ValueUtils.or(format,"");
        pattern = ValueUtils.or(pattern,"");

        Function<ResolvePattern, ResolveFormat.Token> a = mDefined.get(new TokenKey(format, pattern));
        if(null != a) {
            return a.apply(ResolvePattern.compile(ResolveStrategy.FORMAT, expression));
        }
        BiFunction<ResolvePattern,String,ResolveFormat.Token> b = mPattern.get(format);
        if(null != b) {
            return b.apply(ResolvePattern.compile(ResolveStrategy.FORMAT, expression), pattern);
        }
        throw new UnsupportedOperationException("Not supported format pattern: " + format + "," + pattern);
    }
    
    private static NumberFormat getCurrencyFormat(Locale locale) {
        DecimalFormat nf = (DecimalFormat)DecimalFormat.getCurrencyInstance(locale);
        DecimalFormatSymbols fs = nf.getDecimalFormatSymbols();
        fs.setCurrencySymbol("");
        nf.setDecimalFormatSymbols(fs);
        return nf;
    }
    
    private DecimalFormat getDecimalFormat(Locale locale, char dseparator, char gseparator) {
        DecimalFormat fmt = (DecimalFormat)DecimalFormat.getInstance(locale);
        DecimalFormatSymbols dfs = fmt.getDecimalFormatSymbols();
        dfs.setDecimalSeparator(dseparator);
        dfs.setGroupingSeparator(gseparator);
        fmt.setGroupingUsed(true);
        fmt.setDecimalFormatSymbols(dfs);
        return fmt;
    }
    
    private static StrBuffer asHex(StrBuffer target, byte value) {
        int ivalue = value & 0xFF;
        target.append( HC_TABLE[ivalue >>> 4] );
        target.append( HC_TABLE[ivalue & 0xF] );
        return target;
    }
    
    private static final class TokenKey {
        
        private final String format;
        private final String pattern;

        public TokenKey(String format, String pattern) {
            this.format = format;
            this.pattern = pattern;
        }

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

        @Override
        public boolean equals(Object object) {
            if(!(object instanceof TokenKey)) {
                return false;
            }
            TokenKey that = (TokenKey) object;
            return CompareUtils.equals(this.format, that.format) && CompareUtils.equals(this.pattern, that.pattern);
        }
        
    }
    
    private static abstract class RToken implements ResolveFormat.Token {
        
        protected final ResolvePattern iresolver;

        public RToken(ResolvePattern iresolver) {
            this.iresolver = iresolver;
        }
        
        protected Object get(Object context) {
            return iresolver.get(context);
        }
        
        protected <T> T get(Object context, Class<T> type) {
            return LexicalCast.cast(iresolver.get(context), type);
        }
        
        protected void set(Object context, Object value) {
            iresolver.set(context, LexicalCast.cast(value, iresolver.type(context)));
        }

        @Override
        public final void parse(String source, Object context) throws ParseException {
            set(context, parse(source));
        }
        
        public abstract Object parse(String source) throws ParseException;
    }
    
    private static final class FNumber extends RToken {
        
        private final NumberFormat format;

        public FNumber(ResolvePattern resolver, NumberFormat nf) {
            super(resolver);
            this.format = nf;
        }

        @Override
        public String regex() {
            if(format instanceof DecimalFormat) {
                DecimalFormatSymbols dfs = ((DecimalFormat)format).getDecimalFormatSymbols();
                StrBuilder s = new StrBuilder();
                s.append("(");
                s.append(dfs.getMinusSign()).append("?");
                digits(s, dfs);
                s.append("+");
                s.append("(?:")
                    .append(dfs.getExponentSeparator())
                    .append("|")
                    .append(dfs.getExponentSeparator().toLowerCase(Locale.ROOT))
                    .append(")?");
                digits(s, dfs);
                s.append("*");
                s.append("%?");
                s.append(")");
                return s.toString();
            } else {
                return "((?:0x)?[0-9.,'A-Fa-f]+)";
            }
        }

        private StrBuilder digits(StrBuilder s, DecimalFormatSymbols dfs) {
            s.append("[0-9");
            s.append(dfs.getDecimalSeparator());
            s.append(dfs.getGroupingSeparator());
            s.append("]");
            return s;
        }

        @Override
        public void append(Appendable target, Object context) throws IOException {
            target.append(format.format(get(context)));
        }

        @Override
        public Object parse(String source) throws ParseException {
            return format.parse(source);
        }
        
    }
    
    private static final class FDateTime extends RToken {
        
        private final DateFormat format;

        public FDateTime(ResolvePattern resolver, DateFormat nf) {
            super(resolver);
            this.format = nf;
            this.format.setTimeZone(TimeZone.getTimeZone("UTC"));
        }

        @Override
        public String regex() {
            return "(.+)";
        }

        @Override
        public void append(Appendable target, Object context) throws IOException {
            target.append(format.format(get(context,Date.class)));
        }

        @Override
        public Object parse(String source) throws ParseException {
            return format.parse(source);
        }
        
    }
    
    private static final class FDateTime8 extends RToken {

        private final DateTimeFormatter format;

        public FDateTime8(ResolvePattern resolver, String pattern) {
            super(resolver);
            this.format = DateTimeFormatter.ofPattern(pattern).withZone(ZoneOffset.UTC);
        }
        
        @Override
        public String regex() {
            return "(.+)";
        }

        @Override
        public void append(Appendable target, Object context) throws IOException {
            format.formatTo(get(context, Instant.class), target);
        }

        @Override
        public Object parse(String source) {
            return LocalDateTime.parse(source, format);
        }

    }
    
    
    
    private static final class FChoice extends RToken {
        
        private final ChoiceFormat format;

        public FChoice(ResolvePattern resolver, ChoiceFormat nf) {
            super(resolver);
            this.format = nf;
        }
        
        @Override
        public String regex() {
            return "(.+)";
        }

        @Override
        public void append(Appendable target, Object context) throws IOException {
            target.append(format.format(get(context)));
        }

        @Override
        public Object parse(String source) throws ParseException {
            return format.parse(source);
        }
        
    }
    
    private static final class FHexArray extends RToken {
        
        private static final Pattern RE_PARSE = Pattern.compile("\\[([0-9]+)\\]\\{(.*)\\}");
        
        public FHexArray(ResolvePattern resolver) {
            super(resolver);
        }
        
        @Override
        public String regex() {
            return "(\\[[0-9]+\\]\\{(?:0x[0-9a-fA-F]{2}?)?(?:,0x[0-9a-fA-F]{2})*\\})";
        }

        @Override
        public void append(Appendable target, Object context) throws IOException {
            byte[] value = get(context, byte[].class);
            
            if(0 == value.length) {
                return;
            }
            StrBuffer out = new StrBuffer(5 * value.length);
            asHex(out, value[0]);
            for(int i=1; i<value.length; i++) {
                out.append(',');
                out.append('0');
                out.append('x');
                asHex(out, value[i]);
            }
            target
                .append('[')
                .append(String.valueOf(value.length))
                .append("]{")
                .append(out)
                .append("}");
        }

        @Override
        public Object parse(String source) throws ParseException {
            Matcher hit = RE_PARSE.matcher(source);
            if( !hit.matches() ) {
                throw new ParseException("Value is not byte array: " + source, 0);
            }
            int size = Integer.parseInt(hit.group(1));
            byte[] array = new byte[size];
            int index = 0 ;
            for(String v : hit.group(2).split(",")) {
                array[index++] = Byte.parseByte(v.substring(2), 16);
            }
            return array;
        }
        

    }
    
    private static final class FHexStream extends RToken {
        
        public FHexStream(ResolvePattern resolver) {
            super(resolver);
        }
        
        @Override
        public String regex() {
            return "((?:[0-9A-Fa-f]{2})?(?: [0-9A-Fa-f]{2})*)";
        }

        @Override
        public void append(Appendable target, Object context) throws IOException {
            byte[] value = get(context, byte[].class);
            
            if(0 == value.length) {
                return;
            }
            StrBuffer out = new StrBuffer(3 * value.length);
            asHex(out, value[0]);
            for(int i=1; i<value.length; i++) {
                out.append(' ');
                asHex(out, value[i]);
            }
            target.append(out);
        }

        @Override
        public Object parse(String source) {
            String[] ss = source.split(" ");
            int n = ss.length;
            byte[] array = new byte[n];
            for(int i=0; i<n; i++) {
                array[i] = Byte.parseByte(ss[i], 16);
            }
            return array;
        }
        
    }
    
    private static final class FEngineer implements ResolveFormat.Token {
        
        private final ResolvePattern iresolver;
        private final NumberFormat nf;
        private final UnitPrefix[] units;
        private final String re;
        private final Pattern regex;
        
        public FEngineer(ResolvePattern resolver, NumberFormat nf, UnitPrefix[] units) {
            this.iresolver = resolver;
            this.nf = nf;
            this.units = units;
            this.re = "([0-9]+(?:\\.[0-9]+)?(?:" + StringUtils.join(units, "|", b -> b.text()) + "))";
            this.regex = Pattern.compile("([0-9]+(?:\\.[0-9]+)?)(" + StringUtils.join(units, "|", b -> b.text()) + ")");
        }

        @Override
        public String regex() {
            return re;
        }
        
        @Override
        public void append(Appendable target, Object context) throws IOException {
            Object object = iresolver.get(context);
            if (object instanceof BigInteger) {
                BigInteger value = (BigInteger) object;
                if(value.bitLength()<64) {
                    vappend(target, value.longValue());
                } else {
                    vappend(target, new BigDecimal(value));
                }
                return;
            }
            if (object instanceof BigDecimal) {
                vappend(target, (BigDecimal)object);
                return;
            }
            IClass<?> type = IClass.typefor(object);
            if(IAttribute.INTEGER.matches(type)) {
                vappend(target, ((Number)object).longValue());
                return;
            }
            if(IAttribute.NUMBER.matches(type)) {
                vappend(target, ((Number)object).doubleValue());
                return;
            }
            throw new IllegalArgumentException("Cannot format given Object as a Number");
        }

        @Override
        public void parse(String source, Object context) throws ParseException {
            Matcher hit = regex.matcher(source);
            if(!hit.matches()) {
                throw new IllegalArgumentException("Cannot parse Number: " + source);
            }
            UnitPrefix unit = unit(hit.group(2));
            IClass<?> type = iresolver.type(context);
            
            Object out;
            if (type.isSubclass(IClass.typeinfo(BigInteger.class))) {
                out = new BigDecimal(hit.group(1)).multiply(unit.decimalValue()).toBigInteger();
            }
            else if (type.isSubclass(IClass.typeinfo(BigDecimal.class))) {
                out = new BigDecimal(hit.group(1)).multiply(unit.decimalValue());
            }
            else if(IAttribute.INTEGER.matches(type)) {
                out = LexicalCast.cast(Long.parseLong(hit.group(1)) * unit.longValue(), type);
            }
            else {
                out = LexicalCast.cast(Double.parseDouble(hit.group(1)) * unit.doubleValue(), type);
            }
            iresolver.set(context, out);
        }
        
        private UnitPrefix unit(String name) {
            for(UnitPrefix u : units) {
                if(u.text().equals(name)) {
                    return u;
                }
            }
            throw new IllegalArgumentException("Unknown prefix: " + name);
        }
        

        private void vappend(Appendable target, long value) throws IOException {
            if(value < 0) {
                target.append('-');
                value = -value;
            }
            UnitPrefix c = units[0];
            for (UnitPrefix u : units) {
                c = u;
                if (value > c.doubleValue()) {
                    break;
                }
            }
            target.append(nf.format(value/c.doubleValue())).append(c.text());
        }
        
        private void vappend(Appendable target, double value) throws IOException {
            if(value < 0) {
                target.append('-');
                value = -value;
            }
            UnitPrefix c = units[0];
            for (UnitPrefix u : units) {
                c = u;
                if (value > c.doubleValue()) {
                    break;
                }
            }
            target.append(nf.format(value/c.doubleValue())).append(c.text());
        }

        private void vappend(Appendable target, BigDecimal value) throws IOException {
            if(value.signum() < 0) {
                target.append('-');
                value = value.negate();
            }
            UnitPrefix c = units[0];
            for (UnitPrefix u : units) {
                c = u;
                if( value.compareTo(c.decimalValue()) > 0) {
                    break;
                }
            }
            target.append(nf.format(value.divide(c.decimalValue()).doubleValue())).append(c.text());
        }
        
    }
    
    private static final class FGeneric implements ResolveFormat.Token {
        
        private final ResolvePattern resolver;
        
        public FGeneric(ResolvePattern resolver) {
            this.resolver = resolver;
        }

        @Override
        public String regex() {
            return "(.+)";
        }
        
        @Override
        public void append(Appendable target, Object context) throws IOException {
            target.append(String.valueOf(resolver.get(context)));
        }
        
        @Override
        public void parse(String source, Object context) {
            resolver.set(context, LexicalCast.cast(source, resolver.type(context)));
        }
        
    }
    
}
