using System;
using Chernobyl.Mathematics.Vectors;
using Chernobyl.Values;

namespace Chernobyl.Mathematics.Movement
{
    /// <summary>
    /// An <see cref="ITransform"/> that rotates itself so that it will stare
    /// at another <see cref="ITransform"/>. In other words, it's forward
    /// vector (-Z axis) will always point at the position of the target 
    /// <see cref="ITransform"/>.
    /// </summary>
    public class LookAtTransform : MatrixTransform
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="LookAtTransform"/> 
        /// class that starts out enabled.
        /// </summary>
        /// <param name="target">The <see cref="ITransform"/> that this 
        /// <see cref="LookAtTransform"/> will look at (i.e. point its -Z axis
        /// at).</param>
        public LookAtTransform(ITransform target) : this(target, true)
        { }

        /// <summary>
        /// Initializes a new instance of the <see cref="LookAtTransform"/> class.
        /// </summary>
        /// <param name="target">The <see cref="ITransform"/> that this 
        /// <see cref="LookAtTransform"/> will look at (i.e. point its -Z axis
        /// at).</param>
        /// <param name="enable">True if this instance should start looking in
        /// the direction of the <paramref name="target"/> <see cref="ITransform"/>,
        /// false if otherwise.</param>
        public LookAtTransform(ITransform target, bool enable)
        {
            // Note that we must set the _isEnabled field first since the set of
            // the Target property relies on the value of that flag. Also note
            // that we set the _isEnabled field and not the property. This is
            // because the IsEnabled property relies on the value of the Target
            // which is null at the moment.
            _isEnabled = enable;
            Target = target;
        }

        /// <summary>
        /// An event handler that can be given to an event. This method, when
        /// invoked, enables this instance (sets <see cref="IsEnabled"/> to
        /// true) so that this <see cref="LookAtTransform"/> will look in the
        /// direction of the <see cref="Target"/>.
        /// </summary>
        /// <param name="sender">The instance that generated the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance 
        /// containing the event data.</param>
        public void Enable(object sender, EventArgs e)
        {
            IsEnabled = true;
        }

        /// <summary>
        /// An event handler that can be given to an event. This method, when
        /// invoked, disables this instance (sets <see cref="IsEnabled"/> to
        /// false) so that this <see cref="LookAtTransform"/> will stop looking 
        /// in the direction of the <see cref="Target"/>.
        /// </summary>
        /// <param name="sender">The instance that generated the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance 
        /// containing the event data.</param>
        public void Disable(object sender, EventArgs e)
        {
            IsEnabled = false;
        }

        /// <summary>
        /// The <see cref="ITransform"/> that this <see cref="LookAtTransform"/>
        /// will look at (i.e. point its -Z axis at).
        /// </summary>
        public ITransform Target
        {
            get { return _target; }
            set
            {
                if(value == null)
                    throw new ArgumentNullException("value",
                        "LookAtTransform.Target cannot be null.");

                // If we have an old value, detach our event handler from it
                // (but only if we haven't already done so).
                if (_target != null && IsEnabled != false)
                    _target.TransformDirtied -= TargetTransformDirtied;

                _target = value;

                // We don't need to know when the targets position has changed
                // if we are disabled.
                if(IsEnabled == true)
                {
                    _target.TransformDirtied += TargetTransformDirtied;
                    IsUpdateNeeded = true;
                }
            }
        }

        /// <summary>
        /// True if this instance should start looking in the direction of the 
        /// <see cref="Target"/> <see cref="ITransform"/>, false if otherwise.
        /// </summary>
        public bool IsEnabled
        {
            get { return _isEnabled; }
            set
            {
                bool previousValue = _isEnabled;
                _isEnabled = value;

                // We don't need to know when the targets position has changed
                // if we are disabled.
                if(_isEnabled == false)
                {
                    if(previousValue == true)
                        Target.TransformDirtied -= TargetTransformDirtied;
                }
                else if(previousValue == false)
                {
                    Target.TransformDirtied += TargetTransformDirtied;
                    IsUpdateNeeded = true;
                }
            }
        }

        /// <summary>
        /// This method is invoked during the update of the
        /// <see cref="ITransform.WorldMatrix"/> right before the
        /// <see cref="ITransform.WorldMatrix"/> is actually updated. This method
        /// ensures that this <see cref="ITransform"/> continually looks in the 
        /// direction of <see cref="Target"/>.
        /// </summary>
        /// <param name="world">The value of the <see cref="ITransform.WorldMatrix"/>.
        /// It is stored in a <see cref="LazyValue{T}"/> so that the calculation can be
        /// avoided if necessary. Don't call <see cref="LazyValue{T}.Value"/> unless
        /// required.</param>
        protected override void PreUpdate(Values.LazyValue<Matrix4> world)
        {
            base.PreUpdate(world);

            if (IsEnabled == true)
            {
                // Calculate the new axis of the new local matrix.
                Vector3 zAxis = Vector3.Normalize(LocalMatrix.Translation - Target.Position);
                Vector3 xAxis = Vector3.Normalize(Vector3.Cross(LocalMatrix.Up, zAxis));
                Vector3 yAxis = Vector3.Normalize(Vector3.Cross(zAxis, xAxis));

                LocalMatrix = new Matrix4(new Vector4(xAxis.X, yAxis.X, zAxis.X, 0.0f),
                                          new Vector4(xAxis.Y, yAxis.Y, zAxis.Y, 0.0f),
                                          new Vector4(xAxis.Z, yAxis.Z, zAxis.Z, 0.0f),
                                          LocalMatrix.Row3);
            }
        }

        /// <summary>
        /// An event handler that is invoked by the 
        /// <see cref="ITransform.TransformDirtied"/> event of the 
        /// <see cref="Target"/> transform. When invoked, this method forces
        /// this <see cref="ITransform"/> to update its 
        /// <see cref="ITransform.LocalMatrix"/> (i.e. point it in the direction
        /// requested).
        /// </summary>
        /// <param name="sender">The instance that generated the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance 
        /// containing the event data.</param>
        void TargetTransformDirtied(object sender, EventArgs e)
        {
            IsUpdateNeeded = true;
        }

        /// <summary>
        /// The backing field to <see cref="Target"/>.
        /// </summary>
        ITransform _target;

        /// <summary>
        /// The backing field to <see cref="IsEnabled"/>.
        /// </summary>
        bool _isEnabled;
    }
}