﻿using System;
using System.Drawing;
using System.Linq;

namespace Rocket.UI
{
    /// <summary>
    /// Root element for the user interface.
    /// </summary>
    public class Screen : ContentElement, IDisposable, IInputTarget
    {
        /// <summary>
        /// The composition surface.
        /// </summary>
        private readonly Surface surface = new Surface();

        /// <summary>
        /// The pending operations.
        /// </summary>
        private PropertyOptions pendingOperations;

        /// <summary>
        /// The focused control.
        /// </summary>
        private Control focused;

        /// <summary>
        /// The highlighted control.
        /// </summary>
        private Control highlighted;

        /// <summary>
        /// The captured control.
        /// </summary>
        private Control captured;

        /// <summary>
        /// Initializes a new instance of the <see cref="Screen"/> class.
        /// </summary>
        public Screen() : this(null) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="Screen"/> class.
        /// </summary>
        /// <param name="setters">The property value setters.</param>
        public Screen(params Setter[] setters) : base(setters)
        {
            this.Key += (s, e) =>
            {
                if (e.Key)
                {
                    if (e.Match(Keyboard.Tab) || e.Match(Keyboard.DownArrow) || e.Match(Keyboard.RightArrow))
                    {
                        this.FocusNext();
                        e.Handled = true;
                    }
                    else if (e.Match(Keyboard.Tab, ModifierKeys.Shift) || e.Match(Keyboard.UpArrow) || e.Match(Keyboard.LeftArrow))
                    {
                        this.FocusPrevious();
                        e.Handled = true;
                    }
                }
            };
        }

        /// <summary>
        /// Gets the input element with the keyboard focus.
        /// </summary>
        /// <value>The focused input element.</value>
        public Control Focused
        {
            get
            {
                return this.focused;
            }

            set
            {
                if (this.focused != value && (value == null || (value.CanFocus && value.IsEnabled)))
                {
                    if (this.focused != null)
                    {
                        this.focused.OnLostFocus();
                    }

                    this.focused = value;

                    if (this.focused != null)
                    {
                        this.focused.OnGainFocus();
                    }
                }
            }
        }

        /// <summary>
        /// Gets the input element with the mouse highlight.
        /// </summary>
        /// <value>The highlighted input element.</value>
        public Control Highlighted
        {
            get
            {
                return this.highlighted;
            }

            set
            {
                if (this.highlighted != value && (value == null || (value.CanHighlight && value.IsEnabled)))
                {
                    if (this.highlighted != null)
                    {
                        this.highlighted.OnLostHighlight();
                    }

                    this.highlighted = value;

                    if (this.highlighted != null)
                    {
                        this.highlighted.OnGainHighlight();
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets the captured control.
        /// </summary>
        /// <value>The captured control.</value>
        public Control Captured
        {
            get
            {
                return this.captured;
            }

            set
            {
                if (this.captured != value && (value == null || (value.CanCapture && value.IsEnabled)))
                {
                    if (this.captured != null)
                    {
                        this.captured.OnLostCapture();
                    }

                    this.captured = value;

                    if (this.captured != null)
                    {
                        this.captured.OnGainCapture();
                    }
                }
            }
        }

        /// <summary>
        /// Releases the object.
        /// </summary>
        public void Dispose()
        {
            this.surface.Dispose();
        }

        /// <summary>
        /// Focuses the control.
        /// </summary>
        /// <param name="control">The control.</param>
        public void Focus(Control control)
        {
            this.Focused = control;
        }

        /// <summary>
        /// Focuses the next control.
        /// </summary>
        public void FocusNext()
        {
            var next = default(Control);
            var found = false;

            foreach (var element in this.FindChild<Control>(i => i.IsVisible && i.CanFocus && i.IsEnabled))
            {
                if (next == null)
                {
                    next = element;
                }

                if (found)
                {
                    next = element;
                    break;
                }

                if (element == this.Focused)
                {
                    found = true;
                }
            }

            this.Focused = next;
        }

        /// <summary>
        /// Focuses the previous control.
        /// </summary>
        public void FocusPrevious()
        {
            var previous = default(Control);

            foreach (var element in this.FindChild<Control>(i => i.IsVisible && i.CanFocus && i.IsEnabled))
            {
                if (element == this.Focused && previous != null)
                {
                    break;
                }

                previous = element;
            }

            this.Focused = previous;
        }

        /// <summary>
        /// Draws the screen.
        /// </summary>
        /// <param name="width">The screen width.</param>
        /// <param name="height">The screen height.</param>
        public void Draw(float width, float height)
        {
            if (this.surface.Width != width || this.surface.Height != height)
            {
                this.pendingOperations |= PropertyOptions.AffectsMeasure;
            }

            if ((this.pendingOperations & PropertyOptions.AffectsMeasure) == PropertyOptions.AffectsMeasure)
            {
                this.Measure(new SizeF(width, height));
            }

            if ((this.pendingOperations & PropertyOptions.AffectsArrange) == PropertyOptions.AffectsArrange)
            {
                this.Arrange(new RectangleF(0, 0, width, height));
            }

            if ((this.pendingOperations & PropertyOptions.AffectsCompose) == PropertyOptions.AffectsCompose)
            {
                this.surface.Reset(width, height);
                this.Compose(this.surface);
            }

            GPU.Use(Cull.None, Depth.None, Stencil.None, Blend.Alpha);
            this.surface.Present();
            this.pendingOperations = 0;
        }

        /// <summary>
        /// Called when an axis is moved.
        /// </summary>
        /// <param name="axis">The axis.</param>
        public virtual void OnAxis(AxisInput axis) { }

        /// <summary>
        /// Called when a button is pressed or released.
        /// </summary>
        /// <param name="button">The button.</param>
        public virtual void OnButton(ButtonInput button) { }

        /// <summary>
        /// Called when an input device axis is moved.
        /// </summary>
        /// <param name="axis">The input device axis.</param>
        public virtual void OnInputDeviceAxis(InputDeviceAxis axis) { }

        /// <summary>
        /// Called when an input device button is pressed or released.
        /// </summary>
        /// <param name="button">The input device button.</param>
        public virtual void OnInputDeviceButton(InputDeviceButton button) { }

        /// <summary>
        /// Called when gamepad axis is moved.
        /// </summary>
        /// <param name="axis">The gamepad axis.</param>
        public virtual void OnGamepadAxis(GamepadAxis axis) { }

        /// <summary>
        /// Called when a gamepad button is pressed or released.
        /// </summary>
        /// <param name="button">The gamepad button.</param>
        public virtual void OnGamepadButton(GamepadButton button) { }

        /// <summary>
        /// Called when joystick axis is moved.
        /// </summary>
        /// <param name="axis">The joystick axis.</param>
        public virtual void OnJoystickAxis(JoystickAxis axis) { }

        /// <summary>
        /// Called when a joystick button is pressed or released.
        /// </summary>
        /// <param name="button">The joystick button.</param>
        public virtual void OnJoystickButton(JoystickButton button) { }

        /// <summary>
        /// Called when a key is pressed or released.
        /// </summary>
        /// <param name="key">The key.</param>
        public virtual void OnKey(Key key)
        {
            var target = this.Focused as UIElement ?? this;

            if (target.IsVisible)
            {
                target.RaiseEvent(PreviewKeyEvent, KeyEvent, new KeyEventArgs(key, Keyboard.ModifierKeys));
            }
        }

        /// <summary>
        /// Called when a key press produces a character.
        /// </summary>
        /// <param name="character">The character.</param>
        public virtual void OnCharacter(Character character)
        {
            var target = this.Focused as UIElement ?? this;

            if (target.IsVisible)
            {
                target.RaiseEvent(PreviewCharacterEvent, CharacterEvent, new CharacterEventArgs(character));
            }
        }

        /// <summary>
        /// Called when a mouse button is pressed or released.
        /// </summary>
        /// <param name="button">The mouse button.</param>
        public virtual void OnMouseButton(MouseButton button)
        {
            var position = Mouse.Position;

            if (button)
            {
                if (this.Captured != null)
                {
                    this.Captured.RaiseEvent(PreviewMouseButtonEvent, MouseButtonEvent, new MouseButtonEventArgs(button, position));
                }
                else
                {
                    this.Focused = this.HitTest<Control>(position).FirstOrDefault(i => i.CanFocus && i.IsEnabled);
                    this.Captured = this.HitTest<Control>(position).FirstOrDefault(i => i.CanCapture && i.IsEnabled);

                    if (this.Captured != null)
                    {
                        this.Captured.RaiseEvent(PreviewMouseButtonEvent, MouseButtonEvent, new MouseButtonEventArgs(button, position));
                    }
                    else
                    {
                        foreach (var element in this.HitTest<UIElement>(position))
                        {
                            element.RaiseEvent(PreviewMouseButtonEvent, MouseButtonEvent, new MouseButtonEventArgs(button, position));
                            break;
                        }
                    }
                }
            }
            else
            {
                if (this.Captured != null)
                {
                    this.Captured.RaiseEvent(PreviewMouseButtonEvent, MouseButtonEvent, new MouseButtonEventArgs(button, position));
                    this.Captured = null;
                }
                else
                {
                    foreach (var element in this.HitTest<UIElement>(position))
                    {
                        element.RaiseEvent(PreviewMouseButtonEvent, MouseButtonEvent, new MouseButtonEventArgs(button, position));
                        break;
                    }
                }
            }
        }

        /// <summary>
        /// Called when the mouse is moved.
        /// </summary>
        /// <param name="move">The mouse move input.</param>
        public virtual void OnMouseMove(MouseMove move)
        {
            var position = Mouse.Position;

            if (this.Captured != null)
            {
                this.Highlighted = this.HitTest<Control>(position).FirstOrDefault(i => i.CanHighlight && i.IsEnabled && i == this.Captured);

                this.Captured.RaiseEvent(PreviewMouseMoveEvent, MouseMoveEvent, new MouseMotionEventArgs(move, position));
            }
            else
            {
                this.Highlighted = this.HitTest<Control>(position).FirstOrDefault(i => i.CanHighlight && i.IsEnabled);

                foreach (var element in this.HitTest<UIElement>(position))
                {
                    element.RaiseEvent(PreviewMouseMoveEvent, MouseMoveEvent, new MouseMotionEventArgs(move, position));
                    break;
                }
            }
        }

        /// <summary>
        /// Called when the mouse wheel is scrolled.
        /// </summary>
        /// <param name="scroll">The mouse scroll input.</param>
        public virtual void OnMouseScroll(MouseScroll scroll)
        {
            var position = Mouse.Position;

            if (this.Captured != null)
            {
                this.Captured.RaiseEvent(PreviewMouseScrollEvent, MouseScrollEvent, new MouseMotionEventArgs(scroll, position));
            }
            else
            {
                foreach (var element in this.HitTest<UIElement>(position))
                {
                    element.RaiseEvent(PreviewMouseScrollEvent, MouseScrollEvent, new MouseMotionEventArgs(scroll, position));
                    break;
                }
            }
        }

        /// <summary>
        /// Called when an element is invalidated.
        /// </summary>
        /// <param name="element">The invalidated element.</param>
        protected override void OnInvalidate(Element element)
        {
            this.pendingOperations |= PropertyOptions.AffectsMeasure;
            base.OnInvalidate(element);
        }

        /// <summary>
        /// Called when the element tree structure is changed.
        /// </summary>
        /// <param name="removed">The removed element. Null if nothing was removed.</param>
        /// <param name="added">The added element. Null if nothing was added.</param>
        protected override void OnChildChanged(Element removed, Element added)
        {
            this.Invalidate();
            base.OnChildChanged(removed, added);
        }

        /// <summary>
        /// Called when a property value change affects the parent.
        /// </summary>
        /// <param name="source">The source element.</param>
        /// <param name="property">The property.</param>
        /// <param name="previousValue">The previous value.</param>
        protected override void OnParentAffected(Element source, Property property, object previousValue)
        {
            this.pendingOperations |= property.Options;
            base.OnParentAffected(source, property, previousValue);
        }
    }
}
