/*
 ***** BEGIN LICENSE BLOCK *****
 * Version: EPL 2.0/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Eclipse Public
 * License Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.eclipse.org/legal/epl-v20.html
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * Copyright (C) 2017 JRuby Community
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the EPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the EPL, the GPL or the LGPL.
 ***** END LICENSE BLOCK *****/

package org.jruby.ext.securerandom;

import org.jruby.RubyBignum;
import org.jruby.RubyFixnum;
import org.jruby.RubyFloat;
import org.jruby.RubyInteger;
import org.jruby.RubyRange;
import org.jruby.RubyString;
import org.jruby.anno.JRubyMethod;
import org.jruby.anno.JRubyModule;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ConvertBytes;

import java.math.BigInteger;
import java.security.SecureRandom;

/**
 * securerandom.rb native parts.
 */
@JRubyModule(name = "SecureRandom")
public class RubySecureRandom {

    @JRubyMethod(meta = true, name = "random_bytes")
    public static IRubyObject random_bytes(ThreadContext context, IRubyObject self) {
        return RubyString.newStringNoCopy(context.runtime, nextBytes(context, 16));
    }

    @JRubyMethod(meta = true, name = "random_bytes")
    public static IRubyObject random_bytes(ThreadContext context, IRubyObject self, IRubyObject n) {
        return RubyString.newStringNoCopy(context.runtime, nextBytes(context, n));
    }

    @JRubyMethod(meta = true, name = {"gen_random", "bytes"})
    public static IRubyObject gen_random(ThreadContext context, IRubyObject self, IRubyObject n) {
        return random_bytes(context, self, n);
    }

    @JRubyMethod(meta = true)
    public static IRubyObject hex(ThreadContext context, IRubyObject self) {
        return RubyString.newStringNoCopy(context.runtime, ConvertBytes.twosComplementToHexBytes(nextBytes(context, 16), false));
    }

    @JRubyMethod(meta = true)
    public static IRubyObject hex(ThreadContext context, IRubyObject self, IRubyObject n) {
        return RubyString.newStringNoCopy(context.runtime, ConvertBytes.twosComplementToHexBytes(nextBytes(context, n), false));
    }

    @JRubyMethod(meta = true)
    public static IRubyObject uuid(ThreadContext context, IRubyObject self) {
        return RubyString.newStringNoCopy(context.runtime, ConvertBytes.bytesToUUIDBytes(nextBytes(context, 16), false));
    }

    private static byte[] nextBytes(ThreadContext context, IRubyObject n) {
        return nextBytes(context, n.isNil() ? 16 : n.convertToInteger().getIntValue());
    }

    private static byte[] nextBytes(ThreadContext context, int size) {
        if (size < 0) throw context.runtime.newArgumentError("negative argument: " + size);

        byte[] bytes = new byte[size];
        getSecureRandom(context).nextBytes(bytes);

        return bytes;
    }

    @JRubyMethod(meta = true, name = {"random_number", "rand"})
    public static IRubyObject random_number(ThreadContext context, IRubyObject self) {
        return randomDouble(context);
    }

    @JRubyMethod(meta = true, name = {"random_number", "rand"})
    public static IRubyObject random_number(ThreadContext context, IRubyObject self, IRubyObject n) {
        if (n instanceof RubyFixnum) {
            final long bound = ((RubyFixnum) n).getLongValue();
            return ( bound < 0 ) ? randomDouble(context) : randomFixnum(context, 0, bound - 1);
        }
        if (n instanceof RubyFloat) {
            final double bound = ((RubyFloat) n).getDoubleValue();
            return ( bound < 0 ) ? randomDouble(context) : randomDouble(context, 0, bound - Double.MIN_VALUE);
        }
        if (n instanceof RubyBignum) {
            final BigInteger bound = ((RubyBignum) n).getBigIntegerValue();
            return ( bound.signum() < 0 ) ? randomDouble(context) : randomBignum(context, 0, bound);
        }

        if (n instanceof RubyRange) {
            final IRubyObject beg = ((RubyRange) n).begin(context);
            final IRubyObject end = ((RubyRange) n).end(context);
            final boolean exclude = ((RubyRange) n).isExcludeEnd();

            if (beg instanceof RubyFixnum && end instanceof RubyFixnum) {
                long lower = ((RubyFixnum) beg).getLongValue();
                long upper = ((RubyFixnum) end).getLongValue();
                if ( lower > upper ) return randomDouble(context);
                if ( exclude ) upper--; // rand(2) never returns 2 but rand(0..2) does
                return randomFixnum(context, lower, upper);
            }
            if (beg instanceof RubyInteger && end instanceof RubyInteger) {
                BigInteger lower = ((RubyInteger) beg).getBigIntegerValue();
                BigInteger upper = ((RubyInteger) end).getBigIntegerValue();
                if ( lower.compareTo(upper) > 0 ) return randomDouble(context);
                if ( ! exclude ) upper = upper.add(BigInteger.ONE);
                return randomBignum(context, lower, upper);
            }
            if (beg instanceof RubyFloat && end instanceof RubyFloat) {
                double lower = ((RubyFloat) beg).getDoubleValue();
                double upper = ((RubyFloat) end).getDoubleValue();
                if ( lower > upper ) return randomDouble(context);
                if ( exclude ) upper = upper - Double.MIN_VALUE;
                return randomDouble(context, lower, upper);
            }
        }

        throw context.runtime.newArgumentError("invalid argument - " + n.anyToString());
    }

    private static RubyFixnum randomFixnum(final ThreadContext context, final long lower, final long upper) {
        double rnd = getSecureRandom(context).nextDouble();
        rnd = rnd * upper + (1.0 - rnd) * lower + rnd;
        return context.runtime.newFixnum((long) Math.floor(rnd));
    }

    // NOTE: upper is exclusive here compared to others
    private static RubyBignum randomBignum(final ThreadContext context, final Number lower, final BigInteger upperExc) {
        BigInteger lowerBig = lower instanceof BigInteger ? (BigInteger) lower : BigInteger.valueOf(lower.longValue());
        BigInteger bound = upperExc.subtract(lowerBig);
        BigInteger rnd = nextBigInteger(getSecureRandom(context), bound, bound.bitLength());
        return RubyBignum.newBignum(context.runtime, rnd.add(lowerBig));
    }

    private static final int BI_ADD_BITS = 96; // 8+4 random bytes 'wasted'

    // <0, bound)
    private static BigInteger nextBigInteger(final SecureRandom random, final BigInteger bound, final int bits) {
        BigInteger val = new BigInteger(bits + BI_ADD_BITS, random);
        BigInteger rnd = val.mod(bound);

        if (val.add(bound).subtract(rnd).subtract(BigInteger.ONE).bitLength() >= bits + BI_ADD_BITS) {
            return nextBigInteger(random, bound, bits); // highly unlikely to recurse at all
        }
        return rnd;
    }

    private static RubyFloat randomDouble(final ThreadContext context, final double lower, final double upper) {
        double rnd = getSecureRandom(context).nextDouble();
        return context.runtime.newFloat( rnd * upper + (1.0 - rnd) * lower );
    }

    private static RubyFloat randomDouble(final ThreadContext context) {
        return context.runtime.newFloat( getSecureRandom(context).nextDouble() );
    }

    private static SecureRandom getSecureRandom(ThreadContext context) {
        return context.getSecureRandom();
    }

}
