package common.rng;

import java.util.List;
import java.util.concurrent.atomic.AtomicLong;

public class Random {
    private static final long MULT = 0x5DEECE66DL;
    private static final long ADDEND = 0xBL;
    private static final long MASK = (1L << 48) - 1;
    private static final double UNIT = 0x1.0p-53; // 1.0 / (1L << 53)
    private static final AtomicLong UNIQ = new AtomicLong(8682522807148012L);
    
	private long seed;
    private double gaussCache;
    private boolean gaussCached = false;

    private static long syncUnique() {
        for(;;) {
            long current = UNIQ.get();
            long next = current * 181783497276652981L;
            if(UNIQ.compareAndSet(current, next))
                return next;
        }
    }

    private static long initScramble(long seed) {
        return (seed ^ MULT) & MASK;
    }

    public Random() {
        this(syncUnique() ^ System.nanoTime());
    }

    public Random(long seed) {
        this.seed = initScramble(seed);
    }

    public void setSeed(long seed) {
        this.seed = initScramble(seed);
        this.gaussCached = false;
    }

    public long getSeed() {
        return this.seed;
    }

    protected int rng(int bits) {
        this.seed = (this.seed * MULT + ADDEND) & MASK;
        return (int)(this.seed >>> (48 - bits));
    }

    public int intv() {
        return this.rng(32);
    }

    public int zrange(int bound) {
        if(bound <= 0)
            throw new IllegalArgumentException("Bereich muss positiv sein");
        int r = this.rng(31);
        int m = bound - 1;
        if((bound & m) == 0) // ^2
            r = (int)((bound * (long)r) >> 31);
        else
            for(int u = r; u - (r = u % bound) + m < 0; u = this.rng(31));
        return r;
    }

    public long longv() {
        return ((long)(this.rng(32)) << 32) + this.rng(32);
    }

    public boolean chance() {
        return this.rng(1) != 0;
    }

    public float floatv() {
        return this.rng(24) / ((float)(1 << 24));
    }

    public double doublev() {
        return (((long)(this.rng(26)) << 27) + this.rng(27)) * UNIT;
    }

    public double gaussian() {
        if(this.gaussCached) {
        	this.gaussCached = false;
            return this.gaussCache;
        }
        else {
            double v1, v2, s;
            do {
                v1 = 2 * doublev() - 1; // between -1 and 1
                v2 = 2 * doublev() - 1; // between -1 and 1
                s = v1 * v1 + v2 * v2;
            }
            while (s >= 1 || s == 0);
            double mul = StrictMath.sqrt(-2 * StrictMath.log(s)/s);
            this.gaussCache = v2 * mul;
            this.gaussCached = true;
            return v1 * mul;
        }
    }
	
	public int excl(int min, int max) {
		return min + this.zrange(max - min);
	}
	
	public int chOffset() {
		return 8 + this.rng(4);
	}
	
	public int roll(int range) {
		return 1 + this.zrange(range);
	}
	
	public int rollBonus(int range, int bonus) {
		return 1 + this.zrange(range) + this.zrange(bonus + 1);
	}
	
	public int range(int min, int max) {
		return min + this.zrange(max + 1 - min);
	}
	
	public int rangeBonus(int min, int max, int bonus) {
		return (min + this.zrange(max + 1 - min)) + this.zrange(bonus + 1);
	}
	
	public int rangeChecked(int min, int max) {
		return min >= max ? min : (this.zrange(max + 1 - min) + min);
	}
	
	public float frange(float min, float max) {
		return min >= max ? min : (this.floatv() * (max - min) + min);
	}
	
	public double drange(double min, double max) {
		return min >= max ? min : (this.doublev() * (max - min) + min);
	}
	
	public boolean chance(int chance) {
		return this.zrange(chance) == 0;
	}
	
	public boolean rarity(int chance) {
		return this.zrange(chance) > 0;
	}
	
	public <T> T chance(T a, T b, int chance) {
		return this.zrange(chance) == 0 ? b : a;
	}
	
	public int chance(int a, int b, int chance) {
		return this.zrange(chance) == 0 ? b : a;
	}
	
	public boolean percent(float chance) {
		return this.floatv() < chance / 100.0f;
	}
	
	public <T> T percent(T a, T b, float chance) {
		return this.floatv() < chance / 100.0f ? b : a;
	}
	
	public <T> T pick(T a, T b) {
		return this.rng(1) != 0 ? b : a;
	}
	
	public <T> T pick(T ... obj) {
		return obj[this.zrange(obj.length)];
	}
	
	public int pick(int ... obj) {
		return obj[this.zrange(obj.length)];
	}
	
	public <T> T pick(List<T> obj) {
		return obj.get(this.zrange(obj.size()));
	}
	
	public <T> T select(T a, T b, int ca, int cb) {
		return this.zrange(ca + cb) >= ca ? b : a;
	}
	
	public <T> T select(T a, T b, float chance) {
		return this.floatv() < chance ? b : a;
	}
}
