using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace InvExplorer
{
    /// <summary>
    /// Speficies the type of transition to apply when switching panels.
    /// </summary>
    public enum TransitionMode
    {
        /// <summary>
        /// Panels switch in and out of view instantaneously.
        /// </summary>
        Instant,
        /// <summary>
        /// Panels fade in and out of view.
        /// </summary>
        Fade,
        /// <summary>
        /// Panels rotate in and out of view.
        /// </summary>
        Project
    }
    /// <summary>
    /// Panel that shows one child at a time and provides an animation 
    /// when switching the active child.
    /// </summary>
    public class TransitionPanel : Grid
    {
        ICommand _cmdSwitchPanel;
        PanelTransition _transition;

        //----------------------------------------------------------------------------------
        #region ** ctor

        /// <summary>
        /// Initializes a new instance of a <see cref="TransitionPanel"/>.
        /// </summary>
        public TransitionPanel()
        {
            // create switch panel command
            _cmdSwitchPanel = new SwitchPanelCommand(this);
            
            // initialize panel visibility when loading
            Loaded += (s, e) =>
            {
                UpdateChildrenVisibility(SelectedIndex, -1);
            };
        }

        #endregion

        //----------------------------------------------------------------------------------
        #region ** object model

        /// <summary>
        /// Gets or sets the index of the child that is currently visible in the panel.
        /// </summary>
        public int SelectedIndex
        {
            get { return (int)GetValue(SelectedIndexProperty); }
            set { SetValue(SelectedIndexProperty, value); }
        }
        /// <summary>
        /// Identifies the <see cref="ItemsSource"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty SelectedIndexProperty =
            DependencyProperty.Register("SelectedIndex", typeof(int),
                typeof(TransitionPanel), new PropertyMetadata(-1, TransitionPanel.OnSelectedIndexChanged));
        static void OnSelectedIndexChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var tp = (TransitionPanel)d;
            tp.Switch((int)e.NewValue, (int)e.OldValue);
            tp.OnSelectedIndexChanged(EventArgs.Empty);    
        }
        /// <summary>
        /// Occurs when the value of the <see cref="SelectedIndex"/> property changes.
        /// </summary>
        public event EventHandler SelectedIndexChanged;
        /// <summary>
        /// Raises the <see cref="SelectedIndexChanged"/> event.
        /// </summary>
        /// <param name="e"><see cref="EventArgs"/> that contais the event data.</param>
        protected virtual void OnSelectedIndexChanged(EventArgs e)
        {
            if (SelectedIndexChanged != null)
                SelectedIndexChanged(this, e);
        }
        /// <summary>
        /// Gets or sets the IPanelTransition class that provides the transition effects.
        /// </summary>
        public TransitionMode TransitionMode
        {
            get { return (TransitionMode)GetValue(TransitionModeProperty); }
            set { SetValue(TransitionModeProperty, value); }
        }
        /// <summary>
        /// Identifies the <see cref="TransitionMode"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty TransitionModeProperty =
            DependencyProperty.Register("TransitionMode", typeof(TransitionMode),
                typeof(TransitionPanel), new PropertyMetadata(TransitionMode.Project));
        /// <summary>
        /// Gets or sets the duration of the transitions.
        /// </summary>
        public double Duration
        {
            get { return (double)GetValue(DurationProperty); }
            set { SetValue(DurationProperty, value); }
        }
        /// <summary>
        /// Identifies the <see cref="Duration"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty DurationProperty =
            DependencyProperty.Register("Duration", typeof(double),
                typeof(TransitionPanel), new PropertyMetadata(500.0));
        /// <summary>
        /// Gets an ICommand used to switch to a panel passed as an index or element name.
        /// </summary>
        public ICommand SwitchPanelCommand
        {
            get { return _cmdSwitchPanel; }
        }
        /// <summary>
        /// Sets the focus to the currently visible child.
        /// </summary>
        /// <returns>True if the focus was set successfully.</returns>
        public bool Focus()
        {
            if (this.SelectedIndex > -1 && this.SelectedIndex < Children.Count)
            {
                var ctl = FindControl(this.Children[this.SelectedIndex]);
                if (ctl != null)
                {
                    return ctl.Focus();
                }
            }
            return false;
        }

        #endregion

        //----------------------------------------------------------------------------------
        #region ** implementation

        void Switch(int newIndex, int oldIndex)
        {
            // update transition if necessary
            switch (TransitionMode)
            {
                case TransitionMode.Instant:
                    if (_transition == null || _transition.GetType() != typeof(PanelTransition))
                    {
                        _transition = new PanelTransition();
                    }
                    break;
                case TransitionMode.Fade:
                    if (_transition == null || _transition.GetType() != typeof(FadeTransition))
                    {
                        _transition = new FadeTransition();
                    }
                    break;
                case TransitionMode.Project:
                    if (_transition == null || _transition.GetType() != typeof(ProjectionTransition))
                    {
                        _transition = new ProjectionTransition();
                    }
                    break;
            }

            // show only the child elements involved in the transition
            UpdateChildrenVisibility(newIndex, oldIndex);

            // apply the transition
            _transition.Switch(GetChild(newIndex), GetChild(oldIndex), Duration);
        }
        void UpdateChildrenVisibility(int newIndex, int oldIndex)
        {
            for (int i = 0; i < Children.Count; i++)
            {
                Children[i].Visibility = i == newIndex || i == oldIndex
                    ? Visibility.Visible
                    : Visibility.Collapsed;
            }
        }
        UIElement GetChild(int index)
        {
            return index > -1 && index < Children.Count ? Children[index] : null;
        }
        Control FindControl(UIElement e)
        {
            var ctl = e as Control;
            if (ctl != null && ctl.IsEnabled && ctl.Visibility == Visibility.Visible)
            {
                return ctl;
            }
            var panel = e as Panel;
            if (panel != null)
            {
                foreach (var child in panel.Children)
                {
                    ctl = FindControl(child);
                    if (ctl != null)
                    {
                        return ctl;
                    }
                }
            }
            return null;
        }

        #endregion
    }

    /// <summary>
    /// Command to switch panels.
    /// Parameter specifies the panel to switch to (index or child name).
    /// No parameter means switch to the next panel, wrapping back to the first at the end.
    /// </summary>
    internal class SwitchPanelCommand : ICommand
    {
        TransitionPanel _panel;

        public SwitchPanelCommand(TransitionPanel panel)
        {
            _panel = panel;
            _panel.SelectedIndexChanged += (s, e) => { OnCanExecuteChanged(e); };
        }

        // ** ICommand
        public bool CanExecute(object parameter)
        {
            // by index
            if (parameter is int)
            {
                var index = (int)parameter;
                return index > -1 && index < _panel.Children.Count && index != _panel.SelectedIndex;
            }

            // by name
            if (parameter is string)
            {
                var name = (string)parameter;
                for (int i = 0; i < _panel.Children.Count; i++)
                {
                    var child = _panel.Children[i] as FrameworkElement;
                    if (child.Name == name && i != _panel.SelectedIndex)
                    {
                        return true;
                    }
                }
                return false;
            }

            // move to next panel
            return _panel.Children.Count > 1;
        }
        public event EventHandler CanExecuteChanged;
        protected virtual void OnCanExecuteChanged(EventArgs e)
        {
            if (CanExecuteChanged != null)
                CanExecuteChanged(this, e);
        }
        public void Execute(object parameter)
        {
            // by index
            if (parameter is int)
            {
                _panel.SelectedIndex = (int)parameter;
                _panel.Focus();
                return;
            }

            // by name
            if (parameter is string)
            {
                var name = (string)parameter;
                for (int i = 0; i < _panel.Children.Count; i++)
                {
                    var child = _panel.Children[i] as FrameworkElement;
                    if (child.Name == name)
                    {
                        _panel.SelectedIndex = i;
                        _panel.Focus();
                        break;
                    }
                }
                return;
            }

            // not specified, move to next panel
            if (_panel.Children.Count > 1)
            {
                _panel.SelectedIndex = (_panel.SelectedIndex + 1) % _panel.Children.Count;
                _panel.Focus();
            }
        }
    }

    /// <summary>
    /// Provides an instantaneous transition.
    /// </summary>
    internal class PanelTransition
    {
        public virtual void Switch(UIElement eIn, UIElement eOut, double duration)
        {
            if (eIn != null)
            {
                eIn.Visibility = Visibility.Visible;
            }
            if (eOut != null)
            {
                eOut.Visibility = Visibility.Collapsed;
            }
        }
    }
    /// <summary>
    /// Provides a projection transition that turns panels in pseudo 3-D.
    /// </summary>
    internal class ProjectionTransition : PanelTransition
    {
        string _property;
        public ProjectionTransition(bool vertical)
        {
            _property = vertical ? "RotationX" : "RotationY";
        }
        public ProjectionTransition() : this(false)
        {
        }
        public override void Switch(UIElement eIn, UIElement eOut, double duration)
        {
            // create storyboard
            var sb = new Storyboard();

            // initialize parameters
            var ts = duration / 2;

            // rotate eOut from zero to 90
            if (eOut != null)
            {
                var da = CreateAnimation(eOut, _property, 0, 90, ts, 0);
                sb.Children.Add(da);

                var qe = new QuadraticEase();
                qe.EasingMode = EasingMode.EaseIn;
                da.EasingFunction = qe;
            }

            // rotate eIn from 90 to zero
            if (eIn != null)
            {
                var da = CreateAnimation(eIn, _property, -90, 0, ts, eOut != null ? ts : 0);
                sb.Children.Add(da);

                var qe = new QuadraticEase();
                qe.EasingMode = EasingMode.EaseOut;
                da.EasingFunction = qe;
            }

            // do it
            sb.Begin();
            sb.Completed += (s, e) => { base.Switch(eIn, eOut, duration); };
        }
        DoubleAnimation CreateAnimation(UIElement e, string property, double from, double to, double duration, double begin)
        {
            if (e.Projection == null)
            {
                e.Projection = new PlaneProjection();
                e.Projection.GetType().GetProperty(property).SetValue(e.Projection, from, null);
            }

            var da = new DoubleAnimation();
            da.From = from;
            da.To = to;
            da.Duration = TimeSpan.FromMilliseconds(duration);
            da.BeginTime = TimeSpan.FromMilliseconds(begin);
            Storyboard.SetTargetProperty(da, new PropertyPath(property));
            Storyboard.SetTarget(da, e.Projection);

            return da;
        }
    }
    /// <summary>
    /// Provides a transition that fades panels in and out of view.
    /// </summary>
    internal class FadeTransition : PanelTransition
    {
        public override void Switch(UIElement eIn, UIElement eOut, double duration)
        {
            // create storyboard
            var sb = new Storyboard();

            // make sure eIn is behind eOut
            if (eIn != null)
            {
                eIn.SetValue(Canvas.ZIndexProperty, 0);
                eIn.RenderTransform = null;
            }

            // fade out, fade in
            if (eOut != null)
            {
                eOut.SetValue(Canvas.ZIndexProperty, 1);

                var da = new DoubleAnimation();
                da.From = 1;
                da.To = 0;
                da.Duration = TimeSpan.FromMilliseconds(duration);
                Storyboard.SetTargetProperty(da, new PropertyPath("Opacity"));
                Storyboard.SetTarget(da, eOut);
                sb.Children.Add(da);

                da = new DoubleAnimation();
                da.From = 0;
                da.To = 1;
                da.Duration = TimeSpan.FromMilliseconds(duration);
                Storyboard.SetTargetProperty(da, new PropertyPath("Opacity"));
                Storyboard.SetTarget(da, eIn);
                sb.Children.Add(da);
            }

            // do it
            sb.Begin();
            sb.Completed += (s, e) => { base.Switch(eIn, eOut, duration); };
        }
    }
}
