using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace BeWo.Controls
{
    /// <summary>
    /// FishEyePanel
    /// </summary>
    public class FishEyePanel : Panel
    {
        // Using a DependencyProperty as the backing store for AnimationMilliseconds.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AnimationMillisecondsProperty = DependencyProperty.Register("AnimationMilliseconds", typeof(int), typeof(FishEyePanel), new UIPropertyMetadata(525));

        public static readonly DependencyProperty MagnificationProperty = DependencyProperty.Register("Magnification", typeof(double), typeof(FishEyePanel), new UIPropertyMetadata(2d));

        // If set true we scale different sized children to a constant width

        // Using a DependencyProperty as the backing store for ScaleToFit.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ScaleToFitProperty = DependencyProperty.Register("ScaleToFit", typeof(bool), typeof(FishEyePanel), new UIPropertyMetadata(true));

        private bool animating;

        private Size ourSize;

        private double totalChildWidth;

        private bool wasMouseOver;

        public FishEyePanel()
        {
            this.Background = Brushes.Transparent;
            this.MouseMove += this.FishEyePanel_MouseMove;
            this.MouseEnter += this.FishEyePanel_MouseEnter;
            this.MouseLeave += this.FishEyePanel_MouseLeave;
        }

        private enum AnimateState
        {
            None, 
            Up, 
            Down
        } ;

        public int AnimationMilliseconds
        {
            get
            {
                return (int)this.GetValue(AnimationMillisecondsProperty);
            }

            set
            {
                this.SetValue(AnimationMillisecondsProperty, value);
            }
        }

        public double Magnification
        {
            get
            {
                return (double)this.GetValue(MagnificationProperty);
            }

            set
            {
                this.SetValue(MagnificationProperty, value);
            }
        }

        public bool ScaleToFit
        {
            get
            {
                return (bool)this.GetValue(ScaleToFitProperty);
            }

            set
            {
                this.SetValue(ScaleToFitProperty, value);
            }
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            if (this.Children == null || this.Children.Count == 0)
            {
                return finalSize;
            }

            this.ourSize = finalSize;
            this.totalChildWidth = 0;

            foreach (UIElement child in this.Children)
            {
                // If this is the first time we've seen this child, add our transforms
                if (child.RenderTransform as TransformGroup == null)
                {
                    child.RenderTransformOrigin = new Point(0, 1);
                    TransformGroup group = new TransformGroup();
                    child.RenderTransform = group;
                    group.Children.Add(new ScaleTransform());
                    group.Children.Add(new TranslateTransform());
                    group.Children.Add(new RotateTransform());
                }

                child.Arrange(new Rect(0, 0, child.DesiredSize.Width, child.DesiredSize.Height));
                this.totalChildWidth += child.DesiredSize.Width;
            }

            this.AnimateAll();

            return finalSize;
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            Size idealSize = new Size(0, 0);

            // Allow children as much room as they want - then scale them
            Size size = new Size(Double.PositiveInfinity, Double.PositiveInfinity);
            foreach (UIElement child in this.Children)
            {
                child.Measure(size);
                idealSize.Width += child.DesiredSize.Width;
                idealSize.Height = Math.Max(idealSize.Height, child.DesiredSize.Height);
            }

            // EID calls us with infinity, but framework doesn't like us to return infinity
            if (double.IsInfinity(availableSize.Height) || double.IsInfinity(availableSize.Width))
            {
                return idealSize;
            }
            else
            {
                return availableSize;
            }
        }

        private void AnimateAll()
        {
            if (this.Children == null || this.Children.Count == 0)
            {
                return;
            }

            this.animating = true;

            double childWidth = this.ourSize.Width / this.Children.Count;

            // Scale the children so they fit in our size
            double overallScaleFactor = this.ourSize.Width / this.totalChildWidth;

            UIElement prevChild = null;
            UIElement theChild = null;
            UIElement nextChild = null;

            double widthSoFar = 0;
            double theChildX = 0;
            double ratio = 0;

            if (this.IsMouseOver)
            {
                double x = Mouse.GetPosition(this).X;
                foreach (UIElement child in this.Children)
                {
                    if (theChild == null)
                    {
                        theChildX = widthSoFar;
                    }

                    widthSoFar += this.ScaleToFit ? childWidth : child.DesiredSize.Width * overallScaleFactor;
                    if (x < widthSoFar && theChild == null)
                    {
                        theChild = child;
                    }

                    if (theChild == null)
                    {
                        prevChild = child;
                    }

                    if (nextChild == null && theChild != child && theChild != null)
                    {
                        nextChild = child;
                        break;
                    }
                }

                if (theChild != null)
                {
                    ratio = (x - theChildX) / (this.ScaleToFit ? childWidth : (theChild.DesiredSize.Width * overallScaleFactor)); // Range 0-1 of where the mouse is inside the child
                }
            }

            // These next few lines took two of us hours to write!
            double mag = this.Magnification;
            double extra = 0;
            if (theChild != null)
            {
                extra += mag - 1;
            }

            if (prevChild == null)
            {
                extra += ratio * (mag - 1);
            }
            else if (nextChild == null)
            {
                extra += (mag - 1) * (1 - ratio);
            }
            else
            {
                extra += mag - 1;
            }

            double prevScale = this.Children.Count * (1 + ((mag - 1) * (1 - ratio))) / (this.Children.Count + extra);
            double theScale = (mag * this.Children.Count) / (this.Children.Count + extra);
            double nextScale = this.Children.Count * (1 + ((mag - 1) * ratio)) / (this.Children.Count + extra);
            double otherScale = this.Children.Count / (this.Children.Count + extra); // Applied to all non-interesting children

            // Adjust for different sized children - we overmagnify large children, so shrink the others
            if (!this.ScaleToFit && this.IsMouseOver)
            {
                double bigWidth = 0;
                double actualWidth = 0;
                if (prevChild != null)
                {
                    bigWidth += prevScale * prevChild.DesiredSize.Width * overallScaleFactor;
                    actualWidth += prevChild.DesiredSize.Width;
                }

                if (theChild != null)
                {
                    bigWidth += theScale * theChild.DesiredSize.Width * overallScaleFactor;
                    actualWidth += theChild.DesiredSize.Width;
                }

                if (nextChild != null)
                {
                    bigWidth += nextScale * nextChild.DesiredSize.Width * overallScaleFactor;
                    actualWidth += nextChild.DesiredSize.Width;
                }

                double w = (this.totalChildWidth - actualWidth) * overallScaleFactor * otherScale;
                otherScale *= (this.ourSize.Width - bigWidth) / w;
            }

            widthSoFar = 0;
            double duration = 0;
            if (this.wasMouseOver != this.IsMouseOver)
            {
                duration = this.AnimationMilliseconds;
            }

            foreach (UIElement child in this.Children)
            {
                double scale = otherScale;
                if (child == prevChild)
                {
                    scale = prevScale;
                }
                else if (child == theChild)
                {
                    scale = theScale;
                }
                else if (child == nextChild)
                {
                    scale = nextScale;
                }

                if (this.ScaleToFit)
                {
                    // Now scale each individual child so it is a standard width
                    scale *= childWidth / child.DesiredSize.Width;
                }
                else
                {
                    // Apply overall scale so all children fit our width
                    scale *= overallScaleFactor;
                }

                this.AnimateTo(child, 0, widthSoFar, this.ourSize.Height - child.DesiredSize.Height, scale, duration);
                widthSoFar += child.DesiredSize.Width * scale;
            }

            this.wasMouseOver = this.IsMouseOver;
        }

        private void AnimateTo(UIElement child, double r, double x, double y, double s, double duration)
        {
            TransformGroup group = (TransformGroup)child.RenderTransform;
            ScaleTransform scale = (ScaleTransform)group.Children[0];
            TranslateTransform trans = (TranslateTransform)group.Children[1];

            // RotateTransform rot = (RotateTransform)group.Children[2];

            if (duration == 0)
            {
                trans.BeginAnimation(TranslateTransform.XProperty, null);
                trans.BeginAnimation(TranslateTransform.YProperty, null);
                scale.BeginAnimation(ScaleTransform.ScaleXProperty, null);
                scale.BeginAnimation(ScaleTransform.ScaleYProperty, null);

                // rot.BeginAnimation(RotateTransform.AngleProperty, null);
                trans.X = x;
                trans.Y = y;
                scale.ScaleX = s;
                scale.ScaleY = s;

                // rot.AngleProperty = r;
                this.animation_Completed(null, null);
            }
            else
            {
                trans.BeginAnimation(TranslateTransform.XProperty, this.MakeAnimation(x, duration, this.animation_Completed));
                trans.BeginAnimation(TranslateTransform.YProperty, this.MakeAnimation(y, duration));
                scale.BeginAnimation(ScaleTransform.ScaleXProperty, this.MakeAnimation(s, duration));
                scale.BeginAnimation(ScaleTransform.ScaleYProperty, this.MakeAnimation(s, duration));
                // rot.BeginAnimation(RotateTransform.AngleProperty, MakeAnimation(r, duration));
            }
        }

        private void FishEyePanel_MouseEnter(object sender, MouseEventArgs e)
        {
            this.InvalidateArrange();
        }

        private void FishEyePanel_MouseLeave(object sender, MouseEventArgs e)
        {
            this.InvalidateArrange();
        }

        private void FishEyePanel_MouseMove(object sender, MouseEventArgs e)
        {
            if (!this.animating)
            {
                this.InvalidateArrange();
            }
        }

        private DoubleAnimation MakeAnimation(double to, double duration)
        {
            return this.MakeAnimation(to, duration, null);
        }

        private DoubleAnimation MakeAnimation(double to, double duration, EventHandler endEvent)
        {
            DoubleAnimation anim = new DoubleAnimation(to, TimeSpan.FromMilliseconds(duration));
            anim.AccelerationRatio = 0.2;
            anim.DecelerationRatio = 0.7;
            if (endEvent != null)
            {
                anim.Completed += endEvent;
            }

            return anim;
        }

        private void animation_Completed(object sender, EventArgs e)
        {
            this.animating = false;
        }
    }
}