#region <<< Using Directives >>>
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;

using Volpe.Cafe.ModelSettings;
using Volpe.Cafe.Utils;
#endregion

namespace Volpe.Cafe.EspressoUI.LegacyControls.Panels
{
    /// <summary>
    /// Base class for the CAFE Model user controls.
    /// </summary>
    public class CafeUserControl : UserControl
    {
        #region <<< Nested Types >>>

        /// <summary>
        /// Provides a base class for the <see cref="TextControl"/> and <see cref="NumericTextControl"/> classes.
        /// </summary>
        public abstract class BaseTextControl
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="BaseTextControl"/> instance, using the specified parameters.
            /// </summary>
            /// <param name="value">The actual text control.</param>
            /// <param name="errorMessage">
            ///     The error message to display to the user if the value of the text control is not valid.
            /// </param>
            /// <param name="ignoreErrorIfHiddenOrDisabled">
            ///     <c>true</c>, if the error message should not be generated if the control is not visible or enabled;
            ///     <c>false</c>, otherwise. Note: once the control becomes visible or enabled, the error message will
            ///     be re-generated as appropriate.
            /// </param>
            protected BaseTextControl(Control value, string errorMessage, bool ignoreErrorIfHiddenOrDisabled)
            {
                this.Value = value;
                this.ErrorMessage = errorMessage;
                this.IgnoreErrorIfHiddenOrDisabled = ignoreErrorIfHiddenOrDisabled;
            }

            /// <summary>
            /// Gets the actual text control wrapped by this instance.
            /// </summary>
            public Control Value { get; }
            /// <summary>
            /// Gets the error message to display to the user if the value of the text control is not valid.
            /// </summary>
            public string  ErrorMessage { get; }
            /// <summary>
            /// Gets whether the error message should be generated if the control is not visible or enabled.
            /// </summary>
            /// <remarks>
            /// Once the control becomes visible or enabled, the error message will be re-generated as appropriate.
            /// </remarks>
            public bool IgnoreErrorIfHiddenOrDisabled { get; }

            /// <summary>Gets whether the value of the text control has changed from the default value.</summary>
            public abstract bool HasChanged { get; }
            /// <summary>Gets whether the current value of the text control represents a valid value.</summary>
            public abstract bool ValueValid { get; }

        }

        /// <summary>
        /// Provides automated verification and change-tracking for controls whose text property may change during
        /// runtime.
        /// </summary>
        public class TextControl : BaseTextControl
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="TextControl"/> instance, using the specified parameters.
            /// </summary>
            /// <param name="value">The actual text control.</param>
            /// <param name="defaultValue">The default value of the text control.</param>
            /// <param name="validValues">An array of valid values for the text control.</param>
            /// <param name="errorMessage">The error message to display to the user if the value of the text control is not
            ///   valid.</param>
            /// <param name="ignoreErrorIfHiddenOrDisabled">true, if the error message should not be generated if the control is
            ///   not visible or enabled; false, otherwise.  Note:  once the control becomes visible or enabled, the error
            ///   message will be re-generated as appropriate.</param>
            public TextControl(
                Control value, string defaultValue, string[] validValues, string errorMessage,
                bool ignoreErrorIfHiddenOrDisabled)
                : base(value, errorMessage, ignoreErrorIfHiddenOrDisabled)
            {
                this.DefaultValue = defaultValue;
                this.ValidValues = validValues;
            }

            /// <summary>Gets or sets the default value of the text control.</summary>
            public string DefaultValue { get; set; }
            /// <summary>Gets an array of valid values for the text control.</summary>
            public string[] ValidValues { get; }

            /// <inheritdoc/>
            public override bool HasChanged => (this.Value.Text != this.DefaultValue);
            /// <inheritdoc/>
            public override bool ValueValid
            {
                get {
                    return this.ValidValues == null
                        || this.ValidValues.Length != 0
                        || StringHelper.EqualsAny(this.Value.Text, this.ValidValues);
                }
            }

        }

        /// <summary>
        /// Provides automated verification, change-tracking, and formatting for controls whose text property represents numeric
        /// values and may change during runtime.
        /// </summary>
        public class NumericTextControl : BaseTextControl
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="NumericTextControl"/> instance, using the specified parameters.
            /// </summary>
            /// <param name="value">The actual numeric text control.</param>
            /// <param name="defaultValue">The default value of the numeric text control.</param>
            /// <param name="minValue">The minimum allowed value of the numeric text control.</param>
            /// <param name="maxValue">The maximum allowed value of the numeric text control.</param>
            /// <param name="errorMessage">The error message to display to the user if the value of the numeric text control is
            ///   not valid.</param>
            /// <param name="ignoreErrorIfHiddenOrDisabled">true, if the error message should not be generated if the control is
            ///   not visible or enabled; false, otherwise.  Note:  once the control becomes visible or enabled, the error
            ///   message will be re-generated as appropriate.</param>
            /// <param name="format">The text format to apply to the value of the numeric text control.</param>
            public NumericTextControl(
                Control value, double defaultValue, double minValue, double maxValue, string errorMessage,
                bool ignoreErrorIfHiddenOrDisabled, string format)
                : base(value, errorMessage, ignoreErrorIfHiddenOrDisabled)
            {
                this.DefaultValue = defaultValue;
                this.MinValue = minValue;
                this.MaxValue = maxValue;
                this.Format = format;
            }

            /// <summary>Gets or sets the default value of the numeric text control.</summary>
            public double DefaultValue { get; set; }
            /// <summary>Gets or sets the minimum allowed value of the numeric text control.</summary>
            public double MinValue { get; set; }
            /// <summary>Gets or sets the maximum allowed value of the numeric text control.</summary>
            public double MaxValue { get; set; }
            /// <summary>Gets or sets the text format to apply to the value of the numeric text control.</summary>
            public string Format { get; set; }

            /// <inheritdoc/>
            public override bool HasChanged => (this.Value.Text.GetDouble() != this.DefaultValue);
            /// <inheritdoc/>
            public override bool ValueValid
            {
                get {
                    if (this.Value.Text.IsNumeric()) {
                        double d = this.Value.Text.GetDouble();
                        return (double.IsNaN(this.MinValue) || d >= this.MinValue) &&
                               (double.IsNaN(this.MaxValue) || d <= this.MaxValue);
                    }
                    return false;
                }
            }

        }

        #endregion

        #region <<< Constructors >>>

        /// <summary>
        /// Initializes a new instance of the <see cref="CafeUserControl"/> class.
        /// </summary>
        protected CafeUserControl()
        {
            this.InitializeComponent();

            this._messages = new Messages();
            this._controlChanges = new List<Control>();
            this._textControls = new List<TextControl>();
            this._numericTextControls = new List<NumericTextControl>();

            this.VisualTracking = true;
            this.VisualTrackingColor = Color.Blue;
        }

        #endregion

        #region <<< Methods >>>

        #region < Component Designer generated code >

        private void InitializeComponent()
        {
            this.SuspendLayout();
            // 
            // CafeUserControl
            // 
            this.AutoScaleMode = AutoScaleMode.None;
            this.AutoScroll = true;
            this.Font = new Font("Microsoft Sans Serif", 8.25F, FontStyle.Regular, GraphicsUnit.Point, 0);
            this.Margin = new Padding(2);
            this.Name = "CafeUserControl";
            this.Size = new Size(112, 122);
            this.ResumeLayout(false);

        }

        #endregion

        /// <summary>
        /// Commits the most recently made changes to the <see cref="ModelingSettings"/> object.<br/><br/>
        /// <b>Note</b>: Overriding classes should call "base.SaveChanges()" to ensure that the changes made to
        /// registered controls are automatically reset.
        /// </summary>
        public virtual void SaveChanges()
        {
            // reset fore-color (if set by visual tracking)
            foreach (var value in this._controlChanges) {
                value.ForeColor = DefaultForeColor;
            }

            // update "default" values of text-controls
            foreach (var value in this._textControls) {
                if (value.ValueValid) {
                    value.DefaultValue = value.Value.Text;
                }
                else {
                    this.RefreshRegisteredTextControl(value);
                }
            }

            foreach (var value in this._numericTextControls) {
                if (value.ValueValid) {
                    value.DefaultValue = value.Value.Text.GetDouble();
                }
                else {
                    this.RefreshRegisteredTextControl(value);
                }
            }

            this._controlChanges.Clear();
        }

        /// <summary>
        /// Creates references between the parent and child checkboxes for the "select-all" group support.
        /// </summary>
        /// <param name="parent">The parent checkbox of the select-all group.</param>
        /// <param name="children">The child checkboxes of the select-all group.</param>
        /// <remarks>
        /// Whenever a check-state of the parent checkbox changes, the check-state of all child checkboxes will change
        /// accordingly.  Simillarly, whenever the check-state of the child checkboxes changes, the check-state of the parent
        /// checkbox will be update to reflect whether 0, 1 or more, or all of the children are checked.
        /// </remarks>
        protected void InitializeCheckBoxes(CheckBox parent, params CheckBox[] children)
        {
            // set tags for the parent
            parent.Tag = children;
            parent.CheckedChanged += new EventHandler(this.ParentCheckBox_CheckedChanged);
            // set tags for the children
            for (int i = 0; i < children.Length; i++)
            {
                children[i].Tag = parent;
                children[i].CheckedChanged += new EventHandler(this.CheckChangesDetector);
                children[i].CheckedChanged += new EventHandler(this.ChildCheckBox_CheckedChanged);
            }
        }
        /// <summary>
        /// Handles the check-changes of the parent checkboxes in the "select-all" group.
        /// </summary>
        /// <param name="sender">The control that invoked the event.</param>
        /// <param name="e">The event data associated with the event.</param>
        protected void ParentCheckBox_CheckedChanged(object sender, EventArgs e)
        {
            CheckBox   parent   = (CheckBox  )sender;
            CheckBox[] children = (CheckBox[])parent.Tag;

            for (int i = 0; i < children.Length; i++)
            {
                children[i].CheckedChanged -= new EventHandler(this.ChildCheckBox_CheckedChanged);
                if      (parent.CheckState == CheckState.Checked  ) { children[i].Checked = true ; }
                else if (parent.CheckState == CheckState.Unchecked) { children[i].Checked = false; }
                children[i].CheckedChanged += new EventHandler(this.ChildCheckBox_CheckedChanged);
            }
        }
        /// <summary>
        /// Handles the check-changes of the child checkboxes in the "select-all" group.
        /// </summary>
        /// <param name="sender">The control that invoked the event.</param>
        /// <param name="e">The event data associated with the event.</param>
        protected void ChildCheckBox_CheckedChanged(object sender, EventArgs e)
        {
            CheckBox   child    = (CheckBox  )sender;
            CheckBox   parent   = (CheckBox  )child .Tag;
            CheckBox[] children = (CheckBox[])parent.Tag;
            int count = children.Count(item => item.Checked);

            parent.CheckState = (count == children.Length) ? CheckState.Checked
                              : (count == 0) ? CheckState.Unchecked
                              : CheckState.Indeterminate;
        }

        /// <summary>
        /// Scans and auto initializes the <see cref="CheckChangesDetector"/> event handler for all <see cref="RadioButton"/>s
        /// and <see cref="CheckBox"/>es within the specified collection of <see cref="Control"/>s.
        /// </summary>
        /// <param name="controls">The child controls to scan.</param>
        protected void AutoInitializeCheckChangesDetector(ControlCollection controls)
        {
            for (int i = 0, count = controls.Count; i < count; i++)
            {
                Control c = controls[i];
                if (c is RadioButton rdb) {
                    rdb.CheckedChanged += new EventHandler(this.CheckChangesDetector);
                }
                else if (c is CheckBox chk) {
                    chk.CheckedChanged += new EventHandler(this.CheckChangesDetector);
                }

                if (c.HasChildren) {
                    this.AutoInitializeCheckChangesDetector(c.Controls);
                }
            }
        }
        /// <summary>
        /// Auto initializes the <see cref="CheckChangesDetector"/> event handler for all <see cref="CheckBox"/>es within the
        /// specified <see cref="CheckBox"/> array.
        /// </summary>
        /// <param name="controls">An array of <see cref="CheckBox"/>es to initialize.</param>
        protected void AutoInitializeCheckChangesDetector(params CheckBox[] controls)
        {
            for (int i = 0; i < controls.Length; i++)
            {
                controls[i].CheckedChanged += new EventHandler(this.CheckChangesDetector);
            }
        }
        /// <summary>
        /// Auto initializes the <see cref="CheckChangesDetector"/> event handler for all <see cref="RadioButton"/>s within the
        /// specified <see cref="RadioButton"/> array.
        /// </summary>
        /// <param name="controls">An array of <see cref="RadioButton"/>s to initialize.</param>
        protected void AutoInitializeCheckChangesDetector(params RadioButton[] controls)
        {
            for (int i = 0; i < controls.Length; i++)
            {
                controls[i].CheckedChanged += new EventHandler(this.CheckChangesDetector);
            }
        }

        /// <summary>
        /// Removes the <see cref="CheckChangesDetector"/> event handler for the specified <see cref="CheckBox"/>.
        /// </summary>
        /// <param name="control">The <see cref="CheckBox"/> to de-initialize.</param>
        protected void DeInitializeCheckChangesDetector(CheckBox control)
        {
            control.CheckedChanged -= new EventHandler(this.CheckChangesDetector);
        }
        /// <summary>
        /// Removes the <see cref="CheckChangesDetector"/> event handler for the specified <see cref="RadioButton"/>.
        /// </summary>
        /// <param name="control">The <see cref="RadioButton"/> to de-initialize.</param>
        protected void DeInitializeCheckChangesDetector(RadioButton control)
        {
            control.CheckedChanged -= new EventHandler(this.CheckChangesDetector);
        }

        /// <summary>
        /// Handles the check-changes of checkable controls, to keep track of setting changes.
        /// </summary>
        /// <param name="sender">The control that invoked the event.</param>
        /// <param name="e">The event data associated with the event.</param>
        protected void CheckChangesDetector(object sender, EventArgs e)
        {
            Control control = (Control)sender;
            if (this._controlChanges.Contains(control)) {
                this._controlChanges.Remove(control);
                if (this.VisualTracking) {
                    control.ForeColor = DefaultForeColor;
                }
            }
            else {
                this._controlChanges.Add(control);
                if (this.VisualTracking) {
                    control.ForeColor = this.VisualTrackingColor;
                }
            }
        }

        /// <summary>
        /// Determines whether the specified control is registered as a <see cref="TextControl"/> or a
        /// <see cref="NumericTextControl"/>. 
        /// </summary>
        /// <param name="value">The control to check if it is registered.</param>
        /// <returns>true, if the specified value is a registered control; false, otherwise.</returns>
        protected bool IsRegistered(Control value)
        {
            return !(this.FindTextControl(value) == null && this.FindNumericTextControl(value) == null);
        }
        /// <summary>
        /// Refreshes the UI (error message, formatting, etc.) for the specified registered control.
        /// </summary>
        /// <param name="value">The control to refresh.</param>
        /// <exception cref="InvalidOperationException">The provided control was not registered.</exception>
        protected void RefreshRegisteredTextControl(Control value)
        {
            if (this.IsRegistered(value)) {
                this.RefreshRegisteredTextControl((BaseTextControl)value.Tag);
                if (value.Tag is NumericTextControl) {
                    this.NumericTextControl_Leave(value, EventArgs.Empty);
                }
            }
            else {
                throw new InvalidOperationException("The provided control was not registered.");
            }
        }

        /// <summary>
        /// Searches the <see cref="_textControls"/> list for the specified value, to determine if it is registered as a
        /// <see cref="TextControl"/>.
        /// </summary>
        /// <param name="value">The control to search for.</param>
        /// <returns>The <see cref="TextControl"/> instance associated with the specified value, or null, if the control is not
        ///   registered as a <see cref="TextControl"/>.</returns>
        protected TextControl FindTextControl(Control value)
        {
            for (int i = 0, count = this._textControls.Count; i < count; i++)
            {
                if ((this._textControls[i]).Value == value)
                {
                    return this._textControls[i];
                }
            }
            return null;
        }
        /// <summary>
        /// Searches the <see cref="_numericTextControls"/> list for the specified value, to determine if it is registered as a
        /// <see cref="NumericTextControl"/>.
        /// </summary>
        /// <param name="value">The control to search for.</param>
        /// <returns>The <see cref="NumericTextControl"/> instance associated with the specified value, or null, if the control
        ///   is not registered as a <see cref="NumericTextControl"/>.</returns>
        protected NumericTextControl FindNumericTextControl(Control value)
        {
            for (int i = 0, count = this._numericTextControls.Count; i < count; i++)
            {
                if ((this._numericTextControls[i]).Value == value)
                {
                    return this._numericTextControls[i];
                }
            }
            return null;
        }

        /// <summary>
        /// Registers the specified control as a <see cref="TextControl"/>, using the specified parameters.
        /// </summary>
        /// <param name="value">The control to register.</param>
        /// <param name="defaultValue">The default value of the text control.</param>
        /// <param name="validValues">An array of valid values for the text control.  Use null to ignore this parameter.</param>
        /// <param name="errorMessage">The error message to display to the user if the value of the text control is not
        ///   valid.  Use null to ignore this parameter.</param>
        /// <param name="ignoreErrorIfHiddenOrDisabled">true, if the error message should not be generated if the control is
        ///   not visible or enabled; false, otherwise.  Note:  once the control becomes visible or enabled, the error
        ///   message will be re-generated as appropriate.</param>
        protected void RegisterTextControl(Control value, string defaultValue, string[] validValues, string errorMessage,
            bool ignoreErrorIfHiddenOrDisabled)
        {
            if (this.IsRegistered(value)) {
                throw new InvalidOperationException("The provided control was already registered.");
            }

            TextControl tc = new TextControl(value, defaultValue, validValues, errorMessage, ignoreErrorIfHiddenOrDisabled);
            this._textControls.Add(tc);
            value.Tag = tc;
            this.RegisterEventsForTextControls(value, false);
        }
        /// <summary>
        /// Registers the specified control as a <see cref="NumericTextControl"/>, using the specified parameters.
        /// </summary>
        /// <param name="value">The control to register.</param>
        /// <param name="defaultValue">The default value of the numeric text control.</param>
        /// <param name="minValue">The minimum allowed value of the numeric text control.</param>
        /// <param name="maxValue">The maximum allowed value of the numeric text control.</param>
        /// <param name="errorMessage">The error message to display to the user if the value of the numeric text control is
        ///   not valid.</param>
        /// <param name="ignoreErrorIfHiddenOrDisabled">true, if the error message should not be generated if the control is
        ///   not visible or enabled; false, otherwise.  Note:  once the control becomes visible or enabled, the error
        ///   message will be re-generated as appropriate.</param>
        /// <param name="format">The text format to apply to the value of the numeric text control.</param>
        protected void RegisterNumericTextControl(Control value, double defaultValue, double minValue, double maxValue,
            string errorMessage, bool ignoreErrorIfHiddenOrDisabled, string format)
        {
            if (this.IsRegistered(value)) {
                throw new InvalidOperationException("The provided control was already registered.");
            }

            NumericTextControl tc = new NumericTextControl(value, defaultValue, minValue, maxValue, errorMessage,
                ignoreErrorIfHiddenOrDisabled, format);
            this._numericTextControls.Add(tc);
            value.Tag = tc;
            this.RegisterEventsForTextControls(value, false);
        }

        private void RegisterEventsForTextControls(Control value, bool isNumeric)
        {
            value.TextChanged += this.TextControl_TextOrVisibleOrEnabledChanged;
            value.VisibleChanged += this.TextControl_TextOrVisibleOrEnabledChanged;
            value.EnabledChanged += this.TextControl_TextOrVisibleOrEnabledChanged;

            if (value is ComboBox cb) {
                cb.SelectedIndexChanged += this.TextControl_TextOrVisibleOrEnabledChanged;
            }
            else if (value is ListBox lb) {
                lb.SelectedIndexChanged += this.TextControl_TextOrVisibleOrEnabledChanged;
            }

            if (isNumeric) {
                value.Leave += this.NumericTextControl_Leave;
                value.KeyPress += this.NumericTextControl_KeyPress;
            }
        }

        private void TextControl_TextOrVisibleOrEnabledChanged(object sender, EventArgs e)
        {
            BaseTextControl tc = (BaseTextControl)((Control)sender).Tag;
            this.RefreshRegisteredTextControl(tc);
        }

        private void RefreshRegisteredTextControl(BaseTextControl control)
        {
            this._messages.RemoveMessage(control.Value);
            if (this.VisualTracking) {
                control.Value.ForeColor = DefaultForeColor;
                control.Value.BackColor = Color.Empty;
            }
            this._controlChanges.Remove(control.Value);

            if (control.ValueValid && control.HasChanged) {
                if (this.VisualTracking) {
                    control.Value.ForeColor = this.VisualTrackingColor;
                }
                this._controlChanges.Add(control.Value);
            }
            else if (!control.ValueValid) {
                if (!control.IgnoreErrorIfHiddenOrDisabled || (control.Value.Visible && control.Value.Enabled)) {
                    string err = control.ErrorMessage;
                    if (err != null && err != string.Empty) {
                        if (control is NumericTextControl ntc) {
                            if (err.IndexOf("{MIN}") != -1) { err = err.Replace("{MIN}", ntc.MinValue.ToString()); }
                            if (err.IndexOf("{MAX}") != -1) { err = err.Replace("{MAX}", ntc.MaxValue.ToString()); }
                        }
                        this._messages.AddMessage(err, null, Color.Red, control.Value);
                    }
                    if (this.VisualTracking) {
                        control.Value.ForeColor = Color.Red;
                        control.Value.BackColor = Color.Honeydew;
                    }
                }
            }
        }

        private void NumericTextControl_Leave(object sender, EventArgs e)
        {
            object tag = ((Control)sender).Tag;
            if (tag is NumericTextControl ntc)
            {
                if (ntc.Format != null && ntc.Format != string.Empty && ntc.Value.Text.IsNumeric())
                {
                    ntc.Value.Text = ntc.Value.Text.GetDouble().ToString(ntc.Format);
                }
            }
        }
        private void NumericTextControl_KeyPress(object sender, KeyPressEventArgs e)
        {
            const char backspace = (char)8;
            const char negative  = (char)45;
            const char period    = (char)46;
            char       key       = e.KeyChar;
            e.Handled = !char.IsDigit(key) && key != backspace && key != negative && key != period;
        }

        #endregion

        #region <<< Properties >>>

        /// <summary>
        /// Gets the <see cref="Messages"/> instance that stores the messages generated by this panel.
        /// </summary>
        [Browsable(false)]
        public Messages Messages => this._messages;

        /// <summary>
        /// Gets whether any of the settings have changed since the control has been loaded or since the changes have
        /// been last saved. Only the changes made to the registered controls are considered.
        /// </summary>
        [Browsable(false)]
        public virtual bool SettingsChanged => this._controlChanges.Count != 0;

        /// <summary>
        /// Gets or sets whether settings, which have changed since the last save, will be color-coded.
        /// </summary>
        [Browsable(true)]
        [Category("Behavior")]
        [Description("Indicates whether settings, which have changed since the last save, will be color-coded.")]
        [DefaultValue(true)]
        public bool VisualTracking { get; set; }

        /// <summary>
        /// Gets or sets the color to use when color-coding settings that have changed since the last save.
        /// </summary>
        [Browsable(true)]
        [Category("Behavior")]
        [Description("Determines the color to use when color-coding settings that have changed since the last save.")]
        [DefaultValue(typeof(Color), "Blue")]
        public Color VisualTrackingColor { get; set; }

        #endregion

        #region <<< Variables >>>

        /// <summary>
        /// Stores the messages generated by this panel.
        /// </summary>
        protected Messages _messages;

        /// <summary>
        /// Contains all of the registered controls (check-boxes, radio-boxes, text-boxes, etc.) that have changed
        /// their state or value since the last save.
        /// </summary>
        protected List<Control> _controlChanges;
        /// <summary>
        /// Contains a list of all registered <see cref="TextControl"/> objects.
        /// </summary>
        protected List<TextControl> _textControls;
        /// <summary>
        /// Contains a list of all registered <see cref="NumericTextControl"/> objects.
        /// </summary>
        protected List<NumericTextControl> _numericTextControls;

        #endregion
    }
}
