//
//  Copyright (c) 2011, Neil Alexander T.
//  Copyright (c) 2013, Threema GmbH.
//  All rights reserved.
//
//  Redistribution and use in source and binary forms, with
//  or without modification, are permitted provided that the following
//  conditions are met:
//
//  - Redistributions of source code must retain the above copyright notice,
//    this list of conditions and the following disclaimer.
//  - Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
//  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
//  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
//  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
//  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
//  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
//  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
//  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
//  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
//  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
//  POSSIBILITY OF SUCH DAMAGE.
//

package com.neilalexander.jnacl.crypto;

public class curve25519
{
	final int CRYPTO_BYTES = 32;
	final int CRYPTO_SCALARBYTES = 32;

	static byte[] basev = { 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
	static int[] minusp = { 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 };

    static boolean haveNative;

    static {
        try {
            System.loadLibrary("nacl-jni");
            haveNative = true;
        } catch (UnsatisfiedLinkError e) {
            haveNative = false;
            System.out.println("Warning: using Java curve25519 implementation; expect bad performance");
        }
    }

	public static int crypto_scalarmult_base(byte[] q, byte[] n)
	{
		byte[] basevp = basev;
		return crypto_scalarmult(q, n, basevp);
	}

	static void add(int[] outv, int outvoffset, int[] a, int aoffset, int[] b, int boffset)
	{
		int u = 0;

		for (int j = 0; j < 31; ++j)
		{
			u += a[aoffset + j] + b[boffset + j];
			outv[outvoffset + j] = u & 255;
			u >>>= 8;
		}

		u += a[aoffset + 31] + b[boffset + 31];
		outv[outvoffset + 31] = u;
	}

	static void sub(int[] outv, int outvoffset, int[] a, int aoffset, int[] b, int boffset)
	{
		int u = 218;

		for (int j = 0; j < 31; ++j)
		{
			u += a[aoffset + j] + 65280 - b[boffset + j];
			outv[outvoffset + j] = u & 255;
			u >>>= 8;
		}

		u += a[aoffset + 31] - b[boffset + 31];
		outv[outvoffset + 31] = u;
	}

	static void squeeze(int[] a, int aoffset)
	{
		int u = 0;

		for (int j = 0; j < 31; ++j)
		{
			u += a[aoffset + j];
			a[aoffset + j] = u & 255;
			u >>>= 8;
		}

		u += a[aoffset + 31];
		a[aoffset + 31] = u & 127;
		u = 19 * (u >>> 7);

		for (int j = 0; j < 31; ++j)
		{
			u += a[aoffset + j];
			a[aoffset + j] = u & 255;
			u >>>= 8;
		}

		u += a[aoffset + 31];
		a[aoffset + 31] = u;
	}

	static void freeze(int[] a, int aoffset)
	{
		int[] aorig = new int[32];

		for (int j = 0; j < 32; ++j)
			aorig[j] = a[aoffset + j];

		int[] minuspp = minusp;

		add(a, 0, a, 0, minuspp, 0);

		int negative = -((a[aoffset + 31] >>> 7) & 1);

		for (int j = 0; j < 32; ++j)
			a[aoffset + j] ^= negative & (aorig[j] ^ a[aoffset + j]);
	}

	static void mult(int[] outv, int outvoffset, int[] a, int aoffset, int[] b, int boffset)
	{
		int j;

		for (int i = 0; i < 32; ++i)
		{
			int u = 0;

			for (j = 0; j <= i; ++j)
				u += a[aoffset + j] * b[boffset + i - j];

			for (j = i + 1; j < 32; ++j)
				u += 38 * a[aoffset + j] * b[boffset + i + 32 - j];

			outv[outvoffset + i] = u;
		}

		squeeze(outv, outvoffset);
	}

	static void mult121665(int[] outv, int[] a)
	{
		int j;
		int u = 0;

		for (j = 0; j < 31; ++j)
		{
			u += 121665 * a[j];
			outv[j] = u & 255;
			u >>>= 8;
		}

		u += 121665 * a[31];
		outv[31] = u & 127;
		u = 19 * (u >>> 7);

		for (j = 0; j < 31; ++j)
		{
			u += outv[j];
			outv[j] = u & 255;
			u >>>= 8;
		}

		u += outv[j];
		outv[j] = u;
	}

	static void square(int[] outv, int outvoffset, int[] a, int aoffset)
	{
		int j;

		for (int i = 0; i < 32; ++i)
		{
			int u = 0;

			for (j = 0; j < i - j; ++j)
				u += a[aoffset + j] * a[aoffset + i - j];

			for (j = i + 1; j < i + 32 - j; ++j)
				u += 38 * a[aoffset + j] * a[aoffset + i + 32 - j];

			u *= 2;

			if ((i & 1) == 0)
			{
				u += a[aoffset + i / 2] * a[aoffset + i / 2];
				u += 38 * a[aoffset + i / 2 + 16] * a[aoffset + i / 2 + 16];
			}

			outv[outvoffset + i] = u;
		}

		squeeze(outv, outvoffset);
	}

	static void select(int[] p, int[] q, int[] r, int[] s, int b)
	{
		int bminus1 = b - 1;

		for (int j = 0; j < 64; ++j)
		{
			int t = bminus1 & (r[j] ^ s[j]);
			p[j] = s[j] ^ t;
			q[j] = r[j] ^ t;
		}
	}

	static void mainloop(int[] work, byte[] e)
	{
		int[] xzm1 = new int[64];
		int[] xzm = new int[64];
		int[] xzmb = new int[64];
		int[] xzm1b = new int[64];
		int[] xznb = new int[64];
		int[] xzn1b = new int[64];
		int[] a0 = new int[64];
		int[] a1 = new int[64];
		int[] b0 = new int[64];
		int[] b1 = new int[64];
		int[] c1 = new int[64];
		int[] r = new int[32];
		int[] s = new int[32];
		int[] t = new int[32];
		int[] u = new int[32];

		for (int j = 0; j < 32; ++j)
			xzm1[j] = work[j];

		xzm1[32] = 1;

		for (int j = 33; j < 64; ++j)
			xzm1[j] = 0;

		xzm[0] = 1;

		for (int j = 1; j < 64; ++j)
			xzm[j] = 0;

		int[] xzmbp = xzmb, a0p = a0, xzm1bp = xzm1b;
		int[] a1p = a1, b0p = b0, b1p = b1, c1p = c1;
		int[] xznbp = xznb, up = u, xzn1bp = xzn1b;
		int[] workp = work, sp = s, rp = r;

		for (int pos = 254; pos >= 0; --pos)
		{
			int b = (e[pos / 8] & 0xFF) >>> (pos & 7);
			b &= 1;
			select(xzmb, xzm1b, xzm, xzm1, b);
			add(a0, 	0,	xzmb, 	0,	xzmbp,	32);
			sub(a0p,	32,	xzmb, 	0,	xzmbp, 	32);
			add(a1, 	0,	xzm1b, 	0,	xzm1bp,	32);
			sub(a1p,	32,	xzm1b, 	0,	xzm1bp, 32);
			square(b0p,	0,	a0p,	0);
			square(b0p, 32,	a0p,	32);
			mult(b1p,	0,	a1p,	0, 	a0p,	32);
			mult(b1p,	32,	a1p,	32,	a0p,	0);
			add(c1, 	0,	b1, 	0,	b1p,	32);
			sub(c1p,	32,	b1,		0,	b1p,	32);
			square(rp,	0,	c1p,	32);
			sub(sp,		0,	b0,		0,	b0p,	32);
			mult121665(t, s);
			add(u, 		0,	t, 		0,	b0p,	0);
			mult(xznbp,	0,	b0p,	0,	b0p,	32);
			mult(xznbp,	32, sp,		0,	up,		0);
			square(xzn1bp, 0, c1p,	0);
			mult(xzn1bp, 32, rp, 	0, 	workp, 	0);
			select(xzm, xzm1, xznb, xzn1b, b);
		}

		for (int j = 0; j < 64; ++j)
			work[j] = xzm[j];
	}

	static void recip(int[] outv, int outvoffset, int[] z, int zoffset)
	{
		int[] z2 = new int[32];
		int[] z9 = new int[32];
		int[] z11 = new int[32];
		int[] z2_5_0 = new int[32];
		int[] z2_10_0 = new int[32];
		int[] z2_20_0 = new int[32];
		int[] z2_50_0 = new int[32];
		int[] z2_100_0 = new int[32];
		int[] t0 = new int[32];
		int[] t1 = new int[32];

		/* 2 */
		int[] z2p = z2;
		square(z2p, 0, z, zoffset);

		/* 4 */
		square(t1, 0, z2, 0);

		/* 8 */
		square(t0, 0, t1, 0);

		/* 9 */
		int[] z9p = z9, t0p = t0;
		mult(z9p, 0, t0p, 0, z, zoffset);

		/* 11 */
		mult(z11, 0, z9, 0, z2, 0);

		/* 22 */
		square(t0, 0, z11, 0);

		/* 2^5 - 2^0 = 31 */
		mult(z2_5_0, 0, t0, 0, z9, 0);

		/* 2^6 - 2^1 */
		square(t0, 0, z2_5_0, 0);

		/* 2^7 - 2^2 */
		square(t1, 0, t0, 0);

		/* 2^8 - 2^3 */
		square(t0, 0, t1, 0);

		/* 2^9 - 2^4 */
		square(t1, 0, t0, 0);

		/* 2^10 - 2^5 */
		square(t0, 0, t1, 0);

		/* 2^10 - 2^0 */
		mult(z2_10_0, 0, t0, 0, z2_5_0, 0);

		/* 2^11 - 2^1 */
		square(t0, 0, z2_10_0, 0);

		/* 2^12 - 2^2 */
		square(t1, 0, t0, 0);

		/* 2^20 - 2^10 */
		for (int i = 2; i < 10; i += 2)
		{
			square(t0, 0, t1, 0);
			square(t1, 0, t0, 0);
		}

		/* 2^20 - 2^0 */
		mult(z2_20_0, 0, t1, 0, z2_10_0, 0);

		/* 2^21 - 2^1 */
		square(t0, 0, z2_20_0, 0);

		/* 2^22 - 2^2 */
		square(t1, 0, t0, 0);

		/* 2^40 - 2^20 */
		for (int i = 2; i < 20; i += 2)
		{
			square(t0, 0, t1, 0);
			square(t1, 0, t0, 0);
		}

		/* 2^40 - 2^0 */
		mult(t0, 0, t1, 0, z2_20_0, 0);

		/* 2^41 - 2^1 */
		square(t1, 0, t0, 0);

		/* 2^42 - 2^2 */
		square(t0, 0, t1, 0);

		/* 2^50 - 2^10 */
		for (int i = 2; i < 10; i += 2)
		{
			square(t1, 0, t0, 0);
			square(t0, 0, t1, 0);
		}

		/* 2^50 - 2^0 */
		mult(z2_50_0, 0, t0, 0, z2_10_0, 0);

		/* 2^51 - 2^1 */
		square(t0, 0, z2_50_0, 0);

		/* 2^52 - 2^2 */
		square(t1, 0, t0, 0);

		/* 2^100 - 2^50 */
		for (int i = 2; i < 50; i += 2)
		{
			square(t0, 0, t1, 0);
			square(t1, 0, t0, 0);
		}

		/* 2^100 - 2^0 */
		mult(z2_100_0, 0, t1, 0, z2_50_0, 0);

		/* 2^101 - 2^1 */
		square(t1, 0, z2_100_0, 0);

		/* 2^102 - 2^2 */
		square(t0, 0, t1, 0);

		/* 2^200 - 2^100 */
		for (int i = 2; i < 100; i += 2)
		{
			square(t1, 0, t0, 0);
			square(t0, 0, t1, 0);
		}

		/* 2^200 - 2^0 */
		mult(t1, 0, t0, 0, z2_100_0, 0);

		/* 2^201 - 2^1 */
		square(t0, 0, t1, 0);

		/* 2^202 - 2^2 */
		square(t1, 0, t0, 0);

		/* 2^250 - 2^50 */
		for (int i = 2; i < 50; i += 2)
		{
			square(t0, 0, t1, 0);
			square(t1, 0, t0, 0);
		}

		/* 2^250 - 2^0 */
		mult(t0, 0, t1, 0, z2_50_0, 0);

		/* 2^251 - 2^1 */
		square(t1, 0, t0, 0);

		/* 2^252 - 2^2 */
		square(t0, 0, t1, 0);

		/* 2^253 - 2^3 */
		square(t1, 0, t0, 0);

		/* 2^254 - 2^4 */
		square(t0, 0, t1, 0);

		/* 2^255 - 2^5 */
		square(t1, 0, t0, 0);

		/* 2^255 - 21 */
		int[] t1p = t1, z11p = z11;
		mult(outv, outvoffset, t1p, 0, z11p, 0);
	}

	public static int crypto_scalarmult(byte[] q, byte[] n, byte[] p)
	{
        if (haveNative) {
            try {
                return crypto_scalarmult_native(q, n, p);
            } catch (UnsatisfiedLinkError e) {
                /* fall through to Java implementation */
            }
        }

		int[] work = new int[96];
		byte[] e = new byte[32];

		for (int i = 0; i < 32; ++i)
			e[i] = n[i];

		e[0] &= 248;
		e[31] &= 127;
		e[31] |= 64;

		for (int i = 0; i < 32; ++i)
			work[i] = p[i] & 0xFF;

		mainloop(work, e);

		recip(work, 32, work, 32);
		mult(work, 64, work, 0, work, 32);
		freeze(work, 64);

		for (int i = 0; i < 32; ++i)
			q[i] = (byte) work[64 + i];

		return 0;
	}

    public native static int crypto_scalarmult_native(byte[] q, byte[] n, byte[] p);
}
