package net.ranides.assira.math;

import lombok.EqualsAndHashCode;

import java.math.BigDecimal;
import java.math.BigInteger;

@EqualsAndHashCode
public class NumberPair {

    private final Number a;
    private final Number b;

    private static final Class<?>[] type2real = {
        Float.class,        // 0
        Float.class,        // 1
        Float.class,        // 2
        Double.class,       // 3
        BigDecimal.class    // 4
    };

    private static final Class<?>[] type2int = {
        Byte.class,         // 0
        Short.class,        // 1
        Integer.class,      // 2
        Long.class,         // 3
        BigInteger.class    // 4
    };

    public NumberPair(Number a, Number b) {
        this.a = a;
        this.b = b;
    }

    public static NumberPair of(Number a, Number b) {
        return new NumberPair(a,b);
    }

    private static int real2type(Class<?> a) {
        if(Float.class.equals(a)) {
            return 2;
        }
        if(Double.class.equals(a)) {
            return 3;
        }
        if(BigDecimal.class.equals(a)) {
            return 4;
        }
        return -1;
    }

    private static int int2type(Class<?> a) {
        if(Byte.class.equals(a)) {
            return 0;
        }
        if(Short.class.equals(a)) {
            return 1;
        }
        if(Integer.class.equals(a)) {
            return 2;
        }
        if(Long.class.equals(a)) {
            return 3;
        }
        if(BigInteger.class.equals(a)) {
            return 4;
        }
        return -1;
    }

    private static Class<? extends Number> wider(Class<?> a, Class<?> b) {
        int ai = int2type(a);
        int bi = int2type(b);
        int mi = Math.max(ai, bi);

        if (ai != -1 && bi != -1) {
            return (Class)type2int[mi];
        }

        int af = real2type(a);
        int bf = real2type(b);
        int mf = Math.max(af, bf);

        return (Class)type2real[Math.max(mi,mf)];
    }

    private static <T extends Number> T cast(Class<T> target, Number value) {
        if(target.isInstance(value)) {
            return target.cast(value);
        }
        if(Float.class.equals(target)) {
            return target.cast(value.floatValue());
        }
        if(Double.class.equals(target)) {
            return target.cast(value.doubleValue());
        }
        if(BigDecimal.class.equals(target)) {
            return target.cast(asBigDecimal(value));
        }

        if(Byte.class.equals(target)) {
            return target.cast(value.byteValue());
        }
        if(Short.class.equals(target)) {
            return target.cast(value.shortValue());
        }
        if(Integer.class.equals(target)) {
            return target.cast(value.intValue());
        }
        if(Long.class.equals(target)) {
            return target.cast(value.longValue());
        }
        if(BigInteger.class.equals(target)) {
            return target.cast(BigInteger.valueOf(value.longValue()));
        }

        return target.cast(value);
    }

    private static BigDecimal asBigDecimal(Number value) {
        if(value instanceof BigInteger) {
            return new BigDecimal(((BigInteger) value));
        }
        if(value instanceof Float || value instanceof Double) {
            return new BigDecimal(value.doubleValue());
        }
        return new BigDecimal(value.longValue());
    }

    public NumberPair widen() {
        Class<? extends Number> target = wider(a.getClass(), b.getClass());
        return new NumberPair(cast(target, a), cast(target, b));
    }

    public Number add() {
        Class<? extends Number> target = wider(a.getClass(), b.getClass());

        if(Float.class.equals(target)) {
            return a.floatValue() + b.floatValue();
        }
        if(Double.class.equals(target)) {
            return a.doubleValue() + b.doubleValue();
        }
        if(BigDecimal.class.equals(target)) {
            return asBigDecimal(a).add(asBigDecimal(b));
        }
        if(Byte.class.equals(target)) {
            return a.byteValue() + b.byteValue();
        }
        if(Short.class.equals(target)) {
            return a.shortValue() + b.shortValue();
        }
        if(Integer.class.equals(target)) {
            return a.intValue() + b.intValue();
        }
        if(Long.class.equals(target)) {
            return a.longValue() + b.longValue();
        }
        if(BigInteger.class.equals(target)) {
            return BigInteger.valueOf(a.longValue()).add(BigInteger.valueOf(b.longValue()));
        }

        throw new UnsupportedOperationException("Can't add " + a.getClass() + " & " + b.getClass());
    }

    public Number subtract() {
        Class<? extends Number> target = wider(a.getClass(), b.getClass());

        if(Float.class.equals(target)) {
            return a.floatValue() - b.floatValue();
        }
        if(Double.class.equals(target)) {
            return a.doubleValue() - b.doubleValue();
        }
        if(BigDecimal.class.equals(target)) {
            return asBigDecimal(a).subtract(asBigDecimal(b));
        }
        if(Byte.class.equals(target)) {
            return a.byteValue() - b.byteValue();
        }
        if(Short.class.equals(target)) {
            return a.shortValue() - b.shortValue();
        }
        if(Integer.class.equals(target)) {
            return a.intValue() - b.intValue();
        }
        if(Long.class.equals(target)) {
            return a.longValue() - b.longValue();
        }
        if(BigInteger.class.equals(target)) {
            return BigInteger.valueOf(a.longValue()).subtract(BigInteger.valueOf(b.longValue()));
        }

        throw new UnsupportedOperationException("Can't subtract " + a.getClass() + " & " + b.getClass());
    }

    public Number multiply() {
        Class<? extends Number> target = wider(a.getClass(), b.getClass());

        if(Float.class.equals(target)) {
            return a.floatValue() * b.floatValue();
        }
        if(Double.class.equals(target)) {
            return a.doubleValue() * b.doubleValue();
        }
        if(BigDecimal.class.equals(target)) {
            return asBigDecimal(a).multiply(asBigDecimal(b));
        }
        if(Byte.class.equals(target)) {
            return a.byteValue() * b.byteValue();
        }
        if(Short.class.equals(target)) {
            return a.shortValue() * b.shortValue();
        }
        if(Integer.class.equals(target)) {
            return a.intValue() * b.intValue();
        }
        if(Long.class.equals(target)) {
            return a.longValue() * b.longValue();
        }
        if(BigInteger.class.equals(target)) {
            return BigInteger.valueOf(a.longValue()).multiply(BigInteger.valueOf(b.longValue()));
        }

        throw new UnsupportedOperationException("Can't multiply " + a.getClass() + " & " + b.getClass());
    }

    public Number divide() {
        Class<? extends Number> target = wider(a.getClass(), b.getClass());

        if(Float.class.equals(target)) {
            return a.floatValue() / b.floatValue();
        }
        if(Double.class.equals(target)) {
            return a.doubleValue() / b.doubleValue();
        }
        if(BigDecimal.class.equals(target)) {
            return asBigDecimal(a).divide(asBigDecimal(b));
        }
        if(Byte.class.equals(target)) {
            return a.byteValue() / b.byteValue();
        }
        if(Short.class.equals(target)) {
            return a.shortValue() / b.shortValue();
        }
        if(Integer.class.equals(target)) {
            return a.intValue() / b.intValue();
        }
        if(Long.class.equals(target)) {
            return a.longValue() / b.longValue();
        }
        if(BigInteger.class.equals(target)) {
            return BigInteger.valueOf(a.longValue()).divide(BigInteger.valueOf(b.longValue()));
        }

        throw new UnsupportedOperationException("Can't add " + a.getClass() + " & " + b.getClass());
    }


}
