using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;

//AUS AvalonControlsLibrary

namespace BeWo.Controls
{
    /// <summary>
    /// Time Picker as a control that lets the user select a specific time
    /// </summary>
    [TemplatePart(Name = "PART_Hours", Type = typeof(TextBox))]
    [TemplatePart(Name = "PART_Minutes", Type = typeof(TextBox))]
    [TemplatePart(Name = "PART_Seconds", Type = typeof(TextBox))]
    [TemplatePart(Name = "PART_IncreaseTime", Type = typeof(ButtonBase))]
    [TemplatePart(Name = "PART_DecrementTime", Type = typeof(ButtonBase))]
    public class TimePicker : Control
    {
        /// <summary>
        /// Gets or sets the maximum time that can be selected
        /// </summary>
        public static readonly DependencyProperty MaxTimeProperty = DependencyProperty.Register(
            "MaxTime", 
            typeof(TimeSpan), 
            typeof(TimePicker), 
            new UIPropertyMetadata(
                TimeSpan.MaxValue, 
                delegate(DependencyObject sender, DependencyPropertyChangedEventArgs e)
                    {
                        TimePicker picker = (TimePicker)sender;
                        picker.HourMaxValue = picker.MaxTime.Hours;
                        picker.MinuteMaxValue = picker.MaxTime.Minutes;
                        picker.SecondMaxValue = picker.MaxTime.Seconds;
                        picker.CoerceValue(SelectedTimeProperty); // make sure to update the time if appropiate
                    }));

        /// <summary>
        /// Gets or sets the minimum time selected
        /// </summary>
        public static readonly DependencyProperty MinTimeProperty = DependencyProperty.Register(
            "MinTime", 
            typeof(TimeSpan), 
            typeof(TimePicker), 
            new UIPropertyMetadata(
                TimeSpan.MinValue, 
                delegate(DependencyObject sender, DependencyPropertyChangedEventArgs e)
                    {
                        TimePicker picker = (TimePicker)sender;
                        picker.HourMinValue = picker.MinTime.Hours;
                        picker.MinuteMinValue = picker.MinTime.Minutes;
                        picker.SecondMinValue = picker.MinTime.Seconds;
                        picker.CoerceValue(SelectedTimeProperty); // make sure to update the time if appropiate
                    }));

        /// <summary>
        /// Backing store for the selected hour
        /// </summary>
        public static readonly DependencyProperty SelectedHourProperty = DependencyProperty.Register(
            "SelectedHour", 
            typeof(int), 
            typeof(TimePicker), 
            new UIPropertyMetadata(
                0, 
                delegate(DependencyObject sender, DependencyPropertyChangedEventArgs e)
                    {
                        TimePicker timePicker = (TimePicker)sender;

                        // validate the hour set
                        int hour = MathUtil.ValidateNumber(timePicker.SelectedHour, timePicker.HourMinValue, timePicker.HourMaxValue);
                        if (hour != timePicker.SelectedHour)
                        {
                            timePicker.SelectedHour = hour;
                        }

                        // set the new timespan
                        SetNewTime(timePicker);
                    }));

        /// <summary>
        /// Backing store for the selected minsutes
        /// </summary>
        public static readonly DependencyProperty SelectedMinuteProperty = DependencyProperty.Register(
            "SelectedMinute", 
            typeof(int), 
            typeof(TimePicker), 
            new UIPropertyMetadata(
                0, 
                delegate(DependencyObject sender, DependencyPropertyChangedEventArgs e)
                    {
                        TimePicker timePicker = (TimePicker)sender;

                        // validate the minute set
                        int min = MathUtil.ValidateNumber(timePicker.SelectedMinute, timePicker.MinuteMinValue, timePicker.MinuteMaxValue);
                        if (min != timePicker.SelectedMinute)
                        {
                            timePicker.SelectedMinute = min;
                        }

                        // set the new timespan
                        SetNewTime(timePicker);
                    }));

        /// <summary>
        /// Backing store for the selected second
        /// </summary>
        public static readonly DependencyProperty SelectedSecondProperty = DependencyProperty.Register(
            "SelectedSecond", 
            typeof(int), 
            typeof(TimePicker), 
            new UIPropertyMetadata(
                0, 
                delegate(DependencyObject sender, DependencyPropertyChangedEventArgs e)
                    {
                        TimePicker timePicker = (TimePicker)sender;

                        // validate the minute set
                        int sec = MathUtil.ValidateNumber(timePicker.SelectedSecond, timePicker.SecondMinValue, timePicker.SecondMaxValue);
                        if (sec != timePicker.SelectedSecond)
                        {
                            timePicker.SelectedSecond = sec;
                        }

                        // set the new timespan
                        SetNewTime(timePicker);
                    }));

        /// <summary>
        /// 
        /// </summary>
        public static readonly RoutedEvent SelectedTimeChangedEvent = EventManager.RegisterRoutedEvent("SelectedTimeChanged", RoutingStrategy.Bubble, typeof(TimeSelectedChangedEventHandler), typeof(TimePicker));

        /// <summary>
        /// Backing store for the selected timestamp 
        /// </summary>
        public static readonly DependencyProperty SelectedTimeProperty = DependencyProperty.Register("SelectedTime", typeof(TimeSpan), typeof(TimePicker), new UIPropertyMetadata(new TimeSpan(0, 0, 0), SelectedTimePropertyChanged, ForceValidSelectedTime));

        private int HourMaxValue = 23;

        private int HourMinValue;

        private int MinuteMaxValue = 59;

        private int MinuteMinValue;

        private int SecondMaxValue = 59;

        private int SecondMinValue;

        private TextBox currentlySelectedTextBox;

        // data memebers to store the textboxes for hours, minutes and seconds
        private TextBox hours;

        private bool isUpdatingTime;

        private TextBox minutes;

        private TextBox seconds;

        /// <summary>
        /// Static constructor
        /// </summary>
        static TimePicker()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(TimePicker), new FrameworkPropertyMetadata(typeof(TimePicker)));
        }

        /// <summary>
        /// Default constructor
        /// </summary>
        public TimePicker()
        {
            this.SelectedTime = DateTime.Now.TimeOfDay;
        }

        public event TimeSelectedChangedEventHandler SelectedTimeChanged
        {
            add
            {
                this.AddHandler(SelectedTimeChangedEvent, value);
            }

            remove
            {
                this.RemoveHandler(SelectedTimeChangedEvent, value);
            }
        }

        // the textbox that is selected

        /// <summary>
        /// Gets or sets the maximum time that can be selected
        /// </summary>
        public TimeSpan MaxTime
        {
            get
            {
                return (TimeSpan)this.GetValue(MaxTimeProperty);
            }

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

        /// <summary>
        /// Gets or sets the minimum time that can be selected
        /// </summary>
        public TimeSpan MinTime
        {
            get
            {
                return (TimeSpan)this.GetValue(MinTimeProperty);
            }

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

        /// <summary>
        /// Gets or sets the selected Hour
        /// </summary>
        public int SelectedHour
        {
            get
            {
                return (int)this.GetValue(SelectedHourProperty);
            }

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

        /// <summary>
        /// Gets or sets the selected minutes
        /// </summary>
        public int SelectedMinute
        {
            get
            {
                return (int)this.GetValue(SelectedMinuteProperty);
            }

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

        /// <summary>
        /// Gets or sets the selected second
        /// </summary>
        public int SelectedSecond
        {
            get
            {
                return (int)this.GetValue(SelectedSecondProperty);
            }

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

        /// <summary>
        /// Gets or sets the selected timestamp 
        /// </summary>
        public TimeSpan SelectedTime
        {
            get
            {
                return (TimeSpan)this.GetValue(SelectedTimeProperty);
            }

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

        /// <summary>
        /// override to hook to the Control template elements
        /// </summary>
        public override void OnApplyTemplate()
        {
            // get the hours textbox and hook the events to it
            this.hours = this.GetTemplateChild("PART_Hours") as TextBox;
            this.hours.PreviewTextInput += this.HoursTextChanged;
            this.hours.KeyUp += this.HoursKeyUp;
            this.hours.GotFocus += this.TextGotFocus;
            this.hours.GotMouseCapture += this.TextGotFocus;

            // get the minutes textbox and hook the events to it
            this.minutes = this.GetTemplateChild("PART_Minutes") as TextBox;
            this.minutes.PreviewTextInput += this.MinutesTextChanged;
            this.minutes.KeyUp += this.MinutesKeyUp;
            this.minutes.GotFocus += this.TextGotFocus;
            this.minutes.GotMouseCapture += this.TextGotFocus;

            // get the seconds textbox and hook the events to it
            this.seconds = this.GetTemplateChild("PART_Seconds") as TextBox;
            this.seconds.PreviewTextInput += this.SecondsTextChanged;
            this.seconds.KeyUp += this.SecondsKeyUp;
            this.seconds.GotFocus += this.TextGotFocus;
            this.seconds.GotMouseCapture += this.TextGotFocus;

            // Get the increase button and hook to the click event
            ButtonBase increaseButton = this.GetTemplateChild("PART_IncreaseTime") as ButtonBase;
            increaseButton.Click += this.IncreaseTime;

            // Get the decrease button and hook to the click event
            ButtonBase decrementButton = this.GetTemplateChild("PART_DecrementTime") as ButtonBase;
            decrementButton.Click += this.DecrementTime;
        }

        private static void AdjustCarretIndexOrMoveToNeighbour(TextBox current, TextBox neighbour)
        {
            // if the current is near the end move to neighbour
            if (current.CaretIndex == 1 && neighbour != null)
            {
                neighbour.Focus();
            }

                // if the carrot is in the first index move the caret one index
            else if (current.CaretIndex == 0)
            {
                current.CaretIndex++;
            }
        }

        private static string AdjustText(TextBox textBox, string newText)
        {
            // replace the new text with the old text if there are already 2 char in the textbox
            if (textBox.Text.Length == 2)
            {
                if (textBox.CaretIndex == 0)
                {
                    return newText + textBox.Text[1];
                }
                else
                {
                    return textBox.Text[0] + newText;
                }
            }
            else
            {
                return textBox.CaretIndex == 0
                           ? newText + textBox.Text // if the carrot is in front the text append the new text infront
                           : textBox.Text + newText; // else put it in behind the existing text
            }
        }

        private static object ForceValidSelectedTime(DependencyObject sender, object value)
        {
            TimePicker picker = (TimePicker)sender;
            TimeSpan time = (TimeSpan)value;
            if (time < picker.MinTime)
            {
                return picker.MinTime;
            }

            if (time > picker.MaxTime)
            {
                return picker.MaxTime;
            }

            return time;
        }

        private static void SelectedTimePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            TimePicker timePicker = (TimePicker)sender;
            TimeSpan newTime = (TimeSpan)e.NewValue;
            TimeSpan oldTime = (TimeSpan)e.OldValue;

            if (!timePicker.isUpdatingTime)
            {
                timePicker.BeginUpdateSelectedTime(); // signal that the selected time is being updated

                if (timePicker.SelectedHour != newTime.Hours)
                {
                    timePicker.SelectedHour = newTime.Hours;
                }

                if (timePicker.SelectedMinute != newTime.Minutes)
                {
                    timePicker.SelectedMinute = newTime.Minutes;
                }

                if (timePicker.SelectedSecond != newTime.Seconds)
                {
                    timePicker.SelectedSecond = newTime.Seconds;
                }

                timePicker.EndUpdateSelectedTime(); // signal that the selected time has been updated
                timePicker.OnTimeSelectedChanged(timePicker.SelectedTime, oldTime);
            }
        }

        private static void SetNewTime(TimePicker timePicker)
        {
            if (!timePicker.isUpdatingTime)
            {
                TimeSpan newTime = new TimeSpan(timePicker.SelectedHour, timePicker.SelectedMinute, timePicker.SelectedSecond);

                // check if the time is the same
                if (timePicker.SelectedTime != newTime)
                {
                    timePicker.SelectedTime = newTime;
                }
            }
        }

        private static void TrimSelectedText(TextBox textBox)
        {
            if (textBox.SelectionLength > 0)
            {
                textBox.Text = textBox.Text.Remove(textBox.SelectionStart, textBox.SelectionLength);
            }
        }

        private static void TryFocusNeighbourControl(TextBox currentControl, TextBox leftControl, TextBox rightControl, Key keyPressed)
        {
            if (keyPressed == Key.Left && leftControl != null && currentControl.CaretIndex == 0)
            {
                leftControl.Focus();
            }
            else if (keyPressed == Key.Right && rightControl != null && // if the caret index is the same as the length of the text and the user clicks right key it means that he wants to go to the next textbox
                     currentControl.CaretIndex == currentControl.Text.Length)
            {
                rightControl.Focus();
            }
        }

        private void BeginUpdateSelectedTime()
        {
            this.isUpdatingTime = true;
        }

        private void DecrementTime(object sender, RoutedEventArgs e)
        {
            this.IncrementDecrementTime(false);
        }

        private void EndUpdateSelectedTime()
        {
            this.isUpdatingTime = false;
        }

        private void HoursKeyUp(object sender, KeyEventArgs e)
        {
            // focus the next control
            TryFocusNeighbourControl(this.hours, null, this.minutes, e.Key);

            if (!IncrementDecrementTime(e.Key))
            {
                this.ValidateAndSetHour(this.hours.Text);
            }
        }

        // event handler for the textboxes (hours, minutes, seconds)

        // handle the preview event so that we validate the text before it is set in the textbox's text

        // event handler for the Hour TextBox
        private void HoursTextChanged(object sender, TextCompositionEventArgs e)
        {
            // delete the text that is highlight(selected)
            TrimSelectedText(this.hours);

            // Adjust the text according to the carrot index
            string newText = AdjustText(this.hours, e.Text);

            // validates that the hour is correct if not set a valid value (0 or 24)
            int hourNum = this.ValidateAndSetHour(newText);

            // moves the carrot index or focus the neighbour
            AdjustCarretIndexOrMoveToNeighbour(this.hours, this.minutes);

            // handle the event so that it does not set the text, since we do it manually
            e.Handled = true;
        }

        private void IncreaseTime(object sender, RoutedEventArgs e)
        {
            this.IncrementDecrementTime(true);
        }

        // event handler for the Minute TextBox

        // increments/decrement the selected time accordingly to the selected control
        private bool IncrementDecrementTime(Key selectedKey)
        {
            if (selectedKey == Key.Up)
            {
                this.IncrementDecrementTime(true);
            }
            else if (selectedKey == Key.Down)
            {
                this.IncrementDecrementTime(false);
            }
            else
            {
                return false;
            }

            return true;
        }

        private void IncrementDecrementTime(bool increment)
        {
            // check if hour is selected if yes set it
            if (this.hours == this.currentlySelectedTextBox)
            {
                this.SelectedHour = MathUtil.IncrementDecrementNumber(this.hours.Text, this.HourMinValue, this.HourMaxValue, increment);
            }

                // check if minute is selected if yes set it
            else
            {
                // if (minutes == currentlySelectedTextBox)
                this.SelectedMinute = MathUtil.IncrementDecrementNumber(this.minutes.Text, this.MinuteMinValue, this.MinuteMaxValue, increment);
            }

            // if non of the above are selected assume that the seconds is selected
            // else
            // SelectedMinute = MathUtil.IncrementDecrementNumber(minutes.Text, SecondMinValue, SecondMaxValue, increment);
        }

        private void MinutesKeyUp(object sender, KeyEventArgs e)
        {
            // focus the next control
            TryFocusNeighbourControl(this.minutes, this.hours, this.seconds, e.Key);

            if (!IncrementDecrementTime(e.Key))
            {
                this.ValidateAndSetMinute(this.minutes.Text);
            }
        }

        private void MinutesTextChanged(object sender, TextCompositionEventArgs e)
        {
            // delete the text that is highlight(selected)
            TrimSelectedText(this.minutes);

            // Adjust the text according to the carrot index
            string newText = AdjustText(this.minutes, e.Text);

            // validates that the minute is correct if not set a valid value (0 or 59)
            int minNum = this.ValidateAndSetMinute(newText);

            // moves the carrot index or focus the neighbour
            AdjustCarretIndexOrMoveToNeighbour(this.minutes, this.seconds);

            // handle the event so that it does not set the text, since we do it manually
            e.Handled = true;
        }

        private void OnTimeSelectedChanged(TimeSpan newTime, TimeSpan oldTime)
        {
            TimeSelectedChangedRoutedEventArgs args = new TimeSelectedChangedRoutedEventArgs(SelectedTimeChangedEvent);
            args.NewTime = newTime;
            args.OldTime = oldTime;
            this.RaiseEvent(args);
        }

        private void SecondsKeyUp(object sender, KeyEventArgs e)
        {
            // focus the next control
            TryFocusNeighbourControl(this.seconds, this.minutes, null, e.Key);

            if (!IncrementDecrementTime(e.Key))
            {
                this.ValidateAndSetSeconds(this.seconds.Text);
            }
        }

        private void SecondsTextChanged(object sender, TextCompositionEventArgs e)
        {
            // delete the text that is highlight(selected)
            TrimSelectedText(this.seconds);

            // Adjust the text according to the carrot index
            string newText = AdjustText(this.seconds, e.Text);

            // validates that the second is correct if not set a valid value (0 or 59)
            int secNum = this.ValidateAndSetSeconds(newText);

            // moves the carrot index or focus the neighbour
            AdjustCarretIndexOrMoveToNeighbour(this.seconds, null);

            // handle the event so that it does not set the text, since we do it manually
            e.Handled = true;
        }

        private void TextGotFocus(object sender, RoutedEventArgs e)
        {
            TextBox selectedBox = (TextBox)sender;

            // set the currently selected textbox. 
            // This field is used to check which entity(hour/minute/second) to increment/decrement when user clicks the buttuns
            this.currentlySelectedTextBox = selectedBox;

            // highlight all code so that it is easier to the user to enter new info in the text box
            selectedBox.SelectAll();
        }

        // event handler for the decrease button click

        // validates the hour passed as text and sets it to the SelectedHour property
        private int ValidateAndSetHour(string text)
        {
            int hourNum = MathUtil.ValidateNumber(text, this.HourMinValue, this.HourMaxValue);
            this.SelectedHour = hourNum;
            return hourNum;
        }

        // validates the minute passed as text and sets it to the SelectedMinute property
        private int ValidateAndSetMinute(string text)
        {
            int minNum = MathUtil.ValidateNumber(text, this.MinuteMinValue, this.MinuteMaxValue);
            this.SelectedMinute = minNum;
            return minNum;
        }

        // validates the second passed as text and sets it to the SelectedSecond property
        private int ValidateAndSetSeconds(string text)
        {
            int secNum = MathUtil.ValidateNumber(text, this.SecondMinValue, this.SecondMaxValue);
            this.SelectedSecond = secNum;
            return secNum;
        }

        // focuses the left/right control accordingly to the key passed. Pass null if there is not a neighbour control
    }

    /// <summary>
    /// Delegate for the TimeSelectedChanged event
    /// </summary>
    /// <param name="sender">The object raising the event</param>
    /// <param name="e">The routed event arguments</param>
    public delegate void TimeSelectedChangedEventHandler(object sender, TimeSelectedChangedRoutedEventArgs e);

    /// <summary>
    /// Routed event arguments for the TimeSelectedChanged event
    /// </summary>
    public class TimeSelectedChangedRoutedEventArgs : RoutedEventArgs
    {
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="routedEvent">The event that is raised </param>
        public TimeSelectedChangedRoutedEventArgs(RoutedEvent routedEvent)
            : base(routedEvent)
        {
        }

        /// <summary>
        /// Gets or sets the new time
        /// </summary>
        public TimeSpan NewTime { get; set; }

        /// <summary>
        /// Gets or sets the old time
        /// </summary>
        public TimeSpan OldTime { get; set; }
    }
}