using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using VisiMath;
using VisiMath.Geometry;

namespace VisimBox.Renderer
{
    public class StandardCamera : QTransformation, ICamera
    {
        /// <summary>
        /// Last used width of the viewport
        /// </summary>
        protected int Width;
        /// <summary>
        /// Last used height of the viewport
        /// </summary>
        protected int Height;
        /// <summary>
        /// Field of vision of the camera
        /// </summary>
        protected float FoV;

        protected float zNear;
        protected float zFar;

        protected Matrix4 ProjectionMatrix;
        protected Matrix4 _InvertedProjection;
        protected bool InvProjValid = false;
        protected Matrix4 ViewMatrix;

        #region constructors

        /// <summary>
        /// Constructs camera with specified 45 degrees of field of vision and view distance of 0.f - 300, for specified width and height of the rendering window
        /// </summary>
        public StandardCamera()
            : this(MathTools.QuarterPi)
        {
        }

        /// <summary>
        /// Constructs camera with specified field of vision and view distance of 0.3f - 300, for specified width and height of the rendering window
        /// </summary>
        /// <param name="fov">Field of vision (45 degrees is typical) in radians</param>
        public StandardCamera(float fov) : this(fov, 0.3f, 300f)
        {
        }

        /// <summary>
        /// Constructs camera with specified field of vision and zNear and zFar cliping parameters
        /// </summary>
        /// <param name="fov">Field of vision (45 degrees is typical) in radians</param>
        /// <param name="znear">zNear clipping (minimal view distance)</param>
        /// <param name="zfar">zFar clipping (max visible distance)</param>
        public StandardCamera(float fov, float znear, float zfar)
        {
            Width = -100;
            Height = -100;
            FoV = fov;
            zNear = znear;
            zFar = zfar;
            ViewMatrix = Matrix4.Identitity;
            ProjectionMatrix = Matrix4.Identitity;
            changed = true;
            //CalcProjection();
            trans = Matrix4.Identitity;
        }


        #endregion

        /// <summary>
        /// recalulculated projection matrix
        /// </summary>
        protected void CalcProjection()
        {
            Matrix4.CreatePerspective(FoV, (float)Width / (float)Height, zNear, zFar, ref ProjectionMatrix);
            InvProjValid = false;
        }


        public override void UpdateMatrix()
        {
            //note, more optimization...., also should store inverted view matrix...

            //Vector3 forward = new Vector3(2 * _q.X * _q.Y + 2 * _q.W * _q.Z, 1-2*_q.X*_q.X - 2*_q.Z*_q.Z, 2*_q.Y*_q.Z-2*_q.W*_q.X); //Vector3 forward = _q * Vector3.UnitY;
            //Vector3 up = new Vector3(2*_q.X*_q.Z -2*_q.W*_q.Y, 2*_q.Y*_q.Z + 2*_q.W*_q.X, 1-2*_q.X*_q.X-2*_q.Y*_q.Y); //_q * Vector3.UnitZ;
            //Vector3 right = new Vector3(1-2*_q.Y*_q.Y - 2*_q.Z*_q.Z, 2*_q.X*_q.Y - 2*_q.W*_q.Z, 2*_q.X*_q.Z+2*_q.W*_q.Y); //_q * Vector3.UnitX;

            float xy = 2 * _q.X * _q.Y;
            float xz = 2 * _q.X * _q.Z;
            float yz = 2 * _q.Y * _q.Z;
            float wx = 2 * _q.X * _q.W;
            float wz = 2 * _q.Z * _q.W;
            float wy = 2 * _q.Y * _q.W;
            float xx = 2 * _q.X * _q.X;
            float yy = 2 * _q.Y * _q.Y;
            float zz = 2 * _q.Z * _q.Z;

            ViewMatrix.m00=xy+wz;
            ViewMatrix.m10=-xz+wy;
            ViewMatrix.m20=1-yy-zz;
            ViewMatrix.m30=0;

            ViewMatrix.m01=1-xx-zz;
            ViewMatrix.m11=-yz-wx;
            ViewMatrix.m21=xy-wz;
            ViewMatrix.m31=0;

            ViewMatrix.m02=yz-wx;
            ViewMatrix.m12=-1+xx+yy;
            ViewMatrix.m22=xz+wy;
            ViewMatrix.m32=0;


            ViewMatrix.m03=-(xy+wz)*_position.X-(1-xx-zz)*_position.Y-(yz-wx)*_position.Z;
            ViewMatrix.m13=-(-xz+wy)*_position.X-(-yz-wx)*_position.Y-(-1+xx+yy)*_position.Z;
            ViewMatrix.m23=-(1-yy-zz)*_position.X-(xy-wz)*_position.Y-(xz+wy)*_position.Z;
            ViewMatrix.m33=1;
           
            /*Vector3 forward = _q * Vector3.UnitY;
            Vector3 up = _q * Vector3.UnitZ;
            Vector3 right = _q * Vector3.UnitX;
            */

            //Matrix3 rotmat = new Matrix3(forward, -up, right);

            
            /*
            Vector3 forward = _q * Vector3.UnitX;
            Vector3 side = Vector3.Normalize(Vector3.UnitZ.Cross(ref forward));
            Vector3 up = Vector3.Normalize(forward.Cross(ref side));

            Matrix3 rotmat = new Matrix3(side.X, side.Y, side.Z,
                -up.X, -up.Y, -up.Z,
                forward.X, forward.Y, forward.Z);


            Matrix3 rotmatinv = new Matrix3(forward, side, up);

            _q = Quaternion.CreateFromMatrix(ref rotmatinv);*/

            
            //Vector3 invpos = new Vector3(ref _position, -1);
            //Matrix3 rotmat = new Matrix3(forward, -up, right);
            //ViewMatrix = Matrix4.CreateTranslation(ref invpos);
            //ViewMatrix = new Matrix4(rotmat) * ViewMatrix;
            
            base.UpdateMatrix();

        }

        /// <summary>
        /// Deserializes object from XML
        /// </summary>
        /// <param name="reader">XML reader</param>
        public override void ReadXml(System.Xml.XmlReader reader)
        {
            if (reader["type"] != "QCamera")
                return;
            changed = true;
            while (reader.Read())
            {
                if (reader.IsStartElement())
                {
                    if (reader.Name == "Translation")
                    {
                        _position.ReadXml(reader);
                    }
                    else if (reader.Name == "Rotation")
                    {
                        _q.ReadXml(reader);
                    }
                    else if (reader.Name == "CameraSettings")
                    {
                        zNear = XMLHelper.FloatFromString(reader["ZNear"]);
                        zFar = XMLHelper.FloatFromString(reader["ZFar"]);
                        FoV = XMLHelper.FloatFromString(reader["FOV"]);

                        MathTools.ClampMin(ref zNear, 0.001f);
                        MathTools.ClampMin(ref zFar, zFar + 0.5f);
                        MathTools.Clamp(ref FoV, 0.001f, 3.14f);
                    }
                }
                else return;
            }
        }

        public override void WriteXml(System.Xml.XmlWriter writer)
        {
            writer.WriteAttributeString("type", "QCamera");
            writer.WriteStartElement("Translation");
            _position.WriteXml(writer);
            writer.WriteEndElement();
            writer.WriteStartElement("Rotation");
            _q.WriteXml(writer);
            writer.WriteEndElement();
            writer.WriteStartElement("CameraSettings");
            XMLHelper.WriteFloatAttribute(writer, "ZNear", zNear);
            XMLHelper.WriteFloatAttribute(writer, "ZFar", zFar);
            XMLHelper.WriteFloatAttribute(writer, "FOV", FoV);
            writer.WriteEndElement();
        }

        public override string ToString()
        {
            return "QCamera on " + _position.ToString() + " and rot " + _q.ToString();
        }

        #region ICamera Members

        /// <summary>
        /// Returns projection transformation matrix for specified width/height of the rendering viewport
        /// </summary>
        /// <param name="width">width of the viewport</param>
        /// <param name="height">Height of the viewport</param>
        /// <returns>Perspective or orthonormal projection matrix</returns>
        public Matrix4 GetProjectionTransformation(int width, int height)
        {
            if ((width != Width) || (height != Height))
            {
                Width = width;
                Height = height;
                CalcProjection();
            }
            return ProjectionMatrix;
        }

        public Matrix4 GetViewTransformation()
        {
            if (changed)
                UpdateMatrix();
            return ViewMatrix;
        }

        public VisiMath.Geometry.Ray GetPickingRay(float mousex, float mousey)
        {
            Ray rval = new Ray();
            if (GetPickingRay(mousex, mousey, rval))
                return rval;
            return null;
        }

        public bool GetPickingRay(float mousex, float mousey, VisiMath.Geometry.Ray ray)
        {            
            //from screen to viewport
            mousex = 2 * mousex / Width - 1.0f;
            mousey = 2 * (Height - mousey) / Height - 1.0f;

            if (!InvProjValid)
            {
                _InvertedProjection = Matrix4.Invert(ref ProjectionMatrix);
                InvProjValid = true;
            }

            //from viewport to view coordinates
            Vector3 source = _InvertedProjection.Multiply(new Vector3(mousex, mousey, 0));
            Vector3 target = _InvertedProjection.Multiply(new Vector3(mousex, mousey, 1.0f));

            //todo - store or calculate it together with view matrix
            Matrix4 mat = Matrix4.Invert(ref ViewMatrix);

            //to world coordinates
            mat.TransformFast(ref source);
            mat.TransformFast(ref target);

            ray.FromTwoPoints(source, target);

            //VisimEngine.Instance.Renderer.MainContext.DebugPoint = target;
            //VisimEngine.Instance.Renderer.MainContext.DebugLine1 = source;
            //VisimEngine.Instance.Renderer.MainContext.DebugLine2 = target;

                        
            return true;
        }

        public Vector3 GetViewingNormal()
        {
            //note, could probably just use local X unit vector?
            if (!InvProjValid)
            {
                _InvertedProjection = Matrix4.Invert(ref ProjectionMatrix);
                InvProjValid = true;
            }

            //from viewport to view coordinates
            Vector3 target = _InvertedProjection.Multiply(new Vector3(0, 0, 1));
            trans.TransformFast(ref target);
            target.Normalize();
            return target;
        }

        /// <summary>
        /// Turns camera to look at specified position (X unit vector will be rotated to position)
        /// </summary>
        /// <param name="position">Look position</param>
        public void LookAt(Vector3 position)
        {            
            //todo - direct quaternion computing?

            Vector3 dir = position - _position;

            if (dir.LengthSquared == 0)
                return; //both on the same places
            
            dir.Normalize();
            if (dir.X == 1)
                return; //already looking

            Quaternion newq;


            //Vector3 up = _q * Vector3.UnitZ;
            Vector3 up = Vector3.UnitZ;

            newq = Quaternion.LookAt(dir, up);
            


            /*
            Vector3 rotaxis = Vector3.UnitX.Cross(ref dir);

            float dot = Vector3.UnitX.Dot(ref dir);

            float angle = MathTools.ACos(dot);
            if (dot > 0)
                angle = -angle;


            newq = Quaternion.CreateFromAxisAngle(ref rotaxis, angle);
            */
              
            /*
            dir.X = -dir.X;
            Matrix3 rotmat = Matrix3.CreateRotationNormal(Vector3.UnitX, dir);
            newq = Quaternion.CreateFromMatrix(ref rotmat);
            */
            /*
            if ((dir.X == 0)&&(dir.Z == 0))
            {
                Vector3 right = Rotation*Vector3.UnitY;
                Vector3 up = dir.Cross(ref right);
                right = up.Cross(ref dir);
                newq = new Quaternion(right, up, dir);
            }
            else
            {
                Vector3 right = new Vector3(dir.Z, 0, -dir.Z);
                right.Normalize();
                Vector3 up = dir.Cross(ref right);
                newq = new Quaternion(right, up, dir);
            }
            */

            Rotation = newq;
        }


        #endregion

        /// <summary>
        /// Returns copy of the transformation
        /// </summary>
        /// <returns></returns>
        public override ITransformation GetCopy()
        {
            StandardCamera newtrans = new StandardCamera();
            newtrans._position = this._position;
            newtrans._changed = true;
            newtrans._q = this._q;
            newtrans.FoV = this.FoV;
            return newtrans;
        }

    }

    /// <summary>
    /// Constrained camera with locked X rotation (rolling left to right)
    /// </summary>
    public class FPSCamera : StandardCamera
    {
        /// <summary>
        /// Constructs camera with specified 45 degrees of field of vision and view distance of 0.f - 300, for specified width and height of the rendering window
        /// </summary>
        public FPSCamera() : base()
        {
        }

        /// <summary>
        /// Constructs camera with specified field of vision and view distance of 0.3f - 300, for specified width and height of the rendering window
        /// </summary>
        /// <param name="fov">Field of vision (45 degrees is typical) in radians</param>
        public FPSCamera(float fov) : base(fov, 0.3f, 300f)
        {
        }

        /// <summary>
        /// Constructs camera with specified field of vision and zNear and zFar cliping parameters
        /// </summary>
        /// <param name="fov">Field of vision (45 degrees is typical) in radians</param>
        /// <param name="znear">zNear clipping (minimal view distance)</param>
        /// <param name="zfar">zFar clipping (max visible distance)</param>
        public FPSCamera(float fov, float znear, float zfar) : base(fov, znear, zfar)
        {
        }
        
        public override Quaternion Rotation
        {
            get
            {
                return _q;
            }
            set
            {
                value.Normalize();
                _q = value.ClosestYZ();
                changed = true;
            }
        }

        /// <summary>
        /// Deserializes object from XML
        /// </summary>
        /// <param name="reader">XML reader</param>
        public override void ReadXml(System.Xml.XmlReader reader)
        {
            if (reader["type"] != "FPSCamera")
                return;
            changed = true;
            while (reader.Read())
            {
                if (reader.IsStartElement())
                {
                    if (reader.Name == "Translation")
                    {
                        _position.ReadXml(reader);
                    }
                    else if (reader.Name == "Rotation")
                    {
                        _q.ReadXml(reader);
                    }
                    else if (reader.Name == "CameraSettings")
                    {
                        zNear = XMLHelper.FloatFromString(reader["ZNear"]);
                        zFar = XMLHelper.FloatFromString(reader["ZFar"]);
                        FoV = XMLHelper.FloatFromString(reader["FOV"]);

                        MathTools.ClampMin(ref zNear, 0.001f);
                        MathTools.ClampMin(ref zFar, zFar + 0.5f);
                        MathTools.Clamp(ref FoV, 0.001f, 3.14f);
                    }
                }
                else return;
            }
        }

        /// <summary>
        /// Serializes object to XML
        /// </summary>
        /// <param name="writer">XML writer to save values</param>
        public override void WriteXml(System.Xml.XmlWriter writer)
        {
            writer.WriteAttributeString("type", "FPSCamera");
            writer.WriteStartElement("Translation");
            _position.WriteXml(writer);
            writer.WriteEndElement();
            writer.WriteStartElement("Rotation");
            _q.WriteXml(writer);
            writer.WriteEndElement();
            writer.WriteStartElement("CameraSettings");
            XMLHelper.WriteFloatAttribute(writer, "ZNear", zNear);
            XMLHelper.WriteFloatAttribute(writer, "ZFar", zFar);
            XMLHelper.WriteFloatAttribute(writer, "FOV", FoV);
            writer.WriteEndElement();
        }

        public override string ToString()
        {
            return "FPSCamera on " + _position.ToString() + " and rot " + _q.ToString();
        }

        /// <summary>
        /// Returns copy of the transformation
        /// </summary>
        /// <returns></returns>
        public override ITransformation GetCopy()
        {
            FPSCamera newtrans = new FPSCamera();
            newtrans._position = this._position;
            newtrans._changed = true;
            newtrans._q = this._q;
            newtrans.FoV = this.FoV;
            return newtrans;
        }


    }
}
