/*
 *                    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.xtal;

import javax.vecmath.Matrix3d;
import javax.vecmath.Matrix4d;
import javax.vecmath.Point3i;
import javax.vecmath.Vector3d;
import java.io.Serializable;
import java.util.Locale;

import static java.lang.Math.abs;


/**
 * Representation of a transformation in a crystal:
 * - a transformation id (each of the transformations in a space group, 0 to m)
 * - a crystal translation
 * The transformation matrix in crystal basis is stored, representing the basic
 * transformation together with the crystal translation.
 * Contains methods to check for equivalent transformations.
 *
 *
 * @author duarte_j
 *
 */
public class CrystalTransform implements Serializable {

	private static final long serialVersionUID = 1L;


	public static final Matrix4d IDENTITY = new Matrix4d(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1);

	/**
	 * The space group to which this transform belongs
	 */
	private final SpaceGroup sg;

	/**
	 * The transform id corresponding to the SpaceGroup's transform indices.
	 * From 0 (identity) to m (m=number of symmetry operations of the space group)
	 * It is unique within the unit cell but equivalent units of different crystal unit cells
	 * will have same id
	 */
	private int transformId;

	/**
	 * The 4-dimensional matrix transformation in crystal basis.
	 * Note that the translational component of this matrix is not necessarily
	 * identical to crystalTranslation since some operators have fractional
	 * translations within the cell
	 */
	private Matrix4d matTransform;

	/**
	 * The crystal translation (always integer)
	 */
	private Point3i crystalTranslation;


	/**
	 * Creates a new CrystalTransform representing the identity transform
	 * in cell (0,0,0)
	 */
	public CrystalTransform(SpaceGroup sg) {
		this.sg = sg;
		this.transformId = 0;
		this.matTransform = (Matrix4d)IDENTITY.clone();
		this.crystalTranslation = new Point3i(0,0,0);
	}

	/**
	 * Represents the n-th transform
	 * @param sg
	 * @param transformId
	 */
	public CrystalTransform(SpaceGroup sg, int transformId) {
		this.sg = sg;
		this.transformId = transformId;
		if (sg==null && transformId==0) {
			this.matTransform = (Matrix4d)IDENTITY.clone();
		} else if (sg==null) {
			throw new IllegalArgumentException("Space Group cannot be null if transformId!=0");
		} else {
			this.matTransform = (Matrix4d)sg.getTransformation(transformId).clone();
		}
		this.crystalTranslation = new Point3i(0,0,0);
	}

	/**
	 * Copy constructor
	 * @param transform
	 */
	public CrystalTransform(CrystalTransform transform) {
		this.sg = transform.sg;
		this.transformId = transform.transformId;
		this.matTransform = new Matrix4d(transform.matTransform);
		this.crystalTranslation = new Point3i(transform.crystalTranslation);
	}

	public Matrix4d getMatTransform() {
		return matTransform;
	}

	public void setMatTransform(Matrix4d matTransform) {
		this.matTransform = matTransform;
	}

	public Point3i getCrystalTranslation() {
		return crystalTranslation;
	}

	public void translate(Point3i translation) {
		matTransform.m03 = matTransform.m03+translation.x;
		matTransform.m13 = matTransform.m13+translation.y;
		matTransform.m23 = matTransform.m23+translation.z;

		crystalTranslation.add(translation);

	}

	/**
	 * Returns true if the given CrystalTransform is equivalent to this one.
	 * Two crystal transforms are equivalent if one is the inverse of the other, i.e.
	 * their transformation matrices multiplication is equal to the identity.
	 * @param other
	 * @return
	 */
	public boolean isEquivalent(CrystalTransform other) {
		Matrix4d mul = new Matrix4d();
		mul.mul(this.matTransform,other.matTransform);

		if (mul.epsilonEquals(IDENTITY, 0.0001)) {
			return true;
		}
		return false;
	}

	/**
	 * Tells whether this transformation is a pure crystal lattice translation,
	 * i.e. no rotational component and an integer translation vector.
	 * @return
	 */
	public boolean isPureCrystalTranslation() {
		return (transformId==0 && (crystalTranslation.x!=0 || crystalTranslation.y!=0 || crystalTranslation.z!=0));
	}

	/**
	 * Tells whether this transformation is the identity: no rotation and no translation
	 * @return
	 */
	public boolean isIdentity() {
		return (transformId==0 && crystalTranslation.x==0 && crystalTranslation.y==0 && crystalTranslation.z==0);
	}

	/**
	 * Tells whether this transformation is a pure translation:
	 * either a pure crystal (lattice) translation or a fractional (within
	 * unit cell) translation: space groups Ixxx, Cxxx, Fxxx have operators
	 * with fractional translations within the unit cell.
	 * @return
	 */
	public boolean isPureTranslation() {
		if (isPureCrystalTranslation()) return true;
		if (isPureMatrixTranslation()) return true;
		return false;
	}

	/**
	 * This method will help check if the matrix translation is pure or not.
	 * @return boolean
	 */
	private boolean isPureMatrixTranslation(){
		return 	SpaceGroup.deltaComp(matTransform.m00,1,SpaceGroup.DELTA) &&
				SpaceGroup.deltaComp(matTransform.m01,0,SpaceGroup.DELTA) &&
				SpaceGroup.deltaComp(matTransform.m02,0,SpaceGroup.DELTA) &&

				SpaceGroup.deltaComp(matTransform.m10,0,SpaceGroup.DELTA) &&
				SpaceGroup.deltaComp(matTransform.m11,1,SpaceGroup.DELTA) &&
				SpaceGroup.deltaComp(matTransform.m12,0,SpaceGroup.DELTA) &&

				SpaceGroup.deltaComp(matTransform.m20,0,SpaceGroup.DELTA) &&
				SpaceGroup.deltaComp(matTransform.m21,0,SpaceGroup.DELTA) &&
				SpaceGroup.deltaComp(matTransform.m22,1,SpaceGroup.DELTA) &&
				(Math.abs(matTransform.m03-0.0)>SpaceGroup.DELTA || Math.abs(matTransform.m13-0.0)>SpaceGroup.DELTA || Math.abs(matTransform.m23-0.0)>SpaceGroup.DELTA);
	}

	/**
	 * Tells whether this transformation contains a fractional translational
	 * component (whatever its rotational component). A fractional translation
	 * together with a rotation means a screw axis.
	 * @return
	 */
	public boolean isFractionalTranslation() {
		if ((Math.abs(matTransform.m03-crystalTranslation.x)>SpaceGroup.DELTA) ||
			(Math.abs(matTransform.m13-crystalTranslation.y)>SpaceGroup.DELTA) ||
			(Math.abs(matTransform.m23-crystalTranslation.z)>SpaceGroup.DELTA)) {
			return true;
		}
		return false;
	}

	/**
	 * Tells whether this transformation is a rotation disregarding the translational component,
	 * i.e. either pure rotation or screw rotation, but not improper rotation.
	 * @return
	 */
	public boolean isRotation() {
		// if no SG, that means a non-crystallographic entry (e.g. NMR). We return false
		if (sg==null) return false;

		// this also takes care of case <0 (improper rotations): won't be considered as rotations
		if (sg.getAxisFoldType(this.transformId)>1) return true;

		return false;
	}

	/**
	 * Returns the TransformType of this transformation: AU, crystal translation, fractional translation
	 * , 2 3 4 6-fold rotations, 2 3 4 6-fold screw rotations, -1 -3 -2 -4 -6 inversions/rotoinversions.
	 * @return
	 */
	public TransformType getTransformType() {

		// if no SG, that means a non-crystallographic entry (e.g. NMR). We return AU as type
		if (sg==null) return TransformType.AU;

		int foldType = sg.getAxisFoldType(this.transformId);
		boolean isScrewOrGlide = false;
		Vector3d translScrewComponent = getTranslScrewComponent();
		if (Math.abs(translScrewComponent.x-0.0)>SpaceGroup.DELTA ||
			Math.abs(translScrewComponent.y-0.0)>SpaceGroup.DELTA ||
			Math.abs(translScrewComponent.z-0.0)>SpaceGroup.DELTA) {

			isScrewOrGlide = true;
		}

		if (foldType>1) {

			if (isScrewOrGlide) {
				switch (foldType) {
				case 2:
					return TransformType.TWOFOLDSCREW;
				case 3:
					return TransformType.THREEFOLDSCREW;
				case 4:
					return TransformType.FOURFOLDSCREW;
				case 6:
					return TransformType.SIXFOLDSCREW;
				default:
					throw new NullPointerException("This transformation did not fall into any of the known types! This is most likely a bug.");
				}
			} else {
				switch (foldType) {
				case 2:
					return TransformType.TWOFOLD;
				case 3:
					return TransformType.THREEFOLD;
				case 4:
					return TransformType.FOURFOLD;
				case 6:
					return TransformType.SIXFOLD;
				default:
					throw new NullPointerException("This transformation did not fall into any of the known types! This is most likely a bug.");
				}
			}

		} else if (foldType<0) {
			switch (foldType) {
			case -1:
				return TransformType.ONEBAR;
			case -2:
				if (isScrewOrGlide) {
					return TransformType.GLIDE;
				}
				return TransformType.TWOBAR;
			case -3:
				return TransformType.THREEBAR;
			case -4:
				return TransformType.FOURBAR;
			case -6:
				return TransformType.SIXBAR;
			default:
				throw new NullPointerException("This transformation did not fall into any of the known types! This is most likely a bug.");
			}
		} else {
			if (isIdentity()) {
				return TransformType.AU;
			}
			if (isPureCrystalTranslation()) {
				return TransformType.XTALTRANSL;
			}
			if (isFractionalTranslation()) {
				return TransformType.CELLTRANSL;
			}
			throw new NullPointerException("This transformation did not fall into any of the known types! This is most likely a bug.");
		}

	}

	public Vector3d getTranslScrewComponent() {

		return getTranslScrewComponent(matTransform);

	}

	public int getTransformId() {
		return transformId;
	}

	public void setTransformId(int transformId) {
		this.transformId = transformId;
	}

	@Override
	public String toString() {
		return String.format("[%2d-(%s)]",transformId,toXYZString());
	}

	/**
	 * Expresses this transformation in terms of x,y,z fractional coordinates.
	 *
	 * Examples:
	 * @return
	 */
	public String toXYZString() {
		StringBuilder str = new StringBuilder();

		for(int i=0;i<3;i++) { //for each row
			boolean emptyRow = true;

			double coef; // TODO work with rational numbers


			// X
			coef = matTransform.getElement(i, 0);

			// Three cases for coef: zero, one, non-one
			if(abs(coef) > 1e-6 ) { // Non-zero
				if( abs( abs(coef)-1 ) < 1e-6 ) { // +/- 1
					if( coef < 0 ) {
						str.append("-");
					}
				} else {
					str.append(formatCoef(coef));
					str.append("*");
				}
				str.append("x");
				emptyRow = false;
			}

			// Y
			coef = matTransform.getElement(i, 1);

			if(abs(coef) > 1e-6 ) { // Non-zero
				if( abs( abs(coef)-1 ) < 1e-6 ) { // +/- 1
					if( coef < 0 ) {
						str.append("-");
					} else if( !emptyRow ) {
						str.append("+");
					}
				} else {
					if( !emptyRow && coef > 0) {
						str.append("+");
					}
					str.append(formatCoef(coef));
					str.append("*");
				}
				str.append("y");
				emptyRow = false;
			}

			// Z
			coef = matTransform.getElement(i, 2);

			if(abs(coef) > 1e-6 ) { // Non-zero
				if( abs( abs(coef)-1 ) < 1e-6 ) { // +/- 1
					if( coef < 0 ) {
						str.append("-");
					} else if( !emptyRow ) {
						str.append("+");
					}
				} else {
					if( !emptyRow && coef > 0) {
						str.append("+");
					}
					str.append(formatCoef(coef));
					str.append("*");
				}
				str.append("z");
				emptyRow = false;
			}

			// Intercept
			coef = matTransform.getElement(i, 3);

			if(abs(coef) > 1e-6 ) { // Non-zero
				if( !emptyRow && coef > 0) {
					str.append("+");
				}
				str.append(formatCoef(coef));
			}

			if(i<2) {
				str.append(",");
			}
		}


		return str.toString();
	}
	/**
	 * helper function to format simple fractions into rationals
	 * @param coef
	 * @return
	 */
	private String formatCoef(double coef) {
		double tol = 1e-6; // rounding tolerance

		// zero case
		if( Math.abs(coef) < tol) {
			return "0";
		}

		// integer case
		long num = Math.round(coef);
		if( Math.abs(num - coef) < tol) {
			return Long.toString(num);
		}

		// Other small cases
		for(int denom = 2; denom < 12; denom++ ) {
			num = Math.round(coef*denom);
			if( num - coef*denom < tol ) {
				return String.format("%d/%d",num, denom);
			}
		}

		// Give up and use floating point;
		return String.format(Locale.US, "%.3f", coef);
	}

	/**
	 * Given a transformation matrix containing a rotation and translation returns the
	 * screw component of the rotation.
	 * See http://www.crystallography.fr/mathcryst/pdf/Gargnano/Aroyo_Gargnano_1.pdf
	 * @param m
	 * @return
	 */
	public static Vector3d getTranslScrewComponent(Matrix4d m) {

		int foldType = SpaceGroup.getRotAxisType(m);
		// For reference see:
		// http://www.crystallography.fr/mathcryst/pdf/Gargnano/Aroyo_Gargnano_1.pdf

		Vector3d transl = null;

		Matrix3d W =
				new Matrix3d(m.m00,m.m01,m.m02,
						m.m10,m.m11,m.m12,
						m.m20,m.m21,m.m22);

		if (foldType>=0) {

			// the Y matrix: Y = W^k-1 + W^k-2 ... + W + I  ; with k the fold type
			Matrix3d Y = new Matrix3d(1,0,0, 0,1,0, 0,0,1);
			Matrix3d Wk = new Matrix3d(1,0,0, 0,1,0, 0,0,1);

			for (int k=0;k<foldType;k++) {
				Wk.mul(W); // k=0 Wk=W, k=1 Wk=W^2, k=2 Wk=W^3, ... k=foldType-1, Wk=W^foldType
				if (k!=foldType-1) Y.add(Wk);
			}

			transl = new Vector3d(m.m03, m.m13, m.m23);
			Y.transform(transl);

			transl.scale(1.0/foldType);

		} else {

			if (foldType==-2) { // there are glide planes only in -2
				Matrix3d Y = new Matrix3d(1,0,0, 0,1,0, 0,0,1);
				Y.add(W);

				transl = new Vector3d(m.m03, m.m13, m.m23);
				Y.transform(transl);

				transl.scale(1.0/2.0);
			} else { // for -1, -3, -4 and -6 there's nothing to do: fill with 0s
				transl = new Vector3d(0,0,0);
			}
		}

		return transl;
	}
}
