using System.Diagnostics;
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;

namespace WpfSandbox
{
    /// <summary>
    /// コマンドに関するサービスを提供します。
    /// </summary>
    public class CommandService : DependencyObject
    {
        /// <summary>
        /// <see cref="RoutedEventBinding"/> に紐づいたコマンドの添付プロパティを識別します。
        /// </summary>
        public static readonly DependencyProperty RoutedEventBindingProperty =
            DependencyProperty.RegisterAttached(
                "RoutedEventBinding",
                typeof(RoutedEventBinding),
                typeof(CommandService),
                new PropertyMetadata(null, OnRoutedEventChanged));

        /// <summary>
        /// 実行されるコマンドの添付プロパティを識別します。
        /// </summary>
        private static readonly DependencyProperty CommandProperty =
            DependencyProperty.Register(
                "Command",
                typeof(ICommand),
                typeof(CommandService),
                new PropertyMetadata(null));

        /// <summary>
        /// コマンドの実行時にコマンドへ渡すことのできるユーザー定義データの値の添付プロパティを識別します。
        /// </summary>
        private static readonly DependencyProperty CommandParameterProperty =
            DependencyProperty.Register(
                "CommandParameter",
                typeof(object),
                typeof(CommandService),
                new PropertyMetadata(null));

        /// <summary>
        /// コマンドが実行されているオブジェクトの添付プロパティを識別します。
        /// </summary>
        private static readonly DependencyProperty CommandTargetProperty =
            DependencyProperty.Register(
                "CommandTarget",
                typeof(IInputElement),
                typeof(CommandService),
                new PropertyMetadata(null));

        /// <summary>
        /// <see cref="RoutedEventBinding"/> に紐づいたコマンドを実行する処理の添付プロパティを識別します。
        /// </summary>
        private static readonly DependencyProperty RoutedEventActionProperty =
            DependencyProperty.Register(
                "RoutedEventAction",
                typeof(RoutedEventHandler),
                typeof(CommandService),
                new PropertyMetadata(null));

        /// <summary>
        /// <see cref="RoutedEvent"/> に紐づいたコマンドを取得します。
        /// </summary>
        /// <param name="d">対象の要素。</param>
        /// <returns><see cref="RoutedEvent"/> に紐づいたコマンド。</returns>
        [AttachedPropertyBrowsableForType(typeof(UIElement))]
        public static RoutedEventBinding GetRoutedEventBinding(DependencyObject d)
        {
            return (RoutedEventBinding)d.GetValue(RoutedEventBindingProperty);
        }

        /// <summary>
        /// <see cref="RoutedEvent"/> に紐づいたコマンドを設定します。
        /// </summary>
        /// <param name="d">対象の要素。</param>
        /// <param name="routedEventCommand"><see cref="RoutedEvent"/> に紐づいたコマンド。</param>
        [AttachedPropertyBrowsableForType(typeof(UIElement))]
        public static void SetRoutedEventBinding(DependencyObject d, RoutedEventBinding routedEventBinding)
        {
            d.SetValue(RoutedEventBindingProperty, routedEventBinding);
        }

        /// <summary>
        /// <see cref="RoutedEvent"/> に紐づいたコマンドの更新時の処理です。
        /// </summary>
        /// <param name="d">プロパティの値が変更されたオブジェクト。</param>
        /// <param name="e">イベント引数。</param>
        private static void OnRoutedEventChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            UIElement element = d as UIElement;
            if (element == null)
            {
                return;
            }

            RoutedEventBinding oldRoutedEventBinding = e.OldValue as RoutedEventBinding;
            if (oldRoutedEventBinding != null)
            {
                RemoveHandler(oldRoutedEventBinding, element);
            }

            RoutedEventBinding newRoutedEventBinding = e.NewValue as RoutedEventBinding;
            if (newRoutedEventBinding != null)
            {
                AddHandler(newRoutedEventBinding, element);
            }
        }

        /// <summary>
        /// イベントハンドラを追加します。
        /// </summary>
        /// <param name="routedEventBinding"><see cref="RoutedEvent"/> に紐づいたコマンド。</param>
        /// <param name="element">イベントハンドラを追加する要素。</param>
        /// <exception cref="ArgumentNullException"><paramref name="element"/>が<c>null</c>です。</exception>
        private static void AddHandler(RoutedEventBinding routedEventBinding, UIElement element)
        {
            Debug.Assert(routedEventBinding != null, "routedEventBinding != null");
            Debug.Assert(element != null, "element != null");

            if (routedEventBinding.RoutedEvent == null)
            {
                return;
            }

            if (routedEventBinding.Command != null)
            {
                BindingOperations.SetBinding(element, CommandProperty, routedEventBinding.Command);
            }

            if (routedEventBinding.CommandParameter != null)
            {
                BindingOperations.SetBinding(element, CommandParameterProperty, routedEventBinding.CommandParameter);
            }

            if (routedEventBinding.CommandTarget != null)
            {
                BindingOperations.SetBinding(element, CommandTargetProperty, routedEventBinding.CommandTarget);
            }

            RoutedEventHandler routedEventHandler = (sender, e) =>
            {
                ICommand command = GetValue<ICommand>(element, CommandProperty);
                if (command == null)
                {
                    return;
                }

                object parameter = GetValue<object>(element, CommandParameterProperty);
                RoutedCommand routed = command as RoutedCommand;
                if (routed != null)
                {
                    IInputElement target = GetValue<IInputElement>(element, CommandTargetProperty) ?? e.Source as IInputElement;
                    if (routed.CanExecute(parameter, target))
                    {
                        routed.Execute(parameter, target);
                    }
                }
                else
                {
                    if (command.CanExecute(parameter))
                    {
                        command.Execute(parameter);
                    }
                }
            };
            element.SetValue(RoutedEventActionProperty, routedEventHandler);
            element.AddHandler(routedEventBinding.RoutedEvent, routedEventHandler);
        }

        /// <summary>
        /// イベントハンドラを削除します。
        /// </summary>
        /// <param name="routedEventBinding"><see cref="RoutedEvent"/> に紐づいたコマンド。</param>
        /// <param name="element">イベントハンドラを削除する要素。</param>
        /// <exception cref="ArgumentNullException"><paramref name="element"/>が<c>null</c>です。</exception>
        private static void RemoveHandler(RoutedEventBinding routedEventBinding, UIElement element)
        {
            Debug.Assert(routedEventBinding != null, "routedEventBinding != null");
            Debug.Assert(element != null, "element != null");

            if (routedEventBinding.RoutedEvent == null)
            {
                return;
            }

            if (routedEventBinding.Command != null)
            {
                BindingOperations.ClearBinding(element, CommandProperty);
            }

            if (routedEventBinding.CommandParameter != null)
            {
                BindingOperations.ClearBinding(element, CommandParameterProperty);
            }

            if (routedEventBinding.CommandTarget != null)
            {
                BindingOperations.ClearBinding(element, CommandTargetProperty);
            }

            RoutedEventHandler routedEventAction = element.GetValue(RoutedEventActionProperty) as RoutedEventHandler;
            if (routedEventAction == null)
            {
                return;
            }

            element.RemoveHandler(routedEventBinding.RoutedEvent, routedEventAction);
        }

        /// <summary>
        /// プロパティの値を取得します。
        /// </summary>
        /// <typeparam name="T">プロパティの値の型。</typeparam>
        /// <param name="d">プロパティの値を取得するオブジェクト。</param>
        /// <param name="dp">プロパティの値を取得する添付プロパティ。</param>
        /// <returns></returns>
        private static T GetValue<T>(DependencyObject d, DependencyProperty dp)
        {
            object value = d.GetValue(dp);
            if (value is T)
            {
                return (T)value;
            }

            return default;
        }
    }
}