using System;
using System.Numerics;
using SlatedGameToolkit.Framework.Utilities;

namespace SlatedGameToolkit.Framework.Graphics.Render
{
    public class Camera : IMoveable {
        private bool viewUpdated = true, projectionUpdated = true, orthographic = false;

        private Vector3 position, lookAt;
        private float nearField = 0.01f, farField = 100f;
        private float width, height;
        private Matrix4x4 lookAtMatrix = Matrix4x4.Identity;
        private Matrix4x4 view = Matrix4x4.Identity, projection;
        
        /// <summary>
        /// Position updates the camera without changing camera orientation if true.
        /// Otherwise, camera will always attempt to look in the direction of the point given by the LookAt vector.
        /// </summary>
        /// <value>Whether or not to change orientation while moving camera position.</value>
        public bool LockedOrientation { get; set; } = false;

        /// <summary>
        /// The up direction for orienting the camera.
        /// </summary>
        /// <value>A vector representing the up direction.</value>
        public Vector3 Up { get; set; } = Vector3.UnitY;

        /// <summary>
        /// What the right direction is for the camera's current orientation.
        /// </summary>
        /// <value>A vector representing the right direction for the camera's current orientation.</value>
        public Vector3 CameraRight
        {
            get
            {
                return Vector3.Cross(CameraFront, Up);
            }
        }

        /// <summary>
        /// The up direction for the camera's current orientation.
        /// </summary>
        /// <value>A vector representing the up direction for the camera's current orientation.</value>
        public Vector3 CameraUp {
            get {
                return Vector3.Cross(CameraFront, -CameraRight);
            }
        }

        public bool Orthographic {
            get {
                return orthographic;
            } 
            
            set {
                orthographic = value;
                projectionUpdated = true;
            }
        }

        public Vector3 Position {
            get {
                return position;
            }
            set {
                Vector3 orientation = CameraFront;
                this.position = value;
                if (LockedOrientation) CameraFront = orientation;
                viewUpdated = true;
            }
        }

        public Vector3 CameraFront {
            get {
                return (LookAt - Position).NormalizeSafe(default(Vector3));
            }
            set {
                LookAt = (position + value.NormalizeSafe(default(Vector3)));
            }
        }

        public Vector3 LookAt {
            get {
                return lookAt;
            }
            set {
                this.lookAt = value;
                viewUpdated = true;
            }
        }

        public virtual float NearField {
            get {
                return nearField;
            }

            set {
                nearField = value;
                projectionUpdated = true;
            }
        }

        public virtual float FarField {
            get {
                return farField;
            }

            set {
                farField = value;
                projectionUpdated = true;
            }
        }

        public virtual float Width {
            get {
                return width;
            }
            set {
                width = value;
                projectionUpdated = true;
            }
        }
        public virtual float Height {
            get {
                return height;
            }
            set {
                height = value;
                projectionUpdated = true;
            }
        }

        public Matrix4x4 ViewMatrix {
            get {
                if (viewUpdated) {
                    view = Matrix4x4.CreateLookAt(Position, LookAt, Up);
                    viewUpdated = false;
                }
                return view;
            }
        }

        public Matrix4x4 ProjectionMatrix {
            get {
                if (projectionUpdated) {
                    if (Orthographic) {
                        projection = Matrix4x4.CreateOrthographic(width, height, nearField, farField);
                    } else {
                        projection = Matrix4x4.CreatePerspective(width, height, nearField, farField);
                    }
                    projectionUpdated = false;
                }
                return projection;
            }
        }

        public Vector3 MoveTo { get; set; }

        /// <summary>
        /// Whether or not the projection has changed from the last time it was read.
        /// </summary>
        /// <value>A bool value indicating if the projection matrix has changed from the last retrieval.</value>
        public bool ProjectionChanged { get => projectionUpdated; }

        /// <summary>
        /// Whether or not the camera view has changed from the last time it was read.
        /// </summary>
        /// <value>A bool value indicating if the matrix has change from the last retrieval.</value>
        public bool ViewChanged { get => viewUpdated; }

        public Camera(float width, float height) {
            if (width <= 0) throw new ArgumentException("Width can't be less than or equal to 0.");
            if (height <= 0) throw new ArgumentException("Height can't be less than or equal to 0.");
            this.Width = width;
            this.Height = height;
            this.CameraFront = -Vector3.UnitZ;
        }
    }
}