// RibbonKeyTipManager.cs
// Copyright (c) 2016 Bentley Systems, Incorporated. All Rights Reserved.

using Haestad.Support.Library;
using Haestad.Support.Support;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Telerik.Windows.Controls;
using Telerik.Windows.Controls.RibbonView;

namespace Haestad.Framework.UI.Integration
{
    /// <summary>
    /// A class that manages registration and application of key tips for the Ribbon.
    /// Automatically generates unique key tips in different contexts, based on element localized labels.
    /// </summary>
    internal class RibbonKeyTipManager
    {
        #region Constructor
        internal RibbonKeyTipManager()
        {
        }
        #endregion

        #region Internal Methods
        internal void UpdateQuickAccessToolbarKeyTips(ItemCollection toolBarItems)
        {
            ClearQuickAccessToolBarKeyTips(toolBarItems);

            const string FIRST_KEY_TIP = "1"; // I18N. OK.
            string keyTipCharacter = String.Empty;

            for (int i = 0; i < toolBarItems.Count; i++)
            {
                FrameworkElement toolBarItem = toolBarItems[i] as FrameworkElement;
                if (TelerikLibrary.GetIsVisibleControl(toolBarItem))
                {
                    if (i == 0)
                        keyTipCharacter = FIRST_KEY_TIP;
                    else
                        keyTipCharacter = GenerateNextQuickAccessToolbarKeyTip(keyTipCharacter);

                    if (keyTipCharacter != String.Empty)
                        RegisterAndSetTopLevelKeyTip(keyTipCharacter, toolBarItem);
                }
            }
        }

        internal void RegisterFileTab(FrameworkElement backstage, string localizedLabel)
        {
            string cleanLabel = StringLibrary.CleanTextResource(localizedLabel);
            string keyTipCharacter = GenerateUniqueKeyTip(cleanLabel, TopLevelKeyTipRegister, true);
            RegisterAndSetTopLevelKeyTip(keyTipCharacter, backstage);
        }

        internal void RegisterRibbonTab(RadRibbonTab ribbonTab, string localizedLabel)
        {
            string cleanLabel = StringLibrary.CleanTextResource(localizedLabel);
            string keyTipCharacter = GenerateUniqueKeyTip(cleanLabel, TopLevelKeyTipRegister, true);
            RegisterAndSetTopLevelKeyTip(keyTipCharacter, ribbonTab);
        }

        internal void RegisterBackstageItem(RadRibbonBackstageItem backstageItem, string localizedLabel)
        {
            string cleanLabel = StringLibrary.CleanTextResource(localizedLabel);
            BackstageControlsList.Add(new ControlAndLabelPair(backstageItem, cleanLabel));
        }

        internal void RegisterTabLevelControl(Control control, string localizedLabel, ButtonSize buttonSize)
        {
            string cleanLabel = StringLibrary.CleanTextResource(localizedLabel);
            if (buttonSize == ButtonSize.Large)
                SingleCharacterPreferentialList.Add(new ControlAndLabelPair(control, cleanLabel));
            else
            {
                if (RibbonExternalConfiguration.Instance.RibbonKeyTipsGrouped)
                    RegisterGroupBasedTabLevelControl(control, cleanLabel);
                else
                    RegisterTabBasedTabLevelControl(control, cleanLabel); 
            }
        }

        internal void AfterAllTabLevelControlsRegistered()
        {
            foreach (ControlAndLabelPair tabLevelKeyTip in SingleCharacterPreferentialList)
                RegisterTabLevelControl(tabLevelKeyTip.Control, tabLevelKeyTip.CleanLabel, true);

            if (RibbonExternalConfiguration.Instance.RibbonKeyTipsGrouped)
            {
                foreach (RibbonGroupToControlAndLabelPairs ribbonGroupToControlAndLabelPairs in OtherGroupBasedControlsList)
                {
                    foreach (ControlAndLabelPair tabLevelKeyTip in ribbonGroupToControlAndLabelPairs.ControlAndLabelPairs)
                        RegisterTabLevelControl(tabLevelKeyTip.Control, ribbonGroupToControlAndLabelPairs.RibbonGroup.Header as string, false);
                }
            }
            else
            {
                foreach (ControlAndLabelPair tabLevelKeyTip in OtherTabBasedControlsList)
                    RegisterTabLevelControl(tabLevelKeyTip.Control, tabLevelKeyTip.CleanLabel, false);

                foreach (ControlAndLabelPair tabLevelKeyTip in OtherTabBasedControlsList)
                    UpdateTabLevelControl(tabLevelKeyTip.Control);
            }

            foreach (ControlAndLabelPair backstageKeyTip in BackstageControlsList)
                RegisterBackstageControl((RadRibbonBackstageItem)backstageKeyTip.Control, backstageKeyTip.CleanLabel);

            // TODO 1: <whar> Potentially for backstage support to be complete, need a similar method to UpdateTabLevelControl to clean up lone
            // "A1" type key tips.

        }
        #endregion

        #region Private Methods

        #region Quick Access Toolbar
        /// <summary>
        /// Method generates quick access toolbar key tips based on the following sequence (derived from Office 2016).
        /// 1, 2, 3, ..., 8, 9, 09, 08, 07, ..., 01.
        /// </summary>
        private string GenerateNextQuickAccessToolbarKeyTip(string lastKeyTip)
        {
            bool intoDoubleDigits = false;
            int firstDigit = 0;
            int secondDigit = 0;

            if (lastKeyTip.Length == 1)
                secondDigit = Convert.ToInt32(lastKeyTip);
            else if (lastKeyTip.Length == 2)
            {
                firstDigit = Convert.ToInt32(lastKeyTip.Substring(0, 1));
                secondDigit = Convert.ToInt32(lastKeyTip.Substring(1, 1));
                intoDoubleDigits = true;
            }

            if (!intoDoubleDigits && secondDigit < 9)
                secondDigit++;
            else if (!intoDoubleDigits && secondDigit == 9)
                intoDoubleDigits = true;
            else if (intoDoubleDigits && secondDigit > 1)
                secondDigit--;
            else if (intoDoubleDigits && secondDigit == 1)
                return String.Empty; // Note: <whar> End of supported sequence.

            if (!intoDoubleDigits)
            {
                return secondDigit.ToString(System.Globalization.CultureInfo.InvariantCulture);
            }
            else
            {
                return firstDigit.ToString(System.Globalization.CultureInfo.InvariantCulture) +
                    secondDigit.ToString(System.Globalization.CultureInfo.InvariantCulture);
            }
        }

        private void ClearQuickAccessToolBarKeyTips(ItemCollection toolBarItems)
        {
            for (int i = 0; i < toolBarItems.Count; i++)
            {
                FrameworkElement toolBarItem = toolBarItems[i] as FrameworkElement;

                string existingKeyTip = Telerik.Windows.Controls.RibbonView.KeyTipService.GetAccessText(toolBarItem);

                if (existingKeyTip != String.Empty)
                    UnregisterAndClearTopLevelKeyTip(existingKeyTip, toolBarItem);
            }
        }
        #endregion

        #region Top Level (QAT, Tabs) Key Tips
        private void RegisterAndSetTopLevelKeyTip(string keyTipCharacter, FrameworkElement element)
        {
            TopLevelKeyTipRegister.Add(keyTipCharacter, element);
            Telerik.Windows.Controls.RibbonView.KeyTipService.SetAccessText(element, keyTipCharacter);
        }

        private void UnregisterAndClearTopLevelKeyTip(string keyTipCharacter, FrameworkElement element)
        {
            TopLevelKeyTipRegister.Remove(keyTipCharacter);
            Telerik.Windows.Controls.RibbonView.KeyTipService.SetAccessText(element, String.Empty);
        }
        #endregion

        #region Tab Level Key Tips (anything under a tab)
        /// <summary>
        /// Results in key tips more closely matched (although at time still loosely) to the actual control label.
        /// Can mean that a series of X1, X2, X3 style tips are spread over the tab.
        /// </summary>
        private void RegisterTabBasedTabLevelControl(Control control, string cleanLabel)
        {
            OtherTabBasedControlsList.Add(new ControlAndLabelPair(control, cleanLabel));
        }

        /// <summary>
        /// Results in key tips that are not matched to the labels, but more closely grouped, for easier selection.
        /// </summary>
        private void RegisterGroupBasedTabLevelControl(Control control, string cleanLabel)
        {
            RadRibbonGroup ribbonGroup = null;
            if (control is RadRibbonGroup)
                ribbonGroup = (RadRibbonGroup)control;
            else
                ribbonGroup = TelerikLibrary.GetOwnerGroup(control);

            bool found = false;
            foreach (RibbonGroupToControlAndLabelPairs ribbonGroupToControlAndLabelPairs in OtherGroupBasedControlsList)
            {
                if (ribbonGroupToControlAndLabelPairs.RibbonGroup.Equals(ribbonGroup))
                {
                    ribbonGroupToControlAndLabelPairs.ControlAndLabelPairs.Add(new ControlAndLabelPair(control, cleanLabel));
                    found = true;
                }
            }
            if (!found)
            {
                RibbonGroupToControlAndLabelPairs ribbonGroupToControlAndLabelPairs = new RibbonGroupToControlAndLabelPairs(ribbonGroup);
                ribbonGroupToControlAndLabelPairs.ControlAndLabelPairs.Add(new ControlAndLabelPair(control, cleanLabel));
                OtherGroupBasedControlsList.Add(ribbonGroupToControlAndLabelPairs);
            }
        }

        private void RegisterTabLevelControl(Control control, string cleanLabel, bool preferSingleCharacter)
        {
            RadRibbonTab ribbonTab = TelerikLibrary.GetOwnerTab(control);

            if (!TabLevelKeyTipRegister.ContainsKey(ribbonTab))
                TabLevelKeyTipRegister.Add(ribbonTab, new Dictionary<string, FrameworkElement>(DEFAULT_CONTAINER_SIZE));

            IDictionary<string, FrameworkElement> tabKeyTipTipRegister = TabLevelKeyTipRegister[ribbonTab];

            string keyTipCharacter = GenerateUniqueKeyTip(cleanLabel, tabKeyTipTipRegister, preferSingleCharacter);
            RegisterAndSetTabLevelKeyTip(ribbonTab, keyTipCharacter, control);

            if (control is RadRibbonGroup)
            {
                keyTipCharacter = GenerateUniqueKeyTip(cleanLabel, tabKeyTipTipRegister, preferSingleCharacter);
                RegisterAndSetTabGroupKeyTip(ribbonTab, keyTipCharacter, (RadRibbonGroup)control);
            }
        }

        private void RegisterAndSetTabLevelKeyTip(RadRibbonTab ribbonTab, string keyTipCharacter, FrameworkElement element)
        {
            TabLevelKeyTipRegister[ribbonTab].Add(keyTipCharacter, element);
            Telerik.Windows.Controls.RibbonView.KeyTipService.SetAccessText(element, keyTipCharacter);
        }

        private void RegisterAndSetTabGroupKeyTip(RadRibbonTab ribbonTab, string keyTipCharacter, RadRibbonGroup group)
        {
            TabLevelKeyTipRegister[ribbonTab].Add(keyTipCharacter, group);
            Telerik.Windows.Controls.RibbonView.KeyTipService.SetAltAccessText(group, keyTipCharacter);
        }

        private void UpdateTabLevelControl(Control control)
        {
            RadRibbonTab ribbonTab = TelerikLibrary.GetOwnerTab(control);
            string existingKeyTip = Telerik.Windows.Controls.RibbonView.KeyTipService.GetAccessText(control);

            if (existingKeyTip.Length == 2 && existingKeyTip.Substring(existingKeyTip.Length - 1, 1) == "1")
            {
                const int NEXT_NUMBER = 2;
                string nextKeyTip = existingKeyTip.Substring(0, 1) + NEXT_NUMBER.ToString(CultureInfo.InvariantCulture);
                IDictionary<string, FrameworkElement> tabKeyTipTipRegister = TabLevelKeyTipRegister[ribbonTab];
                if (!tabKeyTipTipRegister.ContainsKey(nextKeyTip))
                {
                    tabKeyTipTipRegister.Remove(existingKeyTip);
                    // Note: <whar> Don't need to worry about RibbonGroups here since they can never run into this situation (there will always be at least two key tips A1, A2, for example).
                    RegisterAndSetTabLevelKeyTip(ribbonTab, existingKeyTip.Substring(0, 1), control);
                }
            }
        }
        #endregion

        #region Backstage Key Tips
        private void RegisterBackstageControl(Control control, string cleanLabel)
        {
            string keyTipCharacter = GenerateUniqueKeyTip(cleanLabel, BackstageKeyTipRegister, false);
            RegisterAndSetBackstageKeyTip(keyTipCharacter, control);
        }

        private void RegisterAndSetBackstageKeyTip(string keyTipCharacter, Control control)
        {
            BackstageKeyTipRegister.Add(keyTipCharacter, control);
            Telerik.Windows.Controls.RibbonView.KeyTipService.SetAccessText(control, keyTipCharacter);
        }
        #endregion

        #region Generic Key Tip Generation
        private string GenerateUniqueKeyTip(string cleanLabel, IDictionary<string, FrameworkElement> keyTipRegister, bool preferSingleCharacter)
        {
            AddLettersFromLocalizedLabelToLetterPool(cleanLabel);

            string keyTip = GetUniqueKeyTip(cleanLabel, keyTipRegister, preferSingleCharacter);
            if (keyTip != String.Empty)
                return keyTip;

            TraceLibrary.WriteLine(HmTraceLevel.Release, "RibbonKeyTipManager::GenerateUniqueTopLevelKeyTip(). Unable to find unique letter based key tip for: "
                + cleanLabel + ". Trying used letter pool."); // I18N. OK.

            string usedLetterPoolString = GetUsedLetterPoolString();
            keyTip = GetUniqueKeyTip(usedLetterPoolString, keyTipRegister, preferSingleCharacter);
            if (keyTip != String.Empty)
                return keyTip;

            TraceLibrary.WriteLine(HmTraceLevel.Release, "RibbonKeyTipManager::GenerateUniqueTopLevelKeyTip(). Unable to find unique letter based key tip for: " + cleanLabel +
                ". All out of ideas so giving up."); // I18N. OK.

            return String.Empty;
        }

        private string GetUniqueKeyTip(string cleanLabel, IDictionary<string, FrameworkElement> keyTipRegister, bool preferSingleCharacter)
        {
            string keyTip = String.Empty;
            if (preferSingleCharacter)
            {
                keyTip = GenerateSingleLetterControlAndLabelPair(cleanLabel, keyTipRegister);
                if (keyTip != String.Empty)
                    return keyTip;
            }

            keyTip = GenerateDoubleCharacterControlAndLabelPair(cleanLabel, keyTipRegister);
            if (keyTip != String.Empty)
                return keyTip;

            return String.Empty;
        }

        private string GenerateSingleLetterControlAndLabelPair(string cleanLabel, IDictionary<string, FrameworkElement> keyTipRegister)
        {
            string reorderedLabel = ReorderLabelForKeyTipCharacterPreference(cleanLabel);

            for (int i = 0; i < reorderedLabel.Length; ++i)
            {
                string keyTip = reorderedLabel.Substring(i, 1).ToUpper();

                if (!Char.IsLetter(keyTip, 0))
                    continue;

                if (!keyTipRegister.ContainsKey(keyTip))
                {
                    const int FIRST_TIP_NUMBER = 1;
                    string twoLetterkeyTip = keyTip + FIRST_TIP_NUMBER.ToString(CultureInfo.InvariantCulture);

                    if (!keyTipRegister.ContainsKey(twoLetterkeyTip))
                        return keyTip;
                }
            }

            return String.Empty;
        }

        private string GenerateDoubleCharacterControlAndLabelPair(string cleanLabel, IDictionary<string, FrameworkElement> keyTipRegister)
        {
            string reorderedLabel = ReorderLabelForKeyTipCharacterPreference(cleanLabel);

            for (int i = 0; i < reorderedLabel.Length; ++i)
            {
                string candidateKeyTipFirstLetter = reorderedLabel.Substring(i, 1).ToUpper();

                if (keyTipRegister.ContainsKey(candidateKeyTipFirstLetter)) // Note: <whar> If, for example "A" is used already, then "A1" is not valid (clashes).
                    continue;

                const int FIRST_TIP_NUMBER = 1;
                const int LAST_TIP_NUMBER = 9;
                for (int j = FIRST_TIP_NUMBER; j <= LAST_TIP_NUMBER; ++j)
                {
                    string keyTip = candidateKeyTipFirstLetter + j.ToString(CultureInfo.InvariantCulture);

                    if (!Char.IsLetter(keyTip, 0))
                        continue;

                    if (!keyTipRegister.ContainsKey(keyTip))
                        return keyTip;
                }
            }

            return String.Empty;
        }

        private string ReorderLabelForKeyTipCharacterPreference(string cleanLabel)
        {
            Regex regex = new Regex(@"\s"); // I18N. OK. (whitespace)
            string[] words = regex.Split(cleanLabel);

            int maximumLength = 0;
            for (int i = 0; i < words.Length; ++i)
                maximumLength = Math.Max(maximumLength, words[i].Length);

            string reordered = String.Empty;

            for (int i = 0; i < maximumLength; ++i)
            {
                for (int j = 0; j < words.Length; ++j)
                {
                    if (i < words[j].Length)
                        reordered += words[j].Substring(i, 1);
                }
            }

            return reordered;
        }

        /// <summary>
        /// Builds a string from all the unique letters found in the passed in control labels, which should be good enough to find at least one that we can use to generate a unique key tip.
        /// Makes no assumption about locale since it gets the ACTUAL letters/symbols from the localized strings.
        /// Have taken this approach as a last result to finding a unique character to generate a key tip from, since getting an alphabet is not as trivial as it might sound (no support 
        /// in .Net actaully and when you think about it, it makes you ask questions that you probably never thought about, like does a Chinese keyboard have 2000 keys?).
        /// http://stackoverflow.com/questions/314466/generating-an-array-of-letters-in-the-alphabet-in-c-sharp
        /// </summary>
        /// <returns></returns>
        private string GetUsedLetterPoolString()
        {
            string usedLetters = String.Empty;
            IEnumerator<KeyValuePair<string, object>> enumerator = UsedLetterPool.GetEnumerator();
            while (enumerator.MoveNext())
                usedLetters += enumerator.Current.Key;
            return usedLetters;
        }

        private void AddLettersFromLocalizedLabelToLetterPool(string localizedLabel)
        {
            for (int i = 0; i < localizedLabel.Length; ++i)
            {
                string letter = localizedLabel.Substring(i, 1).ToUpper();
                if (!UsedLetterPool.ContainsKey(letter))
                    UsedLetterPool.Add(letter, null);
            }
        }
        #endregion
        #endregion

        #region Private Properties
        /// <summary>
        /// A register of top level access text objects; currently QAT and Ribbon Tabs (both of these can be displayed at the same time
        /// and therefore must not clash with each other).
        /// </summary>
        private IDictionary<string, FrameworkElement> TopLevelKeyTipRegister
        {
            get
            {
                if (m_topLevelKeyTipRegister == null)
                    m_topLevelKeyTipRegister = new Dictionary<string, FrameworkElement>(DEFAULT_CONTAINER_SIZE);
                return m_topLevelKeyTipRegister;
            }
        }

        /// <summary>
        /// A register of backstage access text objects.
        /// </summary>
        private IDictionary<string, FrameworkElement> BackstageKeyTipRegister
        {
            get
            {
                if (m_backstageKeyTipRegister == null)
                    m_backstageKeyTipRegister = new Dictionary<string, FrameworkElement>(DEFAULT_CONTAINER_SIZE);
                return m_backstageKeyTipRegister;
            }
        }

        /// <summary>
        /// A register of tab level access text objects; anything added under a tab.
        /// </summary>
        private IDictionary<RadRibbonTab, IDictionary<string, FrameworkElement>> TabLevelKeyTipRegister
        {
            get
            {
                if (m_tabLevelKeyTipRegister == null)
                    m_tabLevelKeyTipRegister = new Dictionary<RadRibbonTab, IDictionary<string, FrameworkElement>>(DEFAULT_CONTAINER_SIZE);
                return m_tabLevelKeyTipRegister;
            }
        }

        private IList<ControlAndLabelPair> SingleCharacterPreferentialList
        {
            get
            {
                if (m_singleCharacterPreferentialList == null)
                    m_singleCharacterPreferentialList = new List<ControlAndLabelPair>(DEFAULT_CONTAINER_SIZE);
                return m_singleCharacterPreferentialList;
            }
        }

        private IList<ControlAndLabelPair> OtherTabBasedControlsList
        {
            get
            {
                if (m_otherTabBasedControlsList == null)
                    m_otherTabBasedControlsList = new List<ControlAndLabelPair>(DEFAULT_CONTAINER_SIZE);
                return m_otherTabBasedControlsList;
            }
        }

        private IList<RibbonGroupToControlAndLabelPairs> OtherGroupBasedControlsList
        {
            get
            {
                if (m_otherGroupBasedControlsList == null)
                    m_otherGroupBasedControlsList = new List<RibbonGroupToControlAndLabelPairs>(DEFAULT_CONTAINER_SIZE);
                return m_otherGroupBasedControlsList;
            }
        }

        private IList<ControlAndLabelPair> BackstageControlsList
        {
            get
            {
                if (m_backstageControlsList == null)
                    m_backstageControlsList = new List<ControlAndLabelPair>(DEFAULT_CONTAINER_SIZE);
                return m_backstageControlsList;
            }
        }

        private IDictionary<string, object> UsedLetterPool
        {
            get
            {
                if (m_usedLetterPool == null)
                    m_usedLetterPool = new SortedDictionary<string, object>();
                return m_usedLetterPool;
            }
        }
        #endregion

        #region Private Fields
        private IDictionary<string, FrameworkElement> m_topLevelKeyTipRegister;
        private IDictionary<string, FrameworkElement> m_backstageKeyTipRegister;
        private IDictionary<RadRibbonTab, IDictionary<string, FrameworkElement>> m_tabLevelKeyTipRegister;
        private IList<ControlAndLabelPair> m_singleCharacterPreferentialList;
        private IList<ControlAndLabelPair> m_otherTabBasedControlsList;
        private IList<RibbonGroupToControlAndLabelPairs> m_otherGroupBasedControlsList;
        private IList<ControlAndLabelPair> m_backstageControlsList;
        private IDictionary<string, object> m_usedLetterPool;
        #endregion

        #region Private Constants
        private const int DEFAULT_CONTAINER_SIZE = 32;
        #endregion
    }
}
