/*
 *                    BioJava development code
 *
 * This code may be freely distributed and modified under the
 * terms of the GNU Lesser General Public Licence.  This should
 * be distributed with the code.  If you do not have a copy,
 * see:
 *
 *      http://www.gnu.org/copyleft/lesser.html
 *
 * Copyright for this code is held jointly by the individual
 * authors.  These should be listed in @author doc comments.
 *
 * For more information on the BioJava project and its aims,
 * or to join the biojava-l mailing list, visit the home page
 * at:
 *
 *      http://www.biojava.org/
 *
 */
package org.biojava.nbio.structure.symmetry.geometry;

import javax.vecmath.*;
import java.util.ArrayList;
import java.util.List;


/**
 * Generate the permutations and sign changes for a Triple.
 * For instance, (a,0,0) becomes (±a,0,0),(0,±a,0),(0,0,±a). In the general
 * case 48 tuples are returned.
 */
class Permute {

	private List<Point3i> triples = new ArrayList<Point3i>();

	Permute(Point3i t) {
		Point3i tmp = new Point3i();
		tmp.x = t.x;
		tmp.y = t.y;
		tmp.z = t.z;
		triples.add(tmp);
		int n = 1;

		// Unpermuted ±x,±y,±z
		if (t.x != 0) {
			for (int i = 0; i < n; ++i) {
				Tuple3i m = triples.get(i);

				triples.add(new Point3i(-m.x, m.y, m.z));
			}
			n *= 2;
		}

		if (t.y != 0) {
			for (int i = 0; i < n; ++i) {
				Point3i m = triples.get(i);
				triples.add(new Point3i(m.x, -m.y, m.z));
			}
			n *= 2;
		}

		if (t.z != 0) {
			for (int i = 0; i < n; ++i) {
				Point3i m = triples.get(i);
				triples.add(new Point3i(m.x, m.y, -m.z));
			}
			n *= 2;
		}
		if (t.x == t.y && t.y == t.z) {
			return;
		}

		// At least one differing value
		for (int i = 0; i < n; ++i) {
			Point3i m = triples.get(i);
			triples.add(new Point3i(m.y, m.z, m.x));
			triples.add(new Point3i(m.z, m.x, m.y));
		}
		n *= 3;

		if (t.x == t.y || t.y == t.z) {
			return;
		}

		// At least two differing values
		for (int i = 0; i < n; ++i) {
			Point3i m = triples.get(i);
			triples.add(new Point3i(m.y, m.x, m.z));
		}
		n *= 2;
	}

	public int size() {
		return triples.size();
	}

	public Point3i get(int i) {
		return triples.get(i);
	}
};

/**
 * Sample possible orientations. An orientation can be represented as a
 * quaternion or as a rotation axis and angle. The orientations are somewhat
 * evenly distributed over orientation space, but the current implementation
 * is not suited for statistically uniform sampling.
 * @author Peter
 */
public final class SphereSampler {

	private static final List<Quat4d> orientations ;



	// The rotational symmetries of the cube. (Not normalized, since
	// PackSet.Add does this.)
	private static final double[][] cubeSyms = {
		{ 1, 0, 0, 0 },
		// 180 deg rotations about 3 axes
		{ 0, 1, 0, 0 },
		{ 0, 0, 1, 0 },
		{ 0, 0, 0, 1 },
		// +/- 120 degree rotations about 4 leading diagonals
		{ 1, 1, 1, 1 }, { 1, 1, 1, -1 }, { 1, 1, -1, 1 }, { 1, 1, -1, -1 },
		{ 1, -1, 1, 1 }, { 1, -1, 1, -1 },
		{ 1, -1, -1, 1 },
		{ 1, -1, -1, -1 },
		// +/- 90 degree rotations about 3 axes
		{ 1, 1, 0, 0 }, { 1, -1, 0, 0 }, { 1, 0, 1, 0 }, { 1, 0, -1, 0 },
		{ 1, 0, 0, 1 }, { 1, 0, 0, -1 },
		// 180 degree rotations about 6 face diagonals
		{ 0, 1, 1, 0 }, { 0, 1, -1, 0 }, { 0, 1, 0, 1 }, { 0, 1, 0, -1 },
		{ 0, 0, 1, 1 }, { 0, 0, 1, -1 }, };



	// Approximate spacing between grid points
	private static final double delta = 0.15846;
	// Amount of distortion towards grid corners to account for the projection back to a 4-sphere
	private static final double sigma = 0.00;
	private static final int ntot = 7416; // total number of grid points over the 24 cells
	private static final int ncell = 309; // number of grid points per cell
	private static final int nent = 18; // number of distinct grid points (with k>=l>=m)

	// Why not (5,5,3) and (5,5,5)? Would they overlap with other cells? -SB
	private static final int[] k = { 0, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5};
	private static final int[] l = { 0, 1, 0, 2, 2, 1, 3, 3, 0, 2, 2, 4, 4, 4, 1, 3, 3, 5};
	private static final int[] m = { 0, 1, 0, 0, 2, 1, 1, 3, 0, 0, 2, 0, 2, 4, 1, 1, 3, 1};

	// number of permutations to expect for each (k,l,m) tuple
	private static final int[] mult = { 1, 8, 6, 12, 8, 24, 24, 8, 6, 24, 24, 12, 24, 8, 24, 48, 24, 24};

	static
	{

		List<Quat4d> myorientations = new ArrayList<Quat4d>();

		// 60 equally spaced grid points covering all quaternions
		// This could be removed, since the 48-cell bcc lattice has lower angle error -SB
		for (int i = 0; i < IcosahedralSampler.getSphereCount(); i++) {
			myorientations.add(IcosahedralSampler.getQuat4d(i));
		}

		// SB's interpretation of this code:
		// Directly form a 4D grid over the 4-sphere of orientations. Start by
		// making a 3D body-centered lattice with w=1. Then use the hypercube symmetries
		// to extend this grid over the whole surface.
		// The permuted (k,l,m) values together make a diagonal grid out to ±(5,5,5).
		// The point spacing is distorted by the pind() function so that the
		// projection of the points back to the 4-sphere will be more even.

		// This is the c48u309 lattice from Karney 2006, with a max error of 10.07
		// degrees.

		List<Quat4d> grid = new ArrayList<Quat4d>();
		int ncell1 = 0;
		for (int n = 0; n < nent; ++n) { // for each tuple (k,l,m) above
			Permute p = new Permute(new Point3i(k[n], l[n], m[n]));
			assert (mult[n] == p.size());
			for (int i = 0; i < mult[n]; ++i) { // for each permutation
				Point3i t = p.get(i);
				grid.add(new Quat4d(1.0, pind(0.5 * t.x, delta, sigma), pind(
						0.5 * t.y, delta, sigma), pind(0.5 * t.z, delta, sigma)));
			}
			ncell1 += mult[n];
		}
		assert (ncell1 == ncell);
		// Apply symmetry operations to distribute grid over all values of w
		int nc = grid.size();
		assert (nc == ncell);
		for (int n = 1; n < 24; ++n) {
			Quat4d q = new Quat4d(cubeSyms[n][0], cubeSyms[n][1],
					cubeSyms[n][2], cubeSyms[n][3]);
			for (int i = 0; i < nc; ++i) {
				Quat4d qs = new Quat4d();
				qs.mul(q, grid.get(i));
				grid.add(qs);
				//	s.add(times(q, s.getOrientation(i)), s.getWeight(i)); // this data set has no weights
				// Actually, it does have weights but we don't care since we don't need uniform sampling. -SB
			}
		}
		assert (grid.size() == ntot);
		myorientations.addAll(grid);

		orientations = myorientations;

	}

	// this class cannot be instantiated
	private SphereSampler() {
	};

	public static int getSphereCount() {

		return orientations.size();
	}

	public static Quat4d getQuat4d(int index) {

		return orientations.get(index);
	}

	public static void getAxisAngle(int index, AxisAngle4f axisAngle) {

		axisAngle.set(orientations.get(index));
	}

	/**
	 * @param index Index between 0 and {@link #getSphereCount()}-1
	 * @param axisAngle (Output) modified to contain the index-th sampled orientation
	 */
	public static void getAxisAngle(int index, AxisAngle4d axisAngle) {

		axisAngle.set(orientations.get(index));
	}

	// Convert from index to position. The sinh scaling tries to compensate
	// for the bunching up that occurs when [1 x y z] is projected onto the
	// unit sphere.
	private static double pind(double ind, double delta, double sigma) {
		return (sigma == 0) ? ind * delta : Math.sinh(sigma * ind * delta)
				/ sigma;
	}

}
