package com.github.mygreen.supercsv.cellprocessor.format;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
 * 書式を指定した数値のフォーマッタ。
 * 
 * @since 1.2
 * @author T.TSUCHIE
 *
 */
public class NumberFormatWrapper<T extends Number> extends AbstractTextFormatter<T> {
    
    private final NumberFormat formatter;
    
    private final Class<T> type;
    
    private final boolean lenient;
    
    public NumberFormatWrapper(final NumberFormat formatter, final Class<T> type) {
        this(formatter, type, false);
    }
    
    public NumberFormatWrapper(final NumberFormat formatter, final Class<T> type, final boolean lenient) {
        Objects.requireNonNull(formatter);
        Objects.requireNonNull(type);
        
        this.formatter = (NumberFormat) formatter.clone();
        this.type = type;
        this.lenient = lenient;
        
    }
    
    @Override
    public synchronized String print(final Number number) {
        return formatter.format(number);
    }
    
    @Override
    public T parse(final String text) {
        return parse(type, text);
    }
    
    /**
     * 指定した数値のクラスに文字列をパースする。
     * <p>Java標準のクラスタイプをサポートします。</p>
     * 
     * @param type 変換する数値のクラス。
     * @param text パース対象の文字列。
     * @return パースした数値のオブジェクト。
     * @throws IllegalArgumentException サポートしていないクラスタイプが指定された場合。
     * @throws TextParseException fail convert Number or BigDecimal.
     */
    @SuppressWarnings("unchecked")
    synchronized <N extends Number> N parse(final Class<N> type, final String text) {
        
        final Number result;
        if(lenient) {
            try {
                result = formatter.parse(text);
            } catch(ParseException e) {
                throw new TextParseException(text, type, e);
            }
        } else {
            ParsePosition position = new ParsePosition(0);
            result = formatter.parse(text, position);
            
            if(position.getIndex() != text.length()) {
                throw new TextParseException(text, type, String.format("Cannot parse '%s' using fromat %s", text, getPattern()));
            }
        }
        
        try {
            if(result instanceof BigDecimal) {
                // if set DecimalFormat#setParseBigDecimal(true)
                return (N) convertWithBigDecimal(type, (BigDecimal) result, text);
                
            } else {
                return (N) convertWithNumber(type, result, text);
            }
        } catch(NumberFormatException | ArithmeticException e) {
            throw new TextParseException(text, type, e);
        }
        
    }
    
    private Number convertWithNumber(final Class<? extends Number> type, final Number number, final String str) {
        
        if(Byte.class.isAssignableFrom(type) || byte.class.isAssignableFrom(type)) {
            return number.byteValue();
            
        } else if(Short.class.isAssignableFrom(type) || short.class.isAssignableFrom(type)) {
            return number.shortValue() ;
            
        } else if(Integer.class.isAssignableFrom(type) || int.class.isAssignableFrom(type)) {
            return number.intValue();
            
        } else if(Long.class.isAssignableFrom(type) || long.class.isAssignableFrom(type)) {
            return number.longValue();
            
        } else if(Float.class.isAssignableFrom(type) || float.class.isAssignableFrom(type)) {
            return number.floatValue();
            
        } else if(Double.class.isAssignableFrom(type) || double.class.isAssignableFrom(type)) {
            return number.doubleValue();
            
        } else if(type.isAssignableFrom(BigInteger.class)) {
            return new BigInteger(str);
            
        } else if(type.isAssignableFrom(BigDecimal.class)) {
            return new BigDecimal(str);
            
        }
        
        throw new IllegalArgumentException(String.format("not support class type : %s", type.getCanonicalName()));
    }
    
    private Number convertWithBigDecimal(final Class<? extends Number> type, final BigDecimal number, final String str) {
        
        if(Byte.class.isAssignableFrom(type) || byte.class.isAssignableFrom(type)) {
            return lenient ? number.byteValue() : number.byteValueExact();
            
        } else if(Short.class.isAssignableFrom(type) || short.class.isAssignableFrom(type)) {
            return lenient ? number.shortValue() : number.shortValueExact();
            
        } else if(Integer.class.isAssignableFrom(type) || int.class.isAssignableFrom(type)) {
            return lenient ? number.intValue() : number.intValueExact();
            
        } else if(Long.class.isAssignableFrom(type) || long.class.isAssignableFrom(type)) {
            return lenient ? number.longValue() : number.longValueExact();
            
        } else if(Float.class.isAssignableFrom(type) || float.class.isAssignableFrom(type)) {
            return number.floatValue();
            
        } else if(Double.class.isAssignableFrom(type) || double.class.isAssignableFrom(type)) {
            return number.doubleValue();
            
        } else if(type.isAssignableFrom(BigInteger.class)) {
            return lenient ? number.toBigInteger() : number.toBigIntegerExact();
            
        } else if(type.isAssignableFrom(BigDecimal.class)) {
            return number;
            
        }
        
        throw new IllegalArgumentException(String.format("not support class type : %s", type.getCanonicalName()));
        
    }
    
    @Override
    public Optional<String> getPattern() {
        
        if(formatter instanceof DecimalFormat) {
            DecimalFormat df = (DecimalFormat) formatter;
            return Optional.of(df.toPattern());
        }
        
        return Optional.empty();
        
    }
    
    /**
     * パースする際に、数値に変換可能な部分のみ変換するかどうか。
     * <p>例えば、trueのときは、{@literal 123abc} をパースする際に{@literal 123}を数値としてパースします。
     *   <br>falseの場合は、例外{@link TextParseException}をスローします。
     * </p>
     * @return trueの場合、曖昧にパースします。
     */
    public boolean isLenient() {
        return lenient;
    }
    
    @Override
    public Map<String, Object> getMessageVariables() {
        
        final Map<String, Object> vars = new HashMap<>();
        getPattern().ifPresent(p -> vars.put("pattern", p));
        
        return vars;
    }
    
}