//
//  Copyright (c) 2011, Neil Alexander T.
//  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 };

	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 = (int) (-((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;

		for (int pos = 254; pos >= 0; --pos)
		{
			int b = ((int) ((e[pos / 8] & 0xFF) >>> (pos & 7)));
			b &= 1;
			select(xzmb, xzm1b, xzm, xzm1, b);
			add(a0, 	0,	xzmb, 	0, xzmb,	32);
			sub(a0,	32,	xzmb, 	0, xzmb, 	32);
			add(a1, 	0,	xzm1b, 	0, xzm1b,	32);
			sub(a1,	32,	xzm1b, 	0, xzm1b, 32);
			square(b0,	0, a0,	0);
			square(b0, 32, a0,	32);
			mult(b1,	0, a1,	0, a0,	32);
			mult(b1,	32, a1,	32, a0,	0);
			add(c1, 	0,	b1, 	0, b1,	32);
			sub(c1,	32,	b1,		0, b1,	32);
			square(r,	0, c1,	32);
			sub(s,		0,	b0,		0, b0,	32);
			mult121665(t, s);
			add(u, 		0,	t, 		0, b0,	0);
			mult(xznb,	0, b0,	0, b0,	32);
			mult(xznb,	32, s,		0, u,		0);
			square(xzn1b, 0, c1,	0);
			mult(xzn1b, 32, r, 	0, work, 	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 */
		square(z2, 0, z, zoffset);
		
		/* 4 */
		square(t1, 0, z2, 0);
		
		/* 8 */
		square(t0, 0, t1, 0);
		
		/* 9 */
		mult(z9, 0, t0, 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 */
		mult(outv, outvoffset, t1, 0, z11, 0);
	}

	public static int crypto_scalarmult(byte[] q, byte[] n, byte[] p)
	{
		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;
	}
}
