-- Author: Arkaan
-- Addon Name: "Guild Roster Manager"

-- Table to hold all functions
GRM = {};

-- Global tables saved account wide.
-- Just load the settings the first time addon is loaded.
GRM_AddonSettings_Save = {};             -- Configuration saved here for all alts. Each toon gets their own configuration table as they might be in different guilds, thus player may want to configure different.
GRM_LogReport_Save = {};                 -- This will be the stored Log of events and changes.
GRM_GuildMemberHistory_Save = {}         -- Detailed information on each guild member
GRM_PlayersThatLeftHistory_Save = {};    -- Data storage of all players that left the guild, so metadata is stored if they return. Useful for "rejoin" tracking, and to see if players were banned.
GRM_CalendarAddQue_Save = {};            -- Since the add to calendar is protected, and requires a player input, this will be qued here between sessions. { name , eventTitle , eventMonth , eventDay , eventYear , eventDescription } 
GRM_PlayerListOfAlts_Save = {};          -- This is used so the player has a working alt list to reference, so they can add themselves to an alt list.
GRM_DebugLog_Save = {};                  -- Character specific debug log for addon dev use submission.
GRM_Misc = {};                           -- This serves as a backup placeholder to hold important values if a player logs off in the middle of something, it can carry on where it left off by storing a marker.
-- Backups...
GRM_GuildDataBackup_Save = {};

-- slash commands
SLASH_GRM1 = '/roster';
SLASH_GRM2 = '/grm';

-- Global Tables to hold localization dictionary
GRM_L = {};
-- Localaztion array for all language initialization functions.
GRML = {};
-- Useful Variables ( kept in table to keep low upvalues count )
GRM_G = {}; 

-- Addon Details:
GRM_G.Version = "R1.9294";
GRM_G.PatchDay = 1621844254;             -- In Epoch Time
GRM_G.PatchDayString = "1621844254";     -- 2 Versions saves on conversion computational costs... just keep one stored in memory. Extremely minor gains, but very useful if syncing thousands of pieces of data in large guilds.
GRM_G.Patch = "9.0.5";
GRM_G.LvlCap = GetMaxPlayerLevel();
GRM_G.BuildVersion = select ( 4 , GetBuildInfo() ); -- Technically the build level or the patch version as an integer.

-- Initialization Useful Globals 
-- ADDON
GRM_G.addonName = "Guild_Roster_Manager";
-- Player Details
GRM_G.guildName = "";
GRM_G.realmName = string.gsub ( string.gsub ( GetRealmName() , "-" , "" ) , "%s+" , "" );       -- Remove the space since server return calls don't include space on multi-name servers, also removes a hyphen if server is hyphened.Necessary for backend addon to addon comms formatting sender.
GRM_G.addonUser = ( GetUnitName ( "PLAYER" , false ) .. "-" .. GRM_G.realmName );         -- Oddly, GetUnitName set as true will not reliably return realm name on non-merged realms without this formatting.
GRM_G.faction = UnitFactionGroup ( "PLAYER" );
GRM_G.FID = 0;                  -- index for Horde = 1; Ally = 2
GRM_G.F = "";                   -- F for Faction ID  - "H" or "A";
GRM_G.setPID = 0;               -- Since settings are player unique, PID = Player ID -- Only useful so the addon can update settings for the OLD DB...
GRM_G.playerRankID = 0;         -- Player personal rank ID based on rank in the guild. The lowest, 0 , is the Guild Leader. This is only used for sync purposes and is configured on sync configuration.
GRM_G.clubID = 0;               -- The currently selected clubID (for community frame added in 8.0)
GRM_G.gClubID = 0;              -- The immutable guild clubID

-- To ensure frame initialization occurse just once... what a waste in resources otherwise.
GRM_G.timeDelayValue = 0;
GRM_G.timeDelayValue2 = 0;
GRM_G.FramesInitialized = false;
GRM_G.CommunityInitialized = false;
GRM_G.ClassicRosterInitialized = false;
GRM_G.BlizzFramePinsInitialized = false;
GRM_G.OnFirstLoad = true;
GRM_G.currentlyTracking = false;
GRM_G.trackingTriggered = false;
GRM_G.InitializePreCheck = false;
GRM_G.ClassicTaintWarning = false;
GRM_G.ClassicSyncControl = 0;
GRM_G.IntegrityTackingEnabled = false;
GRM_G.oldDB = false;                    -- Pre 1.83 formatting of DB
GRM_G.tagFormatChangeNotice = false;
GRM_G.GRMfunctionDisabled = false;

-- In Combat controls and UI frame handlers for such...
GRM_G.inCombat = false;
GRM_G.minmapButtonDelay = false;
GRM_G.MacroDelay = false;

-- Guild Status holder for checkover.
GRM_G.guildStatusChecked = false;

-- UI Controls global for reset
GRM_G.UIIsLoaded = false;

-- Tempt Logs For FinalReport()
GRM_G.TempNewMember = {};
GRM_G.TempLogPromotion = {};
GRM_G.TempInactiveReturnedLog = {};
GRM_G.TempEventRecommendKickReport = {};
GRM_G.TempEventRecommendPromotionReport = {};
GRM_G.TempEventRecommendDemotionReport = {};
GRM_G.TempLogDemotion = {};
GRM_G.TempLogLeveled = {};
GRM_G.TempLogNote = {};
GRM_G.TempLogONote = {};
GRM_G.TempRankRename = {};
GRM_G.TempRejoin = {};
GRM_G.TempBannedRejoin = {};
GRM_G.TempLeftGuild = {};
GRM_G.TempLeftGuildPlaceholder = {};
GRM_G.TempNameChanged = {};
GRM_G.TempEventReport = {};

-- Useful Globals for Quick Use
GRM_G.rankIndex = 1;
GRM_G.monthIndex = 1;
GRM_G.yearIndex = 1;
GRM_G.dayIndex = 1;
GRM_G.GuildLogDate = {};

-- Alt Helpers
GRM_G.selectedAlt = {};
GRM_G.selectedAltList = {};
GRM_G.currentHighlightIndex = 0;
GRM_G.IsNewToon = false;
GRM_G.MaxAltAutoCompleteList = 30;      -- Max numer allowed for scrolling in the AddAlt auto-complete window.
GRM_G.NumberAltInSelection = 0;
GRM_G.AltAddInProgress = false;
GRM_G.AltRmvInProgress = false;

-- Guildie info
GRM_G.listOfGuildies = {};
GRM_G.autoCompleteBanList = {};
GRM_G.numAccounts = 0;
GRM_G.guildCreationDate = "";
GRM_G.DesignateMain = false;
GRM_G.numRanksHasChanged = false;

-- [PROBABLY NEED TO BE UPDATED, CLEANED, OPTIMIZED, and PINNED AS FRAME CHILD VARIABLE, BUT GOOD FOR NOW]
-- MISC Globals for resource handling... generally to avoid wasteful checks based on timers, position, pause controls.
-- Some of this is just to prevent messy carryover by keeping 1 less argument to a method, by just keeping a global. 
-- Some are for frame/UI control, like "pause" to stop mouseover updates if you are adjusting an input or editing a date or something similar.
-- TIMERS FOR ONUPDATE CONTROL TO AVOID SPAMMY CHECKS
GRM_G.timer = 0;
GRM_G.timer2 = 0; 
GRM_G.timer3 = 0;
GRM_G.timer5 = 0;
GRM_G.ButtonRosterTimer = 0;             -- For the mouseover buttons on the CommunitiesFrame, OnEnter
GRM_G.ButtonRosterTimer2 = 0             -- For use with the older roster
GRM_G.CurrentPinCommunity = true;        -- Pinned to community or old roster
GRM_G.SyncJDTimer = 0;                   -- Use to hide window frame if all alts with dates are removed.
GRM_G.eventTimer = 0;                    -- Use for OnUpdate Limiter for Event Tab on main window.
GRM_G.banListTimer = 60;                 -- OnUpdate timer for the Ban List mention if players in the guild are currently banned. -- 60 to ensure first one is instant
GRM_G.eventTimerTooltip = 0;             -- For the OnUpdate Limiter of the informative tooltip in roster window.
GRM_G.usersTimerTooltip = 0              -- For the OnUpdate Limiter on th AddonUsers window... 
GRM_G.ScanRosterTimer = 0;               -- keep track of how long since last scan.
GRM_G.backupTimer = 0;                   -- For updating the backup frames for tooltip logic.
GRM_G.auditTimer = 0;                    -- For the tooltip on the auditframe
GRM_G.logTimer = 0;                      -- to prevent the filtering when you type from searching too fast... lest it will crash
GRM_G.refreshAddonUserDelay = 0;         -- For rechecking the users online with addon installed
GRM_G.SystemMsgThrottle = 0;             -- To prevent repeat scanning of the system message parsing needlessly.
GRM_G.CommunitiesUpdateTimer = 0;        -- Helps control Zone timer on mouseover window easier.

-- MISC argument resource saving globals.
GRM_G.CurrentlyScanning = false;
GRM_G.playersStillOnServer = {};     -- keeps track of the players that have server transferred off.
GRM_G.LeftBanPlayersStillOnServer = {};     -- Keeping track of just the ban players who are no longer on the server (or possibly deleted toons too)
GRM_G.CharCount = 0;
GRM_G.DelayedAtLeastOnce = false;
GRM_G.CalendarAddDelay = 0; -- Needs to be at least 5 seconds due to server restriction on adding to calendar no more than once per 5 sec. First time can be zero.
GRM_G.timerUIChange = 0;
GRM_G.ShowOfflineChecked = false;
GRM_G.pause = false;                        -- Pause mouseover roster window from updating and changing on mouseover.
GRM_G.rankDateSet = false;
GRM_G.editPromoDate = false;
GRM_G.editJoinDate = false;
GRM_G.editFocusPlayer = false;
GRM_G.editStatusNotify = false
GRM_G.editOnlineStatus = false;
GRM_G.editBirthday = false;
GRM_G.KS = "Player";
GRM_G.guildFinderReported = false;
GRM_G.changeHappenedExitScan = false;
GRM_G.currentName = "";
GRM_G.RecursiveStop = false;
GRM_G.isChecked = false;
GRM_G.isChecked2 = false;
GRM_G.ClickCount = 0;
GRM_G.HasAccessToGuildChat = false;
GRM_G.HasAccessToOfficerChat = false;
GRM_G.tempAltName = "";
GRM_G.firstTimeWarning = true;
GRM_G.tempAddBanClass = "";
GRM_G.ChangesFoundOnLoad = false;
GRM_G.MsgFilterEnabled = false;
GRM_G.MsgFilterDelay = false;
GRM_G.MsgFilterDelay2 = false;
GRM_G.TooManyFriendsWarning = false;
GRM_G.IsOnLogonDelay = time();
GRM_G.TempListNamesAdded = {};
GRM_G.TempListNamesAddedGUIDCheck = {};
GRM_G.OriginalEditBoxValue = "";             -- To hold in case player loses focus of editbox without changing anything.
GRM_G.previousNote = "-%";                   -- Gibberish not for comparison against on first load.
GRM_G.DropDownHighlightLockIndex = 1;
GRM_G.InitiatingBanEdit = false;
GRM_G.AltSideWindowFreeze = false;
GRM_G.AuditSortType = 1;
GRM_G.formatNumber = 1;
GRM_G.MainNameSystemMsgControl = false;     -- Control to prevent it checking the system message
GRM_G.NumberOfHoursTilRecommend = {}        -- number of hours til recommended to kick. This will process once settings are loaded.
GRM_G.SearchFocusControl = false;           -- For auto focusing the search box on the log
GRM_G.EscapeControl = false;
GRM_G.MaxNoteChars = 31;                    -- For keeping track of useful default value that can be upped if Blizz ever updates the interface.
GRM_G.kickSafetyRedundancyCheck = 0;                  -- prevent double reporting.

-- Scanning
GRM_G.ScanIntegrityCheckInitialized = false;
GRM_G.TimeAtCompletion = 0;
GRM_G.SafeListExpirationChecking = false;

-- Export controls
GRM_G.ExportCap = 500;                      -- On exporting guild player details and exporting the guild log, max lines.
GRM_G.CounterCap = 0;                       -- Basically either the size of the log, or the size of the number of guildies, or size of number of left guildies.

-- Live Detection Controls
GRM_G.KickControlCheck = 0;
GRM_G.RejoinControlCheck = 0;

-- Leadership Global Settings Controls
GRM_G.IsRadialChecked = false;

-- ALt Details Frame Controls
GRM_G.altDetailsControl = { 2 , true };
GRM_G.timer6 = 0;                        -- For the alt grouping side window
GRM_G.timer7 = 0;                        -- Alt grouping control timer 2

-- Ban Details Frame Controls
GRM_G.banDetailsControl = { 4 , true };
GRM_G.TimerBanUpdate = 0;
GRM_G.sortRestrictionTimer = 0;

-- UI Sorting restriction controls
GRM_G.sortRestrictionTimer = 0;

-- System message check and controls.
GRM_G.SystemMessageTest = true;
GRM_G.SystemMessageTestAnnounced = false;
GRM_G.DelayCount = 0
GRM_G.TempBanSystemMessage = false;

-- GuildLeader Controls
GRM_G.GuildInfo = "";
GRM_G.GlobalControl1 = false;               -- General sync
GRM_G.GlobalControl2 = false;               -- Ban sync
GRM_G.GlobalControl3 = false;               -- Custom note sync
GRM_G.GlobalControl4 = false;               -- Join date note location set
GRM_G.GlobalControl4_5 = false;             -- Join date headers
GRM_G.GlobalControl5 = false;               -- Timestamp Format
GRM_G.GlobalControl7 = false;               -- !note trigger for public note indicating activation of global setting

-- Calendar Globals
GRM_G.CalendarRegistered = false;
GRM_G.currentCalendarOffset = 1;
GRM_G.IsAltGrouping = false;
GRM_G.CurrentCalendarName = "";
GRM_G.CurrentCalendarHexCode = "";

-- Tooltip holdover.
GRM_G.toolTipScale = 1.0;

-- Backup Controls
GRM_G.selectedFID = "";
GRM_G.BackupEntries = {};
GRM_G.BackupLoadedOnce = false;
GRM_G.BackupFrameSelectDetails = {};

-- Throttle controls
GRM_G.newPlayers = {};
GRM_G.leavingPlayers = {};
GRM_G.CheckingGUIDThroughFriendsList = false;
GRM_G.AutoCompleteThrottle = 0;                 -- important so it doesn't scan through entire database onTextTyping - on use only, mostly ok

-- ColorPicker Controls
GRM_G.MainTagColor = false;
GRM_G.GroupInfoIconColor = false;
GRM_G.MainTagHexCode = "";
GRM_G.CurrentTagColorBox = 0;

-- Current Addon users
GRM_G.currentAddonUsers = {};
GRM_G.ReportedNoOfficerOnly = false;

-- Log Options Controls
GRM_G.LogNumbersColorUpdate = false;
GRM_G.FirstTimeViewed = true;
GRM_G.OldLogHeaderIsOn = false;
GRM_G.IndexOfLastLogEntry = 0;
GRM_G.IndexOfLastTable = "";
GRM_G.fullLogMatch = {};
GRM_G.CurrentTotalCount = 0;

-- Dropdown logic helpers and Roster UI Logic
GRM_G.RosterButtons = {};

-- Version Control
GRM_G.VersionChecked = false;
GRM_G.VersionCheckRegistered = false;
GRM_G.VersionCheckedNames = {};
GRM_G.NeedsToAddSelfToList = false;
GRM_G.ActiveStatusQue = {};

-- For Temporary Slash Command Actions
GRM_G.TemporarySync = false;
GRM_G.ManualScanEnabled = false;
GRM_G.slashCommandSyncTimer = 0;

-- Banning players
GRM_G.TempBanTarget = {};
GRM_G.numberInGuildBans = 0;
GRM_G.CurrentBanSelectedName = {};
GRM_G.KickAllAltsTable = {};
GRM_G.KickAltControl = false;
GRM_G.kickBannedControl = false;
GRM_G.KickAllBannedTable = {};

-- FOR LOCALIZATION
GRM_G.Region = GetLocale();
GRM_G.Localized = false;
GRM_G.LocalizedIndex = 1;
GRM_G.FontChoice = "";
GRM_G.FontModifier = 0;
GRM_G.Status = {};

-- Debugging
GRM_G.DebugLog = {};
GRM_G.DebugEnabled = false;
GRM_G.DebugMsgEnabled = false;

-- AUDIT TOOLS
GRM_G.AuditToolGuildies = {};
GRM_G.JDAuditToolLastSelection = "";
GRM_G.customHeaderJoin = "";
GRM_G.customHeaderRejoin = "";
GRM_G.AuditEntries = {};
GRM_G.AuditEntryTotals = {};
GRM_G.buttonTimer1 = 0;                  -- unknown button join dates audit
GRM_G.buttonTimer2 = 0;                  -- unknown button promo dates
GRM_G.buttonTimer3 = 0;                  -- Unknown button bdays
GRM_G.AuditWindowRefresh = false;           -- Macro tool check

-- GameToolTip Helper
GRM_G.ToolTipTextLeft = {};
GRM_G.ToolTipTextRight = {};

-- Sync
GRM_G.SyncOutdatedReport = false;       -- If player syncs with you is outdated it does one report only ever

-- Addon to Addon Comms, pattern matching for text parsing the comm messages - to be built only on use.
GRM_G.CheckJoinDatePattern = nil;
GRM_G.CheckPromoDatePattern = nil;
GRM_G.CheckAddToCalendarPattern = nil;
GRM_G.CheckAddAltPattern = nil;
GRM_G.CheckRemoveAltPattern = nil;
GRM_G.CheckAltMainPattern = nil;
GRM_G.CheckMainSyncPattern = nil;
GRM_G.CheckAltMainToAltPattern = nil;
GRM_G.CheckCustomNotePattern = nil;
GRM_G.CheckCustomNoteSyncPattern = nil;
GRM_G.CheckBanListPattern = nil;
GRM_G.CheckBanManagementPattern = nil;
GRM_G.UpdateCurrentPlayerInfoPattern = nil;
GRM_G.UpdateLeftPlayerInfoPattern = nil;
GRM_G.UpdateCurrentPlayerBanReasonPattern = nil;
GRM_G.CreationDatePattern = nil;
GRM_G.GlobalControlPermissionPattern = nil;

-- GRM Management Promo/Demote/kick too
GRM_G.HK = false;
GRM_G.AuditWindowRefresh = false;
GRM_G.MacroHotKey = "CTRL-SHIFT-K";
GRM_G.ruleListQueuedForCount = {};

-- Configuration
GRM_G.AddonIsFullyConfigured = false;
GRM_G.SettingsPages = 15;

-- MouseOver
GRM_G.RosterSelection = 0;

-- Which frame to send AddMessage
GRM_G.Chat = {};
GRM_G.UnconfirmedChatTabs = {};
GRM_G.MainHookConfigured = false;

-- Unique Classic frame loads
GRM_G.rankShiftLoaded = false;

-- GRM Modules.
GRM_G.Module = {};

-- Useful Lookup Tables for date indexing.
local monthEnum = { Jan = 1 , Feb = 2 , Mar = 3 , Apr = 4 , May = 5 , Jun = 6 , Jul = 7 , Aug = 8 , Sep = 9 , Oct = 10 , Nov = 11 , Dec = 12 };
local monthEnum2 = { ['1'] = "Jan" , ['2'] = "Feb" , ['3'] = "Mar", ['4'] = "Apr" , ['5'] = "May" , ['6'] = "Jun" , ['7'] = "Jul" , ['8'] = "Aug" , ['9'] = "Sep" , ['10'] = "Oct" , ['11'] = "Nov" , ['12'] = "Dec" };
local monthsFullnameEnum = { January = 1 , February = 2 , March = 3 , April = 4 , May = 5 , June = 6 , July = 7 , August = 8 , September = 9 , October = 10 , November = 11 , December = 12 };
local daysBeforeMonthEnum = { ['1']=0 , ['2']=31 , ['3']=59 , ['4']=90 , ['5']=120 , ['6']=151 , ['7']=181 , ['8']=212 , ['9']=243 , ['10']=273 , ['11']=304 , ['12']=334 };
local daysInMonth = { ['1']=31 , ['2']=28 , ['3']=31 , ['4']=30 , ['5']=31 , ['6']=30 , ['7']=31 , ['8']=31 , ['9']=30 , ['10']=31 , ['11']=30 , ['12']=31 };
local AllClasses = { "Deathknight" , "Demonhunter" , "Druid" , "Hunter" , "Mage" , "Monk" , "Paladin" , "Priest" , "Rogue" , "Shaman" , "Warlock" , "Warrior" };
local classFileIDEnum = { ["WARRIOR"]=1 , ["PALADIN"]=2 , ["HUNTER"]=3 , ["ROGUE"]=4 , ["PRIEST"]=5 , ["DEATHKNIGHT"]=6 , ["SHAMAN"]=7 , ["MAGE"]=8 , ["WARLOCK"]=9 , ["MONK"]=10 , ["DRUID"]=11 , ["DEMONHUNTER"]=12 };
local raceIDEnum = { ["Human"]=1 , ["Orc"]=2 , ["Dwarf"]=3 , ["NightElf"]=4 , ["Scourge"]=5 , ["Tauren"]=6 , ["Gnome"]=7 , ["Troll"]=8 , ["Goblin"]=9 , ["BloodElf"]=10 , ["Draenei"]=11 , ["Worgen"]=22 , ["Pandaren"]=24 , ["Nightborne"]=27 , ["HighmountainTauren"]=28 , ["VoidElf"]=29 , ["LightforgedDraenei"]=30 , ["ZandalariTroll"]=31 , ["KulTiran"]=32 , ["DarkIronDwarf"]=34 , ["Vulpera"]=35 , ["MagharOrc"]=36 , ["Mechagnome"]=37 };

-- Let's global some of these useful frames into a table.
local GuildRanks = {};

------------------------
------ FRAMES ----------
------------------------

-- Live Frames - Keep them separate for easier to read code... for now.
local Initialization = CreateFrame ( "Frame" );
local GeneralEventTracking = CreateFrame ( "Frame" );
local UI_Events = CreateFrame ( "Frame" );
local VersionCheck = CreateFrame ( "Frame" );
local KickAndRankChecking = CreateFrame ( "Frame" );
local AddonUsersCheck = CreateFrame ( "Frame" );
local StatusChecking = CreateFrame ( "Frame" );

--------------------------
--- FUNCTIONS ------------
--------------------------
GRM_G.GroupSyncRestrictionControl = false;
GRM_G.GroupSyncRestrictionTimerDelay = 0;
-- PLAYER STATUS MAINTENANCE FOR INCOMBAT/OUTOFCOMBAT STATUS as well as InGroup and Out of group checks
StatusChecking:RegisterEvent ( "PLAYER_REGEN_ENABLED" );
StatusChecking:RegisterEvent ( "PLAYER_REGEN_DISABLED" );
StatusChecking:RegisterEvent ( "GROUP_ROSTER_UPDATE" );
StatusChecking:SetScript ( "OnEvent" , function( _ , event )
    if event == "PLAYER_REGEN_ENABLED" then
        GRM_G.inCombat = false;

    elseif event == "PLAYER_REGEN_DISABLED" then
        GRM_G.inCombat = true;

    elseif event == "GROUP_ROSTER_UPDATE" then

        if IsInGroup() then
            if not GRM_G.GroupSyncRestrictionControl and ( time() - GRM_G.GroupSyncRestrictionTimerDelay ) > 1 then
                if #GRMsyncGlobals.DesignatedLeader > 0 then
                    GRM.RegisterGuildAddonUsersRefresh();
                end
                GRMsyncGlobals.IsElectedLeader = false;
                GRMsyncGlobals.DesignatedLeader = "";
                GRM_G.GroupSyncRestrictionControl = true;
                GRM_G.GroupSyncRestrictionTimerDelay = time();
            end

        else
            if GRM_G.GroupSyncRestrictionControl and ( time() - GRM_G.GroupSyncRestrictionTimerDelay ) > 1 then
                GRM_G.GroupSyncRestrictionControl = false;
                GRM_G.GroupSyncRestrictionTimerDelay = time();
                GRM.RegisterGuildAddonUsersRefresh();
            end
        end

    end
end);

--------------------------
--- Queries -------
--------------------------

-- Method:          GRM.GenericPlayerQuery ( array , string )
-- What it does:    Returns the int index of an array for identifying the player if they are found in the given array
-- Purpose:         Clean the code.. saves a lot of nested loops, just query here, return the index, and avoid issues, like forgetting to break a loop. 
GRM.GenericPlayerQuery = function ( sortedTable , name )
    local bottomNum = 1;
    local topNum = #sortedTable;
    local mid = 0;

    while topNum >= bottomNum do
        mid = math.floor ( ( bottomNum + topNum ) / 2 );    -- Cut it in half, and ensure it rounds down like dividing an integer

        if name < sortedTable[mid][1] then        -- It is below!
            topNum = mid - 1;

        elseif name == sortedTable[mid][1] then   -- It is a match!
            return mid;

        else                            -- It is above!
            bottomNum = mid + 1;

        end
    end

    return nil;
end

-- Method:          GRM.GetIndexOfPlayerOnList ( table , string )
-- What it Does:    Returns index of player in a list, or nil if not found.
-- Purpose:         Faster searching using binary search algorithm.
GRM.GetIndexOfPlayerOnList = function ( sortedTable , name )
    local bottomNum = 1;
    local topNum = #sortedTable;
    local mid = 0;

    while topNum >= bottomNum do
        mid = math.floor ( ( bottomNum + topNum ) / 2 );    -- Cut it in half, and ensure it rounds down like dividing an integer

        if name < sortedTable[mid].name then        -- It is below!
            topNum = mid - 1;

        elseif name == sortedTable[mid].name then   -- It is a match!
            return mid;

        else                            -- It is above!
            bottomNum = mid + 1;

        end
    end

    return nil;
end

-- Method:          GRM.GetNumKeyedEntries ( table )
-- What it Does:    Returns the count of the number of keyed entries in a table
-- Purpose:         Generic use iterator to return count since '#' only works on string length and arrays length
GRM.GetNumKeyedEntries = function ( t )
    local c = 0;

    for x in pairs ( t ) do
        if type ( x ) == "string" then
            c = c + 1;
        end
    end

    return c;
end

--------------------------
------- SETTINGS ---------
--------------------------


-- Method:          GRM.ClearPermData()
-- What it Does:    Resets all the saved data back to nothing... and does not rebuid it.
-- Purpose:         Mainly for use if ever there is a need to purge the data
GRM.ClearPermData = function()
    -- SPECIAL NOTE (if ever needed);

    -- reusable function to initialize table
    local initializeTable = function ( dataTable )
        local result = dataTable;
        result = nil;
        result = {};
        result["H"] = {};
        result["A"] = {};
        return result;
    end

    -- All Account Wide Save Tables
    local data = { GRM_GuildMemberHistory_Save , GRM_PlayersThatLeftHistory_Save , GRM_CalendarAddQue_Save , GRM_LogReport_Save , GRM_AddonSettings_Save , GRM_PlayerListOfAlts_Save , GRM_GuildDataBackup_Save };
    
    -- Go through each table and configure it.
    for i = 1 , #data do
        data[i] = initializeTable ( data[i] );
    end

    -- Player speicif save tables
    GRM_DebugLog_Save = nil;
    GRM_DebugLog_Save = {};
    GRM_Misc = nil;
    GRM_Misc = {};

    return data[1] , data[2] , data[3] , data[4] , data[5] , data[6] , data[7];
end

-- Method:          GRM.ConfigureMiscForPlayer( string );
-- What it Does:    Builds a file for tracking active data that can be reference back to on a relog... so as to mark where to carry on from
-- Purpose:         In case a player logs out in the middle of critical things, both front and backend, it has a marker stored on where to restart from.
GRM.ConfigureMiscForPlayer = function( playerFullName )
    GRM_Misc[playerFullName] = {
        {},                 -- 1) To hold the details on Added Friends that might need to be removed from recruit list (if added and player logged in that 1 second window)
        {},                 -- 2) Same as above, except now in regards to the Players who left the guild check
        {},                 -- 3) GUID rebuild check - former member, by adding to friends list you can rebuild GUID
    };
end

-- Method:          GRM.GetRankRestrictedDefaultRankIndex();
-- What it Does:    Returns the index based on the rank restricted default of a guild, which is a number based on total number of ranks in the guild.
-- Purpose:         For establishing default settings, it is important to know what the default rank is in the guild.
GRM.GetRankRestrictedDefaultRankIndex = function()
    local rankRestrictedDefault;
    if IsInGuild() then
        rankRestrictedDefault = GuildControlGetNumRanks() - 1;
    else
        rankRestrictedDefault = 2;
    end
    return rankRestrictedDefault;
end

-- Method:          GRM.GetFirstOfficerRank()
-- What it Does:    Gets the first rank that can view officer notes, thus IS an officer
-- Purpose:         So it can have a default Ban Sync Rank.
GRM.GetFirstOfficerRank = function()
    local rank = 1;

    for i = 2 , GuildControlGetNumRanks() do
        if not C_GuildInfo.GuildControlGetRankFlags( i )[12] then
            rank = i - 1;
            break;
        end
    end

    return rank - 1;        -- Because it starts at Index 0 whilst these control Ranks start at 1
end

-- Method:          GRM.SetDefaultAddonSettings( object , int , bool )
-- What it Does:    Establishes the default addon setttings for all of the options and some other misc. stored variables, like minimap position
-- Purpose:         Easy access to settings on a default reset.
GRM.SetDefaultAddonSettings = function ( player , page , isPatch )

    -- Misc needs a tab home
        player["useFullName"] = false;

    -- Page 0 = misc stuff unrelated to specific settings
    if page == 0 then
        player["version"] = GRM_G.Version;                                  -- 1
        player["showMouseoverRetail"] = true                                -- 10
        player["showMouseoverOld"] = true                                   -- 71
        player["minimapPos"] = 345;                                         -- 25
        player["minimapRad"] = 78;                                          -- 26
        player["customPos"] = false;                                        -- 63
        player["minimapCustomPos"] = { "" , "" };                           -- 64 The setpoints of the custom minimap position
        player["CoreWindowPos"] = { "" , "" , 0 , 0 };                      -- 72 Coordinates for core GRM window
        player["macroToolCoordinates"] = { "" , "" , 0 , 0 };               -- 74 Coordinates for Macrol tool Window
        player["disableMacroToolLogSpam"] = false                           -- Macro Tool checkbox on disabling chat spam while using macro tool
        
        -- No reset needed
        player["JDAuditToolFilter"] = false                                 -- 69 Only show players with JD needing updating in JD Audit Tool
        
    -- General Options Tab
    elseif page == 1 then
        player["viewOnLoad"] = true;                                        -- 2
        player["onlyViewIfChanges"] = true;                                 -- 28
        player["colorizeNames"] = true;                                     -- 9 System Messages Class Colored
        player["showMainName"] = true;                                      -- 29 Show main name in chat/whispers when player on alt
        player["syncSettings"] = true;                                      -- 31 Sync among alts in same guild
        player["minimapEnabled"] = true;                                    -- 32 Button on minimap is ON
        player["twentyFourHrScale"] = GRM.Use24HrBasedOnDefaultLanguage();  -- 39
        player["mainTagIndex"] = 2;                                         -- 42
        player["selectedLang"] = GRM_G.LocalizedIndex;                      -- 43 Selected Language ( 1 default is English)
        player["selectedFont"] = 1;                                         -- 44 Selected Font ( 1 is Default Blizz selected font )
        player["fontModifier"] = 0;                                         -- 45 Font Modifier Size
        player["mainTagColor"] = {};                                        -- 46
        player["mainTagColor"]["r"] = 1;                                    -- 46.r
        player["mainTagColor"]["g"] = 0;                                    -- 46.g
        player["mainTagColor"]["b"] = 0;                                    -- 46.b
        player["tooltipSize"] = 0.9;                                        -- 50
        player["dateFormat"] = 1;                                           -- 51
        player["useMainTag"] = true;                                        -- 65
        player["reportChannel"] = {};                                       -- 73 Channel(s) GRM messages go to
        player["defaultTabSelection"] = { false , 1 };
        
    -- Scan Options Tab
    elseif page == 2 then
        player["inactiveHours"] = 336;                                      -- 4
        player["eventAdvanceDays"] = 14                                     -- 5
        player["scanDelay"] = 30                                            -- 6 How often to check for changes ( in seconds )
        player["calendarAnnouncements"] = true;                             -- 12 Use them
        player["reportInactiveReturn"] = true;                              -- 11
        player["onlyAnnounceForMain"] = true;                               -- 17
        player["scanEnabled"] = true;                                       -- 18
        player["levelReportMin"] = 10;                                      -- 23 Only Report level increase greater than or equal to this.
        player["levelFilters"] = {true,true,true,true,true,true,true,true}; -- 47 Level Filter Options - 10 , 20 , 30 , 40 , 50 , 60 , etc...
        player["allAltRequirement"] = true;                                 -- 55 Only report returning from inactivity of ALL alts are past the threshold date.
        player["recordLevelUp"] = true;                                     -- 56 Only record level up changes if true
        
    -- Sync Options Tab
    elseif page == 3 then
        player["syncEnabled"] = true;                                       -- 14
        player["syncRank"] = GRM.GetRankRestrictedDefaultRankIndex()        -- 15 Rank Player must be to accept sync updates from them.
        player["syncChatEnabled"] = true;                                   -- 16 Notifications on sync messages with players
        player["syncSameVersion"] = true;                                   -- 19 Only sync with players with current version
        player["syncBanList"] = true;                                       -- 21
        player["syncRankBanList"] = GRM.GetFirstOfficerRank();              -- 22 Default sync ban restriction
        player["syncSpeed"] = 1;                                            -- 24 On a 1.0 scale - 1 = 100%
        player["exportAllRanks"] = true;                                    -- 35 Share data with ALL guildies, but only receive from your threshold rank
        player["syncCustomNote"] = true;                                    -- 38
        player["syncRankCustom"] = GRM.GetRankRestrictedDefaultRankIndex(); -- 49 Default rank for syncing the custom note... default is same as top rank
        player["syncBDays"] = true                                          -- 68
        player["autoTriggerSync"] = true                                    -- It will only auto-trigger the sync if this setting is enabled
        player["syncDelay"] = 60                                            -- Seconds after logging in to trigger sync. (Lowest option is 15)
        player["syncCompatibilityMsg"] = true                               -- Messages to chat once per session per player if they have an incompatible addon for sync
        
    -- Officer Options Tab
    elseif page == 4 then
        player["addTimestampToNote"] = false;                               -- 7 Join, Officer, or Custom
        player["allowEventsToCalendar"] = true;                             -- 8 
        player["joinDateDestination"] = 1;                                  -- 20 Officer Note = 1, Public Note = 2 , custom = 3
        player["customTags"] = { "" , "" };                                 -- 48 Custom Join and Rejoin tags
        player["includeTag"] = true;                                        -- 57 Include "Joined:" in the officer/public note join date tag
        player["addNotesToLeft"] = true;                                    -- 59 Report notes to the log on leaving guild...
        player["noteSetEnabled"] = true                                     -- 77 Allow non-guildies to set their own note with !note xxxxx
        
    -- Backup Options Tab
    elseif page == 5 then


    -- Help Options Tab
    elseif page == 6 then


    -- UI Options Tab
    elseif page == 7 then
        player["useFade"] = false;                                          -- 52
        
        -- No guild rep pre-Cataclysm expansion.
        if GRM_G.BuildVersion < 40000 then
            player["viewGuildRep"] = false;
        else
            player["viewGuildRep"] = true;                                  -- 53
        end
        
        player["showBorders"] = true;                                       -- 58
        player["showBDay"] = true                                           -- 67
        player["colorizeClassicRosterNames"] = true
        player["UIScaling"] = { 1.0 , 1.33 , 1.0 , 1.0 , 1.0 };             -- 82
        
    -- Log Options and Side Window
    elseif page == 8 then
        player["toLog"] = {};                                               -- 3
        player["toChat"] = {};                                              -- 13

        local s = "toLog";
        for i = 1 , 2 do

            if i == 2 then
                s = "toChat";
            end
            
            player[s]["joined"] = true;
            player[s]["leveled"] = true;
            player[s]["inactiveReturn"] = true;
            player[s]["promotion"] = true;
            player[s]["demotion"] = true;
            player[s]["note"] = true;
            player[s]["officerNote"] = true;
            player[s]["customNote"] = true;
            player[s]["nameChange"] = true;
            player[s]["rankRename"] = true;
            player[s]["eventAnnounce"] = true;
            player[s]["left"] = true;
            player[s]["recommend"] = true;
            player[s]["banned"] = true;
            
        end

        player["showLineNumbers"] = true;                                   -- 36
        player["shiftClickRemove"] = true;                                  -- 37
        player["autoFocusSearch"] = false;                                  -- 66
        player["showTooltip"] = true                                        -- 70
        player["logFontSize"] = 0;                                          -- 78
        player["logColor"] = { 
            { 0.5 , 1.0 , 0.0 },        -- Joined 
            { 0.0 , 0.44 , 0.87 },      -- Leveled
            { 0.0 , 1.0 , 0.87 },       -- Inactive Return
            { 1.0 , 0.914 , 0.0 },      -- Promotions
            { 0.91 , 0.388 , 0.047 },   -- Demotions
            { 1.0 , 0.6 , 1.0 },        -- Note
            { 1.0 , 0.094 , 0.93 },     -- Officer Note
            { 0.24 , 0.69 , 0.49 },     -- Custom Note
            { 0.90 , 0.82 , 0.62 },     -- Name Change
            { 0.64 , 0.102 , 0.102 },   -- Rank Rename
            { 0.0 , 0.8 , 1.0 },        -- Event Announce
            { 0.5 , 0.5 , 0.5 },        -- Left Guild
            { 0.65 , 0.19 , 1.0 },      -- Recommendations
            { 1.0 , 0.0 , 0.0 }         -- Banned Coloring        
        };
        
    -- Export Options
    elseif page == 9 then
        player["exportDelimiter"] = { true , ";" };                         -- 79
        player["exportFilters"] = {true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,true,true} -- 80 "19 filters so far"
        
        -- Guild rep filter needs to be disabled in Classic
        if GRM_G.BuildVersion < 40000 then
            player["exportFilters"][10] = false;
        end

        player["columnHeaders"] = false;                                    -- 81
        
    -- Macro Tool Kick Options
    elseif page == 10 then
        player["kickRules"] = {};
        if not isPatch then
            player["kickRules"][GRM.L ( "Kick Rule {num}" , nil , nil , 1 )] = GRM.BuildNewKickRuleTemplate ( GRM.L ( "Kick Rule {num}" , nil , nil , 1 ) , 1 )          -- 75
        else
            player["kickRules"] = { { 1 , 1 , 1 , 12 , true } };            -- 75
        end
        player["ignoreFilter"] = false;                                     -- 76 Safe List hybrid scrollframe checkbox - true to only show players where actions ignored

    -- Macro Tool Promote Options
    elseif page == 11 then
        player["promoteRules"] = {};

    -- Macro Tool Demote Options
    elseif page == 12 then
        player["demoteRules"] = {};
    
    -- Audit
    elseif page == 13 then
        player["onlyShowIncomplete"] = false;                               -- 30
        player["unknownIsComplete"] = true;                                 -- 33
        player["includeBirthdaysInAudit"] = false
        
    -- Backups
    elseif page == 14 then
        player["allowAutoBackups"] = true;                                  -- 34
        player["autoIntervalDays"] = 7;                                     -- 41

    -- Modules
    elseif page == 15 then

        -- GroupInfoModule
        player["GIModule"] = {};
        player["GIModule"]["enabled"] = true;
        player["GIModule"]["InteractDistanceIndicator"] = true
        player["GIModule"]["tradeIndicatorColorAny"] = { 0 , 0.97 , 0.97 };
        player["GIModule"]["tradeIndicatorColorConnectedRealm"] = { 0 , 0.97 , 0.97 };
        
    end

end

-- Method:          GRM.LoadSettings()
-- What it Does:    On first time loading addon, it builds default addon settings. It checks for addon version change
--                  And, if there are any changes, they will be added into that logic block. 
--                  And new setting can be tagged on.
-- Purpose:         Saving settings between gaming sessions. Also, this is built to provide backwards compatibility for future flexibility on feature adding, if necessary.
GRM.LoadSettings = function()

    if GRM_AddonSettings_Save["H"] == nil and GRM_AddonSettings_Save[1] == nil then
        GRM_GuildMemberHistory_Save , GRM_PlayersThatLeftHistory_Save , GRM_CalendarAddQue_Save , GRM_LogReport_Save , GRM_AddonSettings_Save , GRM_PlayerListOfAlts_Save , GRM_GuildDataBackup_Save = GRM.ClearPermData();
    end

    -- Build the settings
    -- Need to doublecheck Faction Index ID
    if GRM_G.FID == 0 then
        if GRM_G.faction == "Horde" then
            GRM_G.FID = 1;
            GRM_G.selectedFID = "H";
            GRM_G.F = "H";
        else
            GRM_G.FID = 2;
            GRM_G.selectedFID = "A";
            GRM_G.F = "A";
        end
    end

    if GRM_G.F == "" then
        if GRM_G.faction == "Horde" then
            GRM_G.F = "H";
            GRM_G.selectedFID = "H";
        else
            GRM_G.F = "A";
            GRM_G.selectedFID = "A";
        end
    end

    local patchSettingsIsNeeded = false;
    local isFound = false;
    local indexFound = 0;
    local indexFoundFaction = "";

    -- This means we are in the old database that needs to be updated.
    -- Pre 1.84
    if GRM_AddonSettings_Save["H"] == nil then
        GRM_G.oldDB = true;
        for j = 2 , #GRM_AddonSettings_Save[GRM_G.FID] do
            if GRM_AddonSettings_Save[GRM_G.FID][j][1] == GRM_G.addonUser then
                isFound = true;
                indexFound = j;
                indexFoundFaction = GRM_G.FID;

                if GRM_AddonSettings_Save[GRM_G.FID][j][2][1] ~= GRM_G.Version then
                    patchSettingsIsNeeded = true;
                end
                break;
            end
        end
        
        -- Verify at least one profile needs to be updated. This is done to account for new toons as well on an update.
        if not isFound then
            for i = 1 , #GRM_AddonSettings_Save do
                for j = 2 , #GRM_AddonSettings_Save[i] do
                    if GRM_AddonSettings_Save[i][j][2][1] ~= GRM_G.Version then
                        patchSettingsIsNeeded = true;
                        indexFound = j;
                        indexFoundFaction = i;
                        break;
                    end
                end
                if patchSettingsIsNeeded then
                    break;
                end
            end

        -- Integrity check on the addon settings.
        else
            isFound = GRM_Patch.PlayerSettingsIntegrityCheck();
        end
        
    else

        -- 1.84 and on
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser] ~= nil then
            isFound = true;
            indexFoundFaction = GRM_G.F;
            indexFound = GRM_G.addonUser;     -- Maintain formatting compatibility with old indexing for ease.
        end

        for x in pairs ( GRM_AddonSettings_Save ) do
            for y in pairs ( GRM_AddonSettings_Save[x] ) do
                if GRM_AddonSettings_Save[x][y].version ~= nil and GRM_AddonSettings_Save[x][y].version ~= GRM_G.Version then
                    patchSettingsIsNeeded = true;
                    indexFound = y;
                    indexFoundFaction = x;
                    break;
                end
            end
            if patchSettingsIsNeeded then
                break;
            end
        end
        
    end

    -- Build settings for first time.
    if not isFound then
        GRM.Report ( "\n" .. GRM.L ( "Configuring Guild Roster Manager for {name} for the first time." , GetUnitName ( "PLAYER" , false ) ) );

        if not GRM_G.oldDB then

            -- Add new player
            GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser] = {};
            -- Load the Default Settings
            for i = 0 , GRM_G.SettingsPages do
                GRM.SetDefaultAddonSettings ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser] , i , false );
            end

            GRM.ConfigureMiscForPlayer( GRM_G.addonUser );

        else
            -- Old Database so still have FID outdated system
            -- Add new player
            table.insert ( GRM_AddonSettings_Save[GRM_G.FID] , { GRM_G.addonUser } );
            GRM_G.setPID = #GRM_AddonSettings_Save[GRM_G.FID];                                -- We know what the ID is...
            

            local AllDefaultSettings = GRM_Patch.GetDefaultAddonSettings();
            -- Unique Settings added to the player.
            table.insert ( GRM_AddonSettings_Save[GRM_G.FID][GRM_G.setPID] , AllDefaultSettings );
        end

        GRM_G.IsNewToon = true;
        -- Forcing core log window/options frame to load on the first load ever as well
        GRM_G.ChangesFoundOnLoad = true;
    elseif not IsInGuild() then
        GRM_G.IsNewToon = true;
        GRM_G.ChangesFoundOnLoad = true;
    end

    if patchSettingsIsNeeded then
        -- numericV is used to compare older versions.
        local playerV;
        if not GRM_G.oldDB then
            playerV = GRM_AddonSettings_Save[indexFoundFaction][indexFound].version;
        else
            playerV = GRM_AddonSettings_Save[indexFoundFaction][indexFound][2][1];
        end

        -- PATCH FIXES
        GRM_Patch.SettingsCheck ( tonumber ( string.match ( playerV , "R(.+)" ) ) );
        
    else
        -- No need to delay
        GRM.FinalSettingsConfigurations();
    end

end

-- Method:          GRM.FinalSettingsConfigurations()
-- What it Does:    Calculates the final settings configurations
-- Purpose:         Compartmentalizes this so it can only be on call as needed.
GRM.FinalSettingsConfigurations = function()

    if GRM_G.oldDB then
        GRM.ConfigureMiscForPlayer( GRM_G.addonUser );
    end

    -- In case of disconnect during some events that you may need to continue from.
    GRM.MiscCleanupOnLogin();
    -- Let's load that minimap button now too...
    GRM_UI.GRM_MinimapButtonInit();
    -- One time processing, saves a bit of resources for an oft used string manipulation feature.
    GRM_UI.CorePositionInit();
    GRM_UI.CoreToolPositionInit();
    -- Register window to save data to.
    GRM.SetReportWindow( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel );
    -- Set Window Scales
    GRM.SetAllWindowScales();

    GRM.RefreshMainTagHexCode();

    if IsInGuild() then
        GRM.SetGuildInfoDetails(); -- an early systems check too
    end

    -- Re-setup macro
    if GRM_G.BuildVersion < 40000 then
        GRM.BuildGuildRosterHotkeyAndMacroCLASSIC();
    else
        GRM.BuildGuildRosterHotkeyAndMacro();
    end

    -- For sync...
    GRMsyncGlobals.timeAtLogin = time();    -- Important for Sync Leader backend election algorithm.
    GRMsyncGlobals.ThrottleCap = GRMsyncGlobals.ThrottleCap * GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncSpeed;   -- Adjust the throttle cap

    -- The Tag Headers
    GRM.SetJoinAndRejoinTags();

    -- General one-time configurations
    GRM.RefreshNumberOfHoursTilRecommend();

    -- Classic Chat coloring
    GRM.SetClassChatColoring()

    GRM_G.AddonIsFullyConfigured = true;
    GRM_API.Initialized = true;

    -- Settings loaded... carry on.
    GRM.SettingsLoadedFinishDataLoad();

    
end

-------------------------------------
-------- BUILD COMPATIBILITY --------
-------------------------------------

-- ALL these functions are to deal with possibility of Retail and Classic backend APIs no longer being sync'd the same. When 9.0 launched October 2020 the underlying API was changed for many things, but Classic did not receive those updates. It is assumed those will eventually be made the same again in future updates for Classic. By Wrapping them in a GRM function it can be easy to control by just needing to update them here rather than addon-wide.

GRM.CanViewOfficerNote = function()
    if C_GuildInfo.CanViewOfficerNote then
        return C_GuildInfo.CanViewOfficerNote();
    else
        return CanViewOfficerNote();
    end
end

GRM.CanEditOfficerNote = function()
    if C_GuildInfo.CanEditOfficerNote then
        return C_GuildInfo.CanEditOfficerNote();
    else
        return CanEditOfficerNote();
    end
end

GRM.GuildRoster = function()
    if C_GuildInfo.GuildRoster then
        C_GuildInfo.GuildRoster();
    else
        GuildRoster();
    end
end

GRM.GetTodaysDate = function()
    local calendarTime;

    if C_DateAndTime.GetCurrentCalendarTime then
        calendarTime = C_DateAndTime.GetCurrentCalendarTime();
        return calendarTime.weekday, calendarTime.month, calendarTime.monthDay, calendarTime.year;
    else
        calendarTime = C_DateAndTime.GetTodaysDate();
        return calendarTime.weekDay, calendarTime.month, calendarTime.day, calendarTime.year;
    end
end

GRM.GetCurrentCalendarTime = function()
    if C_DateAndTime.GetCurrentCalendarTime then
        return C_DateAndTime.GetCurrentCalendarTime();
    else
        return C_DateAndTime.GetTodaysDate();
    end
end

---------------------------------------
----- END OF BUILD COMPAT... ----------
---------------------------------------

-- What it Deos:    Initializes chat coloring controls for classic
-- Purpose:         Quality of life control
GRM.SetClassChatColoring = function()
    if GRM_G.BuildVersion < 23000 then
        local num = 0;
        
        if not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].colorizeClassicRosterNames then
            num = 1;
            if GetCVar ( "chatClassColorOverride" ) == "2" then
                num = 2;
            end
        end

        SetCVar("chatClassColorOverride" , num );

        hooksecurefunc ( "GuildStatus_Update" , function()
            local button;
            if IsInGuild() and GuildFrame and GuildFrame:IsVisible() then
                for i = 1 , 13 do

                    if GuildStatusFrame and GuildStatusFrame:IsVisible() then
                        button = _G["GuildFrameGuildStatusButton" .. i];
                        if button and button:IsVisible() then
                            GRM.RecolorText( button );
                        end
                    elseif GuildPlayerStatusFrame and GuildPlayerStatusFrame:IsVisible() then
                        button = _G["GuildFrameButton" .. i];
                        if button and button:IsVisible() then
                            GRM.RecolorText( button );
                        end
                    end
                end
            end
        end);

    end
end

-- method:          GRM.RefreshNumberOfHoursTilRecommend()
-- What it Does:    Rebuilds the time on the recommends for the macro tool and the log.
-- Purpose:         Resource saving. No need to process over and over everytime it is looked at unless a change is made.
GRM.RefreshNumberOfHoursTilRecommend = function()
    GRM_G.NumberOfHoursTilRecommend["kick"] = {};
    GRM_G.NumberOfHoursTilRecommend["kickActive"] = {};

    GRM_G.NumberOfHoursTilRecommend["promote"] = {};
    GRM_G.NumberOfHoursTilRecommend["demote"] = {};

    local validateFormat = function ( exactRule , defaultMonths )
        
        if type ( exactRule.numDaysOrMonths ) ~= "number" then
            exactRule.numDaysOrMonths = 12;
            exactRule.isMonths = true;
        end

        if not exactRule.rankSpecialNumDaysOrMonths then
            exactRule.rankSpecialNumDaysOrMonths = defaultMonths;
        end

        return exactRule;
    end

    for ruleName , rule in pairs ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].kickRules ) do
        
        if rule.activityFilter then                                               -- Only need to add if this part is enabled.
            GRM_G.NumberOfHoursTilRecommend["kick"][ruleName] = 0;

            rule = validateFormat ( rule , 12 );

            if rule.isMonths then
                GRM_G.NumberOfHoursTilRecommend["kick"][ruleName] = GRM.GetNumHoursTilRecommend ( rule.numDaysOrMonths );
            else
                GRM_G.NumberOfHoursTilRecommend["kick"][ruleName] = ( rule.numDaysOrMonths * 24 );
            end
        end
        if rule.applyEvenIfActiive then
            GRM_G.NumberOfHoursTilRecommend["kickActive"][ruleName] = 0;
            if rule.rankSpecialIsMonths then
                GRM_G.NumberOfHoursTilRecommend["kickActive"][ruleName] = GRM.GetNumHoursTilRecommend ( rule.rankSpecialNumDaysOrMonths );
            else
                GRM_G.NumberOfHoursTilRecommend["kickActive"][ruleName] = ( rule.rankSpecialNumDaysOrMonths * 24 );
            end
        end
    end

    for ruleName , rule in pairs ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].promoteRules ) do
        if rule.activityFilter then                                               -- Only need to add if this part is enabled.
            GRM_G.NumberOfHoursTilRecommend["promote"][ruleName] = {};
            GRM_G.NumberOfHoursTilRecommend["promote"][ruleName].hours = 0;

            rule = validateFormat ( rule , 3 );

            if rule.isMonths then
                GRM_G.NumberOfHoursTilRecommend["promote"][ruleName].hours = GRM.GetNumHoursTilRecommend ( rule.numDaysOrMonths );
            else
                GRM_G.NumberOfHoursTilRecommend["promote"][ruleName].hours = ( rule.numDaysOrMonths * 24 );
            end

            if not rule.regardlessOfActivity then
                GRM_G.NumberOfHoursTilRecommend["promote"][ruleName].inactive = 0;
                if rule.rankSpecialIsMonths then
                    GRM_G.NumberOfHoursTilRecommend["promote"][ruleName].inactive = GRM.GetNumHoursTilRecommend ( rule.rankSpecialNumDaysOrMonths );
                else
                    GRM_G.NumberOfHoursTilRecommend["promote"][ruleName].inactive = ( rule.rankSpecialNumDaysOrMonths * 24 );
                end
            end

        end
    end

    for ruleName , rule in pairs ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].demoteRules ) do
        if rule.activityFilter then                                               -- Only need to add if this part is enabled.
            GRM_G.NumberOfHoursTilRecommend["demote"][ruleName] = {};
            GRM_G.NumberOfHoursTilRecommend["demote"][ruleName].hours = 0;

            rule = validateFormat ( rule );

            if rule.isMonths then
                GRM_G.NumberOfHoursTilRecommend["demote"][ruleName].hours = GRM.GetNumHoursTilRecommend ( rule.numDaysOrMonths );
            else
                GRM_G.NumberOfHoursTilRecommend["demote"][ruleName].hours = ( rule.numDaysOrMonths * 24 );
            end

            if not rule.regardlessOfActivity then
                GRM_G.NumberOfHoursTilRecommend["demote"][ruleName].evenIfActiveHours = 0;
                if rule.rankSpecialIsMonths then
                    GRM_G.NumberOfHoursTilRecommend["demote"][ruleName].evenIfActiveHours = GRM.GetNumHoursTilRecommend ( rule.rankSpecialNumDaysOrMonths );
                else
                    GRM_G.NumberOfHoursTilRecommend["demote"][ruleName].evenIfActiveHours = ( rule.rankSpecialNumDaysOrMonths * 24 );
                end
            end

        end
    end
end

-- Method:          GRM.GetPageIndex()
-- What it Does:    Returns the index number to the corresponding frame
-- Purpose:         To coordinate in resetting default settings based on the specific frame the player is on.
GRM.GetPageIndex = function()
    local allFrames = { [ GRM_GeneralOptionsFrame ] = 1  , [ GRM_ScanningOptionsFrame ] = 2  , [ GRM_SyncOptionsFrame ] = 3  , [ GRM_OfficerOptionsFrame ] = 4  , [ GRM_UXOptionsFrame ] = 7 , [ GRM_LogExtraOptionsFrame ] = 8  , [GRM_ModulesFrame] = 15 };
    local result;

    for x , y in pairs ( allFrames ) do
        if x:IsVisible() then
            result = y;
        end
    end
    return result;
end

-- Method:          GRM.ResetDefaultSettings( int )
-- What it Does:    Resets the OPTIONS to the default one for only the currently logged in player
-- Purpose:         Easy, quality of life for user in the options, for simple reset.
GRM.ResetDefaultSettings = function( pageIndex )
    local page = pageIndex or GRM.GetPageIndex();
    local needsRefresh = false;
    local resetAll = false;

    if IsShiftKeyDown() and IsControlKeyDown() then
        needsRefresh = true;
        resetAll = true;

        -- resetting ALL player data.
        for i = 0 , GRM_G.SettingsPages do
            GRM.SetDefaultAddonSettings ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser] , i , false );
        end

    elseif page then
        needsRefresh = true;
        -- Reset only specific settings
        GRM.SetDefaultAddonSettings ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser] , page , false );

        if page == 10 then
            GRM.RefreshNumberOfHoursTilRecommend();
        end

    end

    if needsRefresh then

        if ( resetAll or page == 1 ) and GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then
            GRML.SetNewLanguage( GRM_G.LocalizedIndex , false , true );
        end

        -- Check Permissions
        GRM.UpdateGuildLeaderPermissions ( false , true );

        -- Reset frames as needed
        if ( resetAll or page < 9 ) and GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then     -- General options
            GRM_UI.BuildLogFrames();
        end

        if page == 15 then
            GRM_UI.ConfigureGroupInfoRules();
        end

        -- refresh the log as settings might be changed.
        if ( resetAll or page == 8 ) and GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame:IsVisible() then
            GRM_UI.RefreshLogExtraOptions();
            GRM_UI.RefreshLogColorOptions();
            GRM.ResetLogStringPoints ( true );
            GRM.BuildLogComplete( true , false );
        end

        if ( resetAll or page == 9 ) and GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame:IsVisible() then
            GRM_UI.SetExportTabHighlights ( GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.TabPosition );
        end

        if ( resetAll or ( page > 9 and page < 13 ) ) and GRM_UI.GRM_ToolCoreFrame:IsVisible() then
            GRM_UI.RefreshManagementTool( GRM_G.KickAltControl );
        end

        GRM_UI.GRM_MinimapButtonInit();
        GRM_UI.CorePositionInit();
        GRM_UI.CoreToolPositionInit();
        GRM.SetAllWindowScales();
        GRM.RefreshMainTagHexCode();
        GRM.SetReportWindow( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel );

        GRM.RefreshNumberOfHoursTilRecommend();
    end

end

-- Method:          GRM.SyncAddonSettings()
-- What it Does:    It syncs all of the addon settings of the current player with all of the other alts the player has within that guild
-- Purpose:         To have a "global" settings option.
GRM.SyncAddonSettings = function()

    if GRM_G.guildName ~= "" and GRM_PlayerListOfAlts_Save[GRM_G.F][GRM_G.guildName] ~= nil then
        for altName in pairs ( GRM_PlayerListOfAlts_Save[GRM_G.F][GRM_G.guildName] ) do

            if altName ~= GRM_G.addonUser and GRM_AddonSettings_Save[GRM_G.F][altName] ~= nil then

                local tempSettingsHolder = GRM.DeepCopyArray ( { GRM_AddonSettings_Save[GRM_G.F][altName].minimapEnabled , GRM_AddonSettings_Save[GRM_G.F][altName].minimapPos , GRM_AddonSettings_Save[GRM_G.F][altName].minimapRad , GRM_AddonSettings_Save[GRM_G.F][altName].customPos , GRM_AddonSettings_Save[GRM_G.F][altName].minimapCustomPos , GRM_AddonSettings_Save[GRM_G.F][altName].CoreWindowPos , GRM_AddonSettings_Save[GRM_G.F][altName].reportChannel , GRM_AddonSettings_Save[GRM_G.F][altName].macroToolCoordinates } );

                local tempTable = GRM.DeepCopyArray ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser] );  -- You need to set these values or else they won't carry over
                GRM_AddonSettings_Save[GRM_G.F][altName] = tempTable;      -- overwrite each player's settings with the current
                GRM_AddonSettings_Save[GRM_G.F][altName].minimapEnabled = tempSettingsHolder[1];
                GRM_AddonSettings_Save[GRM_G.F][altName].minimapPos = tempSettingsHolder[2];
                GRM_AddonSettings_Save[GRM_G.F][altName].minimapRad = tempSettingsHolder[3];
                GRM_AddonSettings_Save[GRM_G.F][altName].customPos = tempSettingsHolder[4];
                GRM_AddonSettings_Save[GRM_G.F][altName].minimapCustomPos = tempSettingsHolder[5];
                GRM_AddonSettings_Save[GRM_G.F][altName].CoreWindowPos = tempSettingsHolder[6];
                GRM_AddonSettings_Save[GRM_G.F][altName].reportChannel = tempSettingsHolder[7];
                GRM_AddonSettings_Save[GRM_G.F][altName].macroToolCoordinates = tempSettingsHolder[8];
            end
        end
    elseif not GRM_PlayerListOfAlts_Save[GRM_G.F][GRM_G.guildName] then
        GRM_PlayerListOfAlts_Save[GRM_G.F][GRM_G.guildName] = {};
    end
     
end

-- Method:          GRM.SyncAddonSettingsOfNewToon()
-- What it Does:    If a new player is joins the guild, and you already have a player in there, and one of the alts has it set to sync the settings in the guild,
--                  it loads the alt's settings.
-- Purpose:         Settings Sync feature...
GRM.SyncAddonSettingsOfNewToon = function()

    if GRM_G.guildName ~= "" and GRM_PlayerListOfAlts_Save[GRM_G.F][GRM_G.guildName] ~= nil then
        for altName in pairs ( GRM_PlayerListOfAlts_Save[GRM_G.F][GRM_G.guildName] ) do
            if altName ~= GRM_G.addonUser then

                -- Yes, sync needs to happen.
                if GRM_AddonSettings_Save[GRM_G.F][altName].syncSettings then
                    -- Ok, add all settings to a new array.
                    local tempTable = GRM.DeepCopyArray ( GRM_AddonSettings_Save[GRM_G.F][altName] );   
                    GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser] = tempTable;

                end

                break;
            end
        end
    elseif not GRM_PlayerListOfAlts_Save[GRM_G.F][GRM_G.guildName] then
        GRM_PlayerListOfAlts_Save[GRM_G.F][GRM_G.guildName] = {};
    end
end

-- Method:          GRM.SyncSettings()
-- What it Does:    Checks if player has it enabled to sync all addon settings, and if so, initiates that sync
-- Purpose:         To be triggered every single time a settings is modified.
GRM.SyncSettings = function()
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncSettings then
        GRM.SyncAddonSettings();
    end
end


-----------------------------
--- Addon to Addon Comms ----
---- pattern matching -------
-----------------------------

-- Method:          GRM.BuildComPattern ( int , string , int )
-- What it Does:    Returns a string pattern to match for addon to addon comm parsing
-- Purpose:         to easily build and create patterns as needed for comms.
GRM.BuildComPattern = function( numItems , delimiter , delimiterIsOnEnd )
    local result = "";

    for i = 1 , numItems do
        if i < numItems then
            result = result .. "(.+)" .. delimiter;
        else
            if delimiterIsOnEnd then
                result = result .. "(.+)" .. delimiter;
            else
                result = result .. "(.+)";
            end
        end
    end

    return result;
end

-- Method:          GRM.ParseComMsg ( msg )
-- What it Does:    Returns the matches to the given pattern
-- Purpose:         More easily manage comm messages cleaner.
GRM.ParseComMsg = function ( msg , pattern )
    return string.match ( msg , pattern );
end

-----------------------------------
--- All the rest of the methods ---
-----------------------------------

-- Method:          GRM.IsMouseOverAnyChatWindowIncludingCommunities()
-- What it Does:    Reports back if player has the cursor over a given guild window
-- Purpose:         Since there is no right-click trigger, this detects the popup window use, and it will only pin the window properly *IF* it is the appropriate time due to the reusability of this window by Warcraft
GRM.IsMouseOverAnyChatWindowIncludingCommunities = function()
    local result = false;
    local name = "";
    local server = "";

    for i = 1 , #CHAT_FRAMES do
        if GetClickFrame ( CHAT_FRAMES[i] ):IsMouseOver() then
            result = true;
            name = GRM.AppendSameServerName ( DropDownList1.dropdown.chatTarget );
            break;
        end
    end

    if not result then
        
        if ( GRM_G.BuildVersion >= 80000 and CommunitiesFrame and CommunitiesFrame:IsVisible() and CommunitiesFrame:IsMouseOver() and GRM_G.gClubID == C_Club.GetGuildClubId() ) then
        
            if DropDownList1.dropdown.guid ~= nil then
                name , server = select ( 6 , GetPlayerInfoByGUID ( DropDownList1.dropdown.guid ) );
                if name == nil or name == "" then
                    name , server = select ( 6 , GetPlayerInfoByGUID ( DropDownList1.dropdown.guid ) ); -- Try again. Sometimes GUID takes 2 tries as the first just initializes the server to provide info on next call
                end

                if name ~= nil and name ~= "" then
                    if server == "" then
                        name = name .. "-" .. GRM_G.realmName;
                    else
                        name = name .. "-" .. server;
                    end
                end
                result = true;
            end
            
        elseif GRM_UI.GuildRosterFrame and GRM_UI.GuildRosterFrame:IsVisible() and GRM_UI.GuildRosterFrame:IsMouseOver() then
            if DropDownList1.dropdown.guid ~= nil then
                name = DropDownList1.dropdown.name .. "-" .. DropDownList1.dropdown.server;
                result = true;
            end
        end

    end
    
    return result , name;
end

-- Method:          GRM.SetReportWindow ()
-- What it Does:    Sets the addon reporting to a specified custom channel
-- Purpose:         Allow flexibility to send GRM reports to a custom channel so you don't miss anything.
GRM.SetReportWindow = function()
    local isEstablished = false;
    local chatFrame;

    for i = #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel , 1 , -1 do
        isEstablished = false;

        for j = 1 , #CHAT_FRAMES do
            chatFrame = GetClickFrame ( CHAT_FRAMES[j] );

            if chatFrame.name == GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel[i] then
                -- Custom frame writte, custom frame found! Now, let's see if there is a tab

                if GetClickFrame ( chatFrame:GetName() .. "Tab" ):IsVisible() then
                    isEstablished = true;
                    GRM.AddReportChannel ( nil , chatFrame , true );        -- Just adds it to the GRM_G.Chat
                    break;
                end

            end
        end

        if not isEstablished then
            table.remove ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel , i );
        end
    end

    if #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel == 0 then
        GRM_G.Chat = { DEFAULT_CHAT_FRAME };
    end
end

-- Method:          GRM.SetJoinAndRejoinTags()
-- What it Does:    Configures the join and rejoin tag to be either the default tag, or to be the custom.
-- Purpose:         Increased flexibility for the player in terms of visual controls.
GRM.SetJoinAndRejoinTags = function()
    -- Eventually to be modified to allow a custom header configuration. For now just these defaults
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[1] == "" then
        GRM_G.customHeaderJoin = GRM.L ( "Joined:" );
    else
        GRM_G.customHeaderJoin = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[1];
    end

    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[2] == "" then
        GRM_G.customHeaderRejoin = GRM.L ( "Rejoined:" );
    else
        GRM_G.customHeaderRejoin = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[2];
    end
end

-- Method:          GRM.AddReportChannel ( string , object , bool )
-- What it Does:    Adds the custom channel to the saved data so it can be stored on each subsequent session
-- Purpose:         Quality of Life control to user to report info to more than 1 window.
GRM.AddReportChannel = function ( name , newFrame , onlyFrame )
    local isFound = false;

    if not onlyFrame then
        for i = 1 , #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel do
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel[i] == name then
                isFound = true;
                break;
            end
        end

        if not isFound then
            table.insert ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel , name );
        end

        isFound = false;
    end

    for i = 1 , #GRM_G.Chat do
        if GRM_G.Chat[i] == newFrame then
            isFound = true;
            break;
        end
    end

    if not isFound then
        table.insert ( GRM_G.Chat , newFrame );
    end
end

-- Method:          GRM.CreateChatTabs()
-- What it Does:    Using the created listed of channels in the General Options, it creates the tabs if they do not exist in a Que, with a confirmation box
-- Purpose:         UX quality control
GRM.CreateChatTabs = function()
    local customName = GRM_G.UnconfirmedChatTabs[1];
    -- Core Blizzard logic to create a new window
    local frame = FCF_OpenNewWindow ( customName );

    -- Now we configure them
    FCF_CopyChatSettings ( frame , DEFAULT_CHAT_FRAME );
    FCF_DockUpdate();

    -- GRM actions - Add the names and set the reporting logic to GRM database
    GRM.AddReportChannel ( customName , frame , false );

    -- Update the options editbox
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_ReportDestinationEditBox:SetText( GRM.BuildMultiChannelString() );
    
    -- Helpful message to players
    if #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel == 1 and #GRM_G.UnconfirmedChatTabs == 1 then
        GRM.Report ( GRM.L ( "GRM will automatically send messages to the \"{name}\" window if you close this one." , DEFAULT_CHAT_FRAME.name ) );
    elseif #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel > 1 then
        GRM.Report ( GRM.L ( "GRM will automatically send messages to the \"{name}\" window if you remove all custom channels." , DEFAULT_CHAT_FRAME.name ) );
    end

    -- Close the window...
    GRM_UI.GRM_RosterConfirmFrame:Hide();

    table.remove ( GRM_G.UnconfirmedChatTabs , 1 );

    -- recursively repeat this as needed
    if #GRM_G.UnconfirmedChatTabs > 0 then
        GRM.InitiateConfirmFrame ( GRM.L ( "\"{name}\" Chat Window\nDo you wish to create it?" , GRM_G.UnconfirmedChatTabs[1] ) , GRM.CreateChatTabs , nil , nil , GRM.CancelChatTabCreation , true );
    end
end

-- Method:          GRM.CancelChatTabCreation()
-- What it Does:    Cancels this action, and removes the channel tab name
-- Purpose:         UX quality control
GRM.CancelChatTabCreation = function()
    if #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel == 0 then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_ReportDestinationEditBox:SetText ( DEFAULT_CHAT_FRAME.name );
    else
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_ReportDestinationEditBox:SetText ( GRM.BuildMultiChannelString() );
    end
    table.remove ( GRM_G.UnconfirmedChatTabs , 1 );
    if #GRM_G.UnconfirmedChatTabs > 0 then
        GRM.InitiateConfirmFrame ( GRM.L ( "\"{name}\" Chat Window\nDo you wish to create it?" , GRM_G.UnconfirmedChatTabs[1] ) , GRM.CreateChatTabs , nil , nil , GRM.CancelChatTabCreation , true );
    end
end

-- Method:          GRM.EstablishNewCustomReportWindow();
-- What it Does:    Takes a new custom one and determines if there is a window, and if there is no window asks the player if they wish to create a GRM channel\
-- Purpose:         Quality of Life feature for creating custom Window to send reports to.
GRM.EstablishNewCustomReportWindow = function( channelNames )
    local isEstablished = false;
    local chatFrame;
    local channels = GRM.ParseMultiChannelString ( channelNames );
    GRM_G.UnconfirmedChatTabs = {};

    -- No need to do the work if it is already set to default
    if #channels > 0 then
        -- Let's Confirm the frames

        for i = 1 , #channels do
            isEstablished = false;
            for j = 1 , #CHAT_FRAMES do
                chatFrame = GetClickFrame ( CHAT_FRAMES[j] );

                if chatFrame.name == channels[i] then
                    -- Custom frame writte, custom frame found! Now, let's see if there is a tab

                    if GetClickFrame ( chatFrame:GetName() .. "Tab" ):IsVisible() then
                        isEstablished = true;
                        GRM.AddReportChannel ( channels[i] , chatFrame , false );
                        break;
                    end

                end
            end

            if not isEstablished then
                table.insert ( GRM_G.UnconfirmedChatTabs , channels[i] );   -- These are custom chat tabs that don't exist yet and need to be built. Storing them so they can be qued up.
            end
        end

        if #GRM_G.UnconfirmedChatTabs > 0 then
            -- Popup window logic...
            GRM.InitiateConfirmFrame ( GRM.L ( "\"{name}\" Chat Window\nDo you wish to create it?" , GRM_G.UnconfirmedChatTabs[1]  ) , GRM.CreateChatTabs , nil , nil , GRM.CancelChatTabCreation , true );
        end
    else
        GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel = {};
        GRM_G.Chat = { DEFAULT_CHAT_FRAME };
    end

    GRM.CleanupUnusedChannels( channels );
end

-- Method:          GRM.CleanupUnusedChannels()
-- What it Does:    Checks if the channel is still one listed. If not, removes it
-- Purpose:         Quality control for the use of custom channels. Ensures cleanup when channels removed.
GRM.CleanupUnusedChannels = function( channels )
    local isEstablished;

    -- Remove ones not in the Channel List
    for i = #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel , 1 , -1 do
        isEstablished = false;

        for j = 1 , #channels do
            if channels[j] == GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel[i] then
                isEstablished = true;
                break;
            end            
        end

        if not isEstablished then
            table.remove ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel , i );
        end
    end

    for i = #GRM_G.Chat , 1 , -1 do
        isEstablished = false;

        for j = 1 , #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel do
            if GRM_G.Chat[i].name == GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel[j] then
                isEstablished = true;
                break;
            end
        end
    
        if not isEstablished then
            table.remove ( GRM_G.Chat , i );
        end
    end
end

-- Method:          GRM.IsValidChannelName ( string )
-- What it Does:    Determines the number of characters, not byte string lenght, as that is not accurate, and returns true if it is 31 characters or less. Numbers within channel names IS compatible.
-- Purpose:         So the player who is inputing channel names can determine if his are valid.
GRM.IsValidChannelName = function ( name )
    local result = true;

    if GRM.UTF8Len ( name ) > 31 then       -- Length of Max Characters is 31
        result = false;
    end

    return result
end

-- Method:          GRM.ParseMultiChannelString ( string )
-- What it Does:    Takes the string delimited by commas and parsed all the words into a table
-- Purpose:         Particularly for managing multiple channels to send info to
GRM.ParseMultiChannelString = function ( channels )
    channels = GRM.Trim ( channels );
    local result = {};
    local ind = string.find ( channels , "," , 1 , true );
    local tableName = "";
    
    if #channels > 0 then

        while ind ~= nil and #channels > 0 do

            tableName = GRM.Trim ( string.sub ( channels , 1 , ind - 1 ) );

            -- Is Valid Name?
            if GRM.IsValidChannelName ( tableName ) then
                table.insert ( result , tableName );
            end

            channels = string.sub ( channels , ind + 1 );
            ind = string.find ( channels , "," , 1 , true );

            -- No comma on the end
            if ind == nil and #channels > 0 then
               tableName = GRM.Trim ( channels );

               -- Is Valid Name?
               if GRM.IsValidChannelName ( tableName ) then
                    table.insert ( result , tableName );
                end

            end

        end

        if #result == 0 and ind == nil then
            table.insert ( result , GRM.Trim ( channels ) );
        end

    end

    return result;
end

-- Method:          GRM.BuildMultiChannelString()
-- What it Does:    Builds a string with comma delimiters of all the channel names
-- Purpose:         Give the player the option to send messages to multiple channels
GRM.BuildMultiChannelString = function()
    local result = "";

    if #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel == 0 then
        result = DEFAULT_CHAT_FRAME.name
    else
        
        for i = 1 , #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel do
            if i == 1 then
                result = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel[i];
            else
                result = result .. "," .. GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel[i];
            end
        end
    end

    return result;
end

-- Method:          GRM.SetAllWindowScales()
-- What it Does:    Sets the scale to the saved values of the interface.
-- Purpose:         Allow flexibility in how the windows look for GRM.
GRM.SetAllWindowScales = function()
    
    GRM_UI.GRM_RosterChangeLogFrame:SetScale ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].UIScaling[1] );
    GRM_UI.GRM_MemberDetailMetaData:SetScale ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].UIScaling[2] );
    GRM_UI.GRM_ToolCoreFrame:SetScale ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].UIScaling[3] );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame:SetScale ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].UIScaling[4] );
    GRM_UI.GRM_AuditJDTool:SetScale ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].UIScaling[5] );

end

-- Method:          GRM.SlimName(string)
-- What it Does:    Removes the server name after character name.
-- Purpose:         Server name is not important in a guild of the same server since all will be server name.
GRM.SlimName = function( name )
    if name ~= nil then
        if string.find ( name , "-" , 1 ) ~= nil then
            return string.sub ( name , 1 , string.find ( name ,"-" ) - 1 );
        else
            return name;
        end
    else
        return "";
    end
end

-- Method:          GRM.FormatNameWithPlayerServer ( string )
-- What it Does:    Adds the realmName to the end of a player short name, if it is missing.
-- Purpose:         If a person is on the same server, often the server is omitted. This resolves that by adding it, which makes it easier to match in the database
GRM.FormatNameWithPlayerServer = function ( name )
    if string.find ( name , "-" ) == nil then
        name = name .. "-" .. GRM_G.realmName;
    end
    return name;
end

-- Method:          GRM.Use24HrBasedOnDefaultLanguage()
-- What it Does:    Establishes if the language uses the 24hr timestamp
-- Purpose:         On configuring a new toon, it is useful to know what timestamp format is commonly used 24hr or 12hr
GRM.Use24HrBasedOnDefaultLanguage = function()
    local result = true;
    if GRM_G.LocalizedIndex == 1 or GRM_G.LocalizedIndex == 6 then
        result = false;
    end
    return result;
end

-- Method:          GRM.GetMatureFilterNormalizedString ( int )
-- What it Does:    Returns a string with the number of special characters for mature language filter normalization
-- Purpose:         Blizz seems to have a rolling string of special characters that can be inconsistent on how it censors the curse/mature words, and so string compare 
--                  can be inconsistently comparing to different variations of special characters on server calls. This resolves that by normalizing curse word representation to the 
--                  addon and all 5 letter words will be same string, and all 4 letter words will be same string and so on.
GRM.GetMatureFilterNormalizedString = function ( numChars )
    local matureFilterTable = { "$" , "%" , "^" , "&" , "*" , "!" , "@" , "#" };
    local result = "";

    while #result < numChars do
        for i = 1 , #matureFilterTable do
            result = result .. matureFilterTable[i]; -- Add the chair and build the string...
            if #result >= numChars then              -- It is the correct size now, let's break it.
                break;
            end
        end
    end

    return result;
end

-- Method:          GRM.NormalizeMatureWords ( string )
-- What it Does:    Searches a string for Blizz's implementation of various curse word censoring and then overwrites it with a normalized curseword string
-- Purpose:         For comparing strings properly as the curse word censoring with the "Mature Langue Filter" is inconsistent from the server...
GRM.NormalizeMatureWords = function ( text )
    local matureFilterTable = { "$" , "%" , "^" , "&" , "*" , "!" , "@" , "#" };
    local index = 1;
    local count = 1;            -- Count must make it to 3 or else we break
        
    while index <= #text do
        local restart = false;
        for i = index , #text do                        -- No need to parse the final 2 as curse word must be 3 letters +
            local char = string.sub ( text , i , i );       -- Parse out the char to compare to match...
            for j = 1 , #matureFilterTable do
                if char == matureFilterTable[j] then
                    -- POTENTIAL MATCH
                    -- So far, one letter is matching! Let us determine where the next matching letter as well (word must be at least 3 letters, up to 8)
                    count = 1;
                    for r = i + 1 , #text do
                        local isMatch = false;      -- There MUST be a match or we break this and then compare to count... if it is greater than or equal to 3, then we have a curse word
                        local char2 = string.sub ( text , r , r );       -- Parse out the char to compare to match...
                        for s = 1 , #matureFilterTable do
                            if char2 == matureFilterTable[s] then
                                isMatch = true;
                                count = count + 1;
                                break;
                            end
                        end
                        if not isMatch or r == #text then
                            restart = true;
                            if count >= 3 then
                                -- MATCH FOUND!!! CURSE WORD IDENTIFIED!!!
                                text = ( string.sub ( text , 1 , i - 1 ) .. GRM.GetMatureFilterNormalizedString ( count ) .. string.sub ( text , i + count ) );
                            end
                            break;
                        end
                    end
                    break;
                end
            end
            index = index + 1;
            -- Let's break the loop and pickup where we leftoff...
            if restart then
                index = i + count;      -- We might jump a few characters here...
                break;
            end
        end
    end
    return text;
end

--------------------------------------
------ LOCALIZATION LOGIC ------------
--------------------------------------

-- Method:          GRM.L ( string , string , int (or casted int to string) , string , string )
-- What it Does:    Returns the localized string based on the hash table using the key, or the key itself if the value is set to true (meaning no localization necessary or does not exist yet)
-- Purpose:         For ease of localization so people from any region can enjoy the addon in their native tongue!!!
GRM.L = function ( key , playerName , playerName2 , num , custom1 , custom2 )
    if key ~= nil and GRM_L[key] ~= nil then
        if GRM_L[key] == true then      -- If true it has not been localized, or it is English client
            if playerName then          -- It is not nil
                key = string.gsub ( key , "{name}" , playerName );    -- insert playerName where needed - this is because in localization, for example "Arkaan's bday" in Spanish would have name at end of statement
            end
            if playerName2 then          -- It is not nil
                key = string.gsub ( key , "{name2}" , playerName2 );    -- insert playerName where needed - this is because in localization, for example "Arkaan's bday" in Spanish would have name at end of statement
            end
            if num then
                key = string.gsub ( key , "{num}" , num );
            end
            if custom1 then
                key = string.gsub ( key , "{custom1}" , custom1 );
            end
            if custom2 then
                key = string.gsub ( key , "{custom2}" , custom2 );
            end
            return key;
        else
            local result = GRM_L[key];
            if playerName then          -- It is not nil
                result = string.gsub ( result , "{name}" , playerName );    -- insert playerName where needed - this is because in localization, for example "Arkaan's bday" in Spanish would have name at end of statement
            end
            if playerName2 then          -- It is not nil
                result = string.gsub ( result , "{name2}" , playerName2 );    -- insert playerName where needed - this is because in localization, for example "Arkaan's bday" in Spanish would have name at end of statement
            end
            if num then
                result = string.gsub ( result , "{num}" , num );
            end
            if custom1 then
                result = string.gsub ( result , "{custom1}" , custom1 );
            end
            if custom2 then
                result = string.gsub ( result , "{custom2}" , custom2 );
            end
            return result
        end
    else
        if key ~= nil then
            if not GRM_G.Region == "koKR" or key ~= "SYNC USERS" then
                if key ~= "" then
                --     GRM.Report ( "GRM Warning! - Trying to localize an empty string!!! Please report to addon dev the Lua error that occurs. Thank You! This error is causing a forced Lua error so I can trace the bug. Please report the Lua error and I will fix it immediately!" )
                --     local PuposefullyUnusedToForceCodeFailLuaTrace = 5 + nil;    -- Too to force a Lua error so this can be traced.
                -- else
                    GRM.Report ( GRM.L ( "GRM WARNING!!! FAILURE TO LOAD THIS KEY: {name}\nPLEASE REPORT TO ADDON DEV! THANK YOU!" , key ) );  -- for debugging purposes.
                end
            end
        else
            error ( "Localization Key is nil" );
        end
        return key;
    end
end

-- Method:          GRM.NormalizeHitRects ( button , fontstring )
-- What it Does:    It ensures that no matter what the localization/translation, the hitRects mnatch up to the text length perfectly
-- Purpose:         Quality of life 
GRM.NormalizeHitRects = function ( checkButton , checkButtonFontstring )
    checkButton:SetHitRectInsets ( 0 , - checkButtonFontstring:GetWidth() -2 , 0 , 0 );
end

-- Method:          GRM.OrigL ( string )
-- What it Does:    Takes a hash result and returns the key. It's essentially a dictionary lookup in reverse
-- Purpose:         Some of the code needs to be localized only on the front end, but the backend code is based on some English variables
--                  This allows the localized data to be presented to the user, but on the backend to cycle back to the hash key for parse analysis.
GRM.OrigL = function ( localizedString )
    local result = localizedString;
    -- if it is not nil, then we know we already have the OrigL
    if GRM_L[localizedString] == nil then
        for key , y in pairs ( GRM_L ) do
            if y == localizedString then
                result = key;
                break;
            end
        end
    end
    return result;
end

--------------------------------------
--- DATA BACKUP AND SAVE LOGIC -------
--------------------------------------

-- Method:          GRM.Round ( float , int )
-- What it Does:    Returns a given number with the given number of requested decimals places.
-- Purpose:         Clean reporting and aesthetics...
GRM.Round = function ( num , numDecimals )
    local modifier = 10 ^ ( numDecimals or 0 );
    return math.floor ( num * modifier + 0.5 ) / modifier;
end

-- Method:          GRM.Random ( int , int )
-- What it Does:    Returns a random number with millisecond precision
-- Purpose:         Default built-in random number generator not precise enough.
GRM.Random = function ( lower , upper )
    return GRM.Round ( lower + ( upper * math.random() ) , 2 );
end

-- Method:          GRM.DeepCopyArray(array)
-- What it Does:    Makes a Deep copy, including all children, recursively, so as to create a new memory reference of the array
-- Purpose:         In Lua, you cannot just copy a table. It copies the reference and changes made to new table and references the memory to being the same, even if they have different variable names
--                  So, to truly create a unique reference to an array, so if you edit one it doesn't edit both, you need to do a true copy. This basically creates a new empty array and imports each value
--                  to the table. Backups would not be possible without this code right here.
GRM.DeepCopyArray = function( tableToCopy )
    local copy;
    if type ( tableToCopy ) == 'table' then
        copy = {};
        for orig_key , orig_value in next , tableToCopy , nil do
            copy [ GRM.DeepCopyArray ( orig_key ) ] = GRM.DeepCopyArray ( orig_value );     -- This recursive action is essentially taking every multi-D array value and it keeps digging til it builds every layer of multi-dimensional array
        end
        setmetatable ( copy , GRM.DeepCopyArray ( getmetatable ( tableToCopy ) ) );
    else
        copy = tableToCopy;         -- Imported data was not a table... just return orig. value - error protection
    end
    return copy;
end

-- Method:          GRM.AddGuildBackup ( string , string , int )
-- What it Does:    Adds a backup point of the given selected guild.
-- Purpose:         Save your database as needed.
GRM.AddGuildBackup = function( guildName , creationDate , faction )
    if creationDate ~= GRM.L ( "Unknown" ) then

        if GRM_GuildMemberHistory_Save[faction][guildName] ~= nil then
            GRM_GuildDataBackup_Save[faction][guildName].Manual = { 1 };
            GRM_GuildDataBackup_Save[faction][guildName]["Manual"].date = GRM.GetTimestamp();
            GRM_GuildDataBackup_Save[faction][guildName]["Manual"].epochDate = time();
            GRM_GuildDataBackup_Save[faction][guildName]["Manual"].members = GRM.DeepCopyArray ( GRM_GuildMemberHistory_Save[faction][guildName] );
            GRM_GuildDataBackup_Save[faction][guildName]["Manual"].formerMembers = GRM.DeepCopyArray ( GRM_PlayersThatLeftHistory_Save[faction][guildName] );
            GRM_GuildDataBackup_Save[faction][guildName]["Manual"].log = GRM.DeepCopyArray ( GRM_LogReport_Save[faction][guildName] );

            GRM.Report ( GRM.L ( "Backup Point Set for Guild \"{name}\"" , guildName ) );

        else
            GRM.Report ( GRM.L ( "Unable to properly locate guild for backup" ) );
        end

    else
        GRM.Report ( "GRM: Unable to Create Backup for a Guild With Unknown Creation Date! Log into that guild on any alt to update old database." );
    end
end

-- Method:          GRM.RemoveGuildBackup ( string , int , string , boolean )
-- What it Does:    Removes a Backup Point for the guild...
-- Purpose:         Database Backup Management
GRM.RemoveGuildBackup = function( guildName , faction , backupPoint , reportChange )
    if GRM_G.DebugEnabled then
        GRM.AddDebugMessage ( time() .. "GRM.RemoveGuildBackup()?" .. guildName .. "?" .. backupPoint );
    end
    local backupRemoved = false;
    if #GRM_GuildDataBackup_Save[ faction ][ guildName].Manual > 0 and GRM.FormatTimeStamp ( GRM_GuildDataBackup_Save[faction][guildName]["Manual"].date , true ) == backupPoint then
        GRM_GuildDataBackup_Save[ faction ][ guildName ].Manual = {};
        GRM_GuildDataBackup_Save[ faction ][ guildName ]["Manual"].date = "";
        GRM_GuildDataBackup_Save[ faction ][ guildName ]["Manual"].epochDate = 0;
        GRM_GuildDataBackup_Save[ faction ][ guildName ]["Manual"].members = {};
        GRM_GuildDataBackup_Save[ faction ][ guildName ]["Manual"].formerMembers = {};
        GRM_GuildDataBackup_Save[ faction ][ guildName ]["Manual"].log = {};
        backupRemoved = true;

    elseif #GRM_GuildDataBackup_Save[faction][guildName].Auto > 0 and GRM.FormatTimeStamp ( string.gsub ( GRM_GuildDataBackup_Save[faction][guildName]["Auto"].date , "AUTO_" , "" ) , true ) == backupPoint then
        GRM_GuildDataBackup_Save[faction][guildName].Auto = {};
        GRM_GuildDataBackup_Save[ faction ][ guildName ]["Auto"].date = "";
        GRM_GuildDataBackup_Save[ faction ][ guildName ]["Auto"].epochDate = 0;
        GRM_GuildDataBackup_Save[ faction ][ guildName ]["Auto"].members = {};
        GRM_GuildDataBackup_Save[ faction ][ guildName ]["Auto"].formerMembers = {};
        GRM_GuildDataBackup_Save[ faction ][ guildName ]["Auto"].log = {};
        backupRemoved = true;
    end

    if backupRemoved and reportChange then
        GRM.Report ( GRM.L ( "Backup Point Removed for Guild \"{name}\"" , guildName ) );
    end
end

-- Method:          GRM.LoadGuildBackup ( string , string , int , string)
-- What it Does:    Restores backup point of a guild
-- Purpose:         Database Backup Management
GRM.LoadGuildBackup = function( guildName , creationDate , faction , backupPoint )

    if GRM_G.DebugEnabled then
        GRM.AddDebugMessage ( time() .. "GRM.LoadGuildBackup()?" .. guildName .. "?" .. creationDate .. "?" .. backupPoint );
    end

    local target = "";
    if #GRM_GuildDataBackup_Save[faction][guildName]["Manual"] > 0 and GRM.FormatTimeStamp ( GRM_GuildDataBackup_Save[faction][guildName]["Manual"].date , true ) == backupPoint then
        target = "Manual";
    elseif #GRM_GuildDataBackup_Save[faction][guildName]["Auto"] > 0 and GRM.FormatTimeStamp ( string.gsub ( GRM_GuildDataBackup_Save[faction][guildName]["Auto"].date , "AUTO_" , "" ) , true ) == backupPoint then
        target = "Auto";
    end

    if GRM_GuildMemberHistory_Save[faction][guildName] ~= nil then
        -- Updating to save data...
        GRM_GuildMemberHistory_Save[faction][guildName] = GRM.DeepCopyArray ( GRM_GuildDataBackup_Save[faction][guildName][target].members );
        GRM_PlayersThatLeftHistory_Save[faction][guildName] = GRM.DeepCopyArray ( GRM_GuildDataBackup_Save[faction][guildName][target].formerMembers );
        GRM_LogReport_Save[faction][guildName] = GRM.DeepCopyArray ( GRM_GuildDataBackup_Save[faction][guildName][target].log );

        GRM.Report ( GRM.L ( "Backup Point Restored for Guild \"{name}\"" , guildName ) );
        if GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
            GRM.ClearAllFrames( false );
            GRM.PopulateMemberDetails ( GRM_G.currentName );
        end

        GRM.RefreshAllMacroToolFrames();

        if GRM_UI.GRM_AuditJDTool:IsVisible() then
            GRM.AuditRefresh( true );
        end

        if GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame:IsVisible() then
            GRM_UI.SetExportTabHighlights ( GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.TabPosition );
        end;

        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogEditBox:SetText ( "" );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogEditBox:SetText ( GRM.L ( "Search Filter" ) );  -- By clearing it and restoring it, it triggers the build log action
        
        GRM_G.changeHappenedExitScan = true;
    end
end
-- Method:          GRM.ResetAllBackups()
-- What it Does:    Wipes all backup data, but then reinitializes an index for each guild
-- Purpose:         For managing the database of guild backups
GRM.ResetAllBackups = function()
    -- Reset the backup data in case any player was messing around with it...
    for f in pairs ( GRM_GuildDataBackup_Save ) do
        for guild in pairs ( GRM_GuildDataBackup_Save[f] ) do
            if type ( guild ) == "string" then
                GRM_GuildDataBackup_Save[f][guild] = {};
                GRM_GuildDataBackup_Save[f][guild].Manual = {};
                GRM_GuildDataBackup_Save[f][guild]["Manual"].date = "";
                GRM_GuildDataBackup_Save[f][guild]["Manual"].epochDate = 0;
                GRM_GuildDataBackup_Save[f][guild]["Manual"].members = {};
                GRM_GuildDataBackup_Save[f][guild]["Manual"].formerMembers = {};
                GRM_GuildDataBackup_Save[f][guild]["Manual"].log = {};
                GRM_GuildDataBackup_Save[f][guild].Auto = {};
                GRM_GuildDataBackup_Save[f][guild]["Auto"].date = "";
                GRM_GuildDataBackup_Save[f][guild]["Auto"].epochDate = 0;
                GRM_GuildDataBackup_Save[f][guild]["Auto"].members = {};
                GRM_GuildDataBackup_Save[f][guild]["Auto"].formerMembers = {};
                GRM_GuildDataBackup_Save[f][guild]["Auto"].log = {};
            end
        end
    end

end

-- Method:          GRM.GetNumGuildiesInGuild ( string , string )
-- What it Does:    Returns the number of current guildies there are
-- Purpose:         For accurate reporting on most recent current snapshot of a guild.
GRM.GetNumGuildiesInGuild = function ( guildName , faction )
    local c = 0;

    for _ , p in pairs ( GRM_GuildMemberHistory_Save[faction][guildName] ) do
        if type ( p ) == "table" then
            c = c + 1;
        end
    end

    return c;
end

-- Method:          GRM.GetNumStoredFormerMembers ( string , string )
-- What it Does:    Returns the number of former guildies there are
-- Purpose:         For accurate reporting on most recent current snapshot of a guild.
GRM.GetNumStoredFormerMembers = function ( guildName , faction )
    local c = 0;

    for _ , p in pairs ( GRM_PlayersThatLeftHistory_Save[faction][guildName] ) do
        if type ( p ) == "table" then
            c = c + 1;
        end
    end

    return c;
end

-- Method:          GRM.AutoSetBackup()
-- What it Does:    Checks every guild in the game you have saved and sets an Auto
-- Purpose:         To help the user protect their data, an autobackup point is set...
GRM.AutoSetBackup = function()
    local needsAutoBackup = false;
    -- Ability to Enable Auto-Save function...
    -- First, determine if backup has been auto-saved already by identifying last save point.

    for f in pairs ( GRM_GuildDataBackup_Save ) do
        for guild in pairs ( GRM_GuildDataBackup_Save[f] ) do
            needsAutoBackup = false;

            if GRM_GuildDataBackup_Save[f][guild][1] ~= "" then
                if #GRM_GuildDataBackup_Save[f][guild].Auto == 0 then
                    needsAutoBackup = true;
                elseif ( time() - GRM_GuildDataBackup_Save[f][guild]["Auto"].epochDate ) >= ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].autoIntervalDays * 86400 ) then
                    needsAutoBackup = true;
                end

                if needsAutoBackup then
                    -- Now, let's save it...
                    if GRM_GuildMemberHistory_Save[f][guild] ~= nil then
                        GRM_GuildDataBackup_Save[f][guild].Auto = { 1 };
                        GRM_GuildDataBackup_Save[f][guild]["Auto"].date = ( "AUTO_" .. GRM.GetTimestamp() );
                        GRM_GuildDataBackup_Save[f][guild]["Auto"].epochDate = time();
                        GRM_GuildDataBackup_Save[f][guild]["Auto"].members = GRM.DeepCopyArray ( GRM_GuildMemberHistory_Save[f][guild] );
                        GRM_GuildDataBackup_Save[f][guild]["Auto"].formerMembers = GRM.DeepCopyArray ( GRM_PlayersThatLeftHistory_Save[f][guild] );
                        GRM_GuildDataBackup_Save[f][guild]["Auto"].log = GRM.DeepCopyArray ( GRM_LogReport_Save[f][guild] );
                    end

                end
            end
        end
    end
end

-- Method:          GRM.IsMergedRealmServer()
-- What it Does:    Returns true if the player is currently on a merged realm server
-- Purpose:         Useful to know in certain circumstances, like not relying on the guild name alone to identify guild home.
GRM.IsMergedRealmServer = function()
    local result = false;
    if #GetAutoCompleteRealms() > 0 then
        result = true;
    end
    return result
end

-- Method:          GRM.GetAllConnectedRealms()
-- What it Does:    Returns all the connected realm names in an array, and if there are none, it just returns the player's own array.
-- Purpose:         Useful for autocomplete reasons.
GRM.GetAllConnectedRealms = function()
    local realms = GetAutoCompleteRealms();
    if #realms == 0 then
        realms = { GRM_G.realmName };
    end
    return realms;
end

-- Method:          GRM.PurgeGuildFromDatabase( string , string)
-- What it Does:    Completely purges a guild from the player database... that it is not currently logged into
-- Purpose:         Cleanup old guild data from a guild the player is no longer a part of.
GRM.PurgeGuildFromDatabase = function ( guildName , faction )

    if guildName == GRM_G.guildName or guildName == GRM.SlimName ( GRM_G.guildName ) then
        GRM.Report ( "\n" .. GRM.L ( "Player Cannot Purge the Guild Data they are Currently In!!!" ) .. "\n" .. GRM.L( "To reset your current guild data type '/grm clearguild'" ) );
    else

        if GRM_GuildMemberHistory_Save[faction][guildName] ~= nil then

            GRM_GuildMemberHistory_Save[faction][guildName] = nil;
            GRM_PlayersThatLeftHistory_Save[faction][guildName] = nil;
            GRM_CalendarAddQue_Save[faction][guildName] = nil;
            GRM_LogReport_Save[faction][guildName] = nil;
            GRM_GuildDataBackup_Save[faction][guildName] = nil;
            GRM_PlayerListOfAlts_Save[faction][guildName] = nil;

            GRM.Report ( GRM.L ( "{name} has been removed from the database." , guildName ) );

        else
            -- remove the saved data if any exists as well
            -- Do a purge of the guild regardless, if it's showing up here, it means it's get lingering bad data.
            if GRM_GuildDataBackup_Save[faction][guildName] ~= nil then
                GRM_GuildDataBackup_Save[faction][guildName] = nil;
                GRM.Report ( GRM.L ( "Error: Guild Not Found..." ) );
            else
                GRM.Report ( GRM.L ( "{name} has been removed from the database." , guildName ) );
            end
        end
    end
end

--------------------------------------
-------- DEBUGGING -------------------
--------------------------------------

-- Method:          GRM.DebugLog ( int )
-- What it Does:    Prints out the Debug Log the last X number of items that occurred before logging off or disconnecting.
-- Purpose:         Occasionally disconnects happen. This will let me know what happened!
GRM.DebugLog = function ( numToShow )
    local index;
    if numToShow < 0 or #GRM_G.DebugLog - numToShow < 0 then
        index = 0;
        numToShow = #GRM_G.DebugLog;
    else
        index = #GRM_G.DebugLog - numToShow;
    end

    GRM.Report ( string.upper ( GRM.L ( "Debugger Start" )  .. ": " .. numToShow .. "/" .. #GRM_G.DebugLog ) );
    for i = index + 1 , #GRM_G.DebugLog do
        GRM.Report( GRM_G.DebugLog[i] );
    end
end

-- Method:          GRM.AddDebugMessage ( string )
-- What it Does:    Addes messages of recent events to debug log...
-- Purpose:         Debugging tracking
GRM.AddDebugMessage = function ( msg )
    -- To prevent too large of a debug log...
    if msg == "" then
        msg = "Empty Msg";
    end
    if #GRM_G.DebugLog < 250 then
        table.insert ( GRM_G.DebugLog , time() .. ": " .. msg );
    else
        local tempLog = {};
        for i = #GRM_G.DebugLog - 50 , #GRM_G.DebugLog do
            table.insert ( tempLog , time() .. ": " .. GRM_G.DebugLog[i] );
        end
        GRM_G.DebugLog = tempLog;
        table.insert ( GRM_G.DebugLog , msg );
    end
end

-- Method:          GRM.DebugMessages()
-- What it Does:    Enables debugging messages
-- Purpose:         Get rid of need of 2 sets of programs... just enable and disable.
GRM.DebugMessages = function()
    if GRM_G.DebugMsgEnabled then
        GRM_G.DebugMsgEnabled = false;
        GRM.Report ( GRM.L ( "Debugging Enabled" ) );
    else
        GRM_G.DebugMsgEnabled = true;
        GRM.Report ( GRM.L ( "Debugging Disabled" ) );
    end
end

--------------------------------------
------ GROUP METHODS AND LOGIC -------
--------------------------------------

-- Method:          GRM.GetClubEpochJoinTime ( int )
-- What it Does:    Returns the epoch stamp of when a player joined a guild or community
-- Prupose:         Useful to know exact date...
GRM.GetClubEpochJoinTime = function ( clubID )
    local info = C_Club.GetClubInfo ( clubID );
    local result = -1;
    if info then
        result = GRM.ConvertToRealEpochNumber ( info.joinTime );
    end
    return result;
end

-- Method:          GRM.GetSelectedClubID()
-- What it Does:    Returns the selected club ID - to be used as GRM expands to all communities
-- Purpose:         To pull data from specific club only.
GRM.GetSelectedClubID = function()
    return CommunitiesFrame:GetSelectedClubId();
end

-- Method:          GRM.GetStreamViewMarker ( int , int )
-- What it Does:    Returns the time as an epoch timestamp format of wher ethe cutoff of the Unread messages lies
-- Purpose          on back reading the chat stream of the guild or community, this will tell you the time your unread messages begin.
GRM.GetStreamViewMarker = function( clubID , channelID )
    local streamViewMarker = C_Club.GetStreamViewMarker ( clubID , channelID );
    local number = nil;
    if streamViewMarker then
        number = GRM.ConvertToRealEpochNumber ( streamViewMarker );
    end
    return number;
end

-- Method:          GRM.ConvertToRealEpochNumber ( Number in scientific notation, like a float, 1.52223232323e+15 )
-- What it Does:    Returns the epoch real number from a converted number in the default Lua scientific notation format.
-- Purpose:         Blizz provides a microsecond version of the epoch time stamp, which is unnecessary, and rather than rewrite all my other epoch methods, I just convert to proper epoch format here
GRM.ConvertToRealEpochNumber = function ( scientificNotedNumber )
    return math.floor ( ( tonumber ( string.format ( "%.0f" , scientificNotedNumber ) ) / 1000000 ) + 0.5 );
end

-- Method:          GRM.GetFullNameClubMember ( guid(as string) )
-- What it Does:    Appends the server to the end of the player name properly...
-- Purpose:         To append the full player name properly since it is not given by default
GRM.GetFullNameClubMember = function( memberGUID )
    local name , realm = select ( 6 , GetPlayerInfoByGUID ( memberGUID ) );
    local result = "";
    if name ~= nil and string.find ( name , "-" ) == nil then
        if realm == "" then
            result = name .. "-" .. GRM_G.realmName;
        else
            result = name .. "-" .. realm;
        end
    end
    return result;
end

-- Method:          GRM.AppendSameServerName ( string )
-- What it Does:    Adds the server name to the player's name if it is not there
-- Purpose:    AppendSameServerName = fun     In some cases you need the full name-serverName to do some actions, when the server only gives you the name of players you are on same realm with trimmed of server. This adds it back.
GRM.AppendSameServerName = function ( memberName )
    if memberName ~= nil and memberName ~= "" and string.find ( memberName , "-" ) == nil then
        memberName = memberName .. "-" .. GRM_G.realmName;
    end
    return memberName;
end

-- Method:          GRM.GetNumGuildies()
-- What it Does:    Returns the int number of total toons within the guild, including main/alts
-- Purpose:         For book-keeping and tracking total guild membership.
--                  Overall, this is mostly redundant as a simple GetNumGuildMembers() call is the same thing, however, this is just a tech Demo
--                  as a coding example of how to pull info and return it in your own function.
--                  A simple "GetNumGuildMembers()" would result in the same result in less steps. This is just more explicit to keep it within the style of the functions of the addon.
GRM.GetNumGuildies = function()
    return select ( 1 , GetNumGuildMembers() );
end

-- Method:          GRM.PlayerIsInGuild ( string )
-- What it Does:    Returns true if the player is in the guild
-- Purpose:         Simple Query check
GRM.PlayerIsInGuild = function( name )
    if GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name] ~= nil then
        return true;
    else
        return false;
    end
end

-- Method:          GRM.GetNumMains()
-- What it Does:    Returns the total number of players designated as "Main" in the guild
-- Purpose:         Mainly for audit log stat reporting.  
GRM.GetNumMains = function()
    local count = 0;
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            if player.isMain then
                count = count + 1;
            end
        end
    end
    return count;
end

-- Method:          GetListOfGuildRanks( bool )
-- What it Does:    Gets a list of all rank names for dropdown menu
-- Purpose:         For building the macro dropdown menu for destination rank
GRM.GetListOfGuildRanks = function( includeLeader , descending , asString , test )
    local numRanks = GuildControlGetNumRanks(); -- minus 1 because we are not including the guild leader
    local result = {};
    local delimiter = "||";

    local c = 1;
    if not includeLeader then
        c = 2;
    end

    if asString then
        result = "";
    end

    if descending then
        for i = c , numRanks do

            if asString then
                if i == numRanks then
                    result = result .. GuildControlGetRankName ( i );
                else
                    result = result .. GuildControlGetRankName ( i ) .. delimiter;
                end
            else
                table.insert ( result , GuildControlGetRankName ( i ) );
            end
        end
    else
        for i = numRanks , c , -1 do
            if asString then
                if i == c then
                    result = result .. GuildControlGetRankName ( i );
                else
                    result = result .. GuildControlGetRankName ( i ) .. delimiter;
                end
            else
                table.insert ( result , GuildControlGetRankName ( i ) );
            end
        end
    end

    return result;
end

-- Method:          GetNumberPatternMatches ( string , string )
-- What it Does:    Returns the integer count number of matches of a pattern, like how many commas in a string
-- Purpose:         Easier to parse some strings with simpler logic.
GRM.GetNumberPatternMatches = function ( text , pattern )
    return select ( 2 , string.gsub ( text , pattern , "" ) );
end

-- Method:          GRM.CreateGuildCreationDatePattern()
-- What it Does:    Creates the guild Creation Date Pattern
-- Purpose:         Easier parse the creation date without repetitive work.
GRM.CreateGuildCreationDatePattern = function()
    local pattern = GUILD_INFO_TEMPLATE;

    if GRM_G.BuildVersion >= 80000 or GRM_G.Region == "deDE" or GRM_G.Region == "frFR" or GRM_G.Region == "koKR" then
        for i = 1 , 5 do
            pattern = string.gsub ( pattern  , "%%" .. i .. "$d" , "(%%d+)" );
        end
    else
        pattern = string.gsub ( pattern  , "%%d" , "(%%d+)" );
    end

    return pattern;
end

-- Method:          GRM.SetSystemMessageFilter ( self , string , string )
-- What it Does:    Starts tracking the system messages for filtering. This is only triggered on the audit frame initialization or if a player has left the guild
-- Purpose:         To control system message spam when doing server inquiries
GRM.SetSystemMessageFilter = function ( _ , _ , msg , ... )
    local result = false;
    GRM_G.SystemMessageTest = true;

    if time() - GRM_G.IsOnLogonDelay > 1 and not GRM_G.TempBanSystemMessage then
        -- GUILD INFO FILTER (GuildInfo())
        if ( GRM_G.MsgFilterDelay and ( string.find ( msg , GRM.L ( "Guild: " ) ) ~= nil or string.find ( msg , GRM.L ( "Guild created " ) ) ~= nil ) ) then       -- These may need to be localized. I have not yet tested if other regions return same info. It IS system info.
            if string.find ( msg , GRM.L ( "Guild created " ) ) ~= nil and ( ( time() - GRM_G.SystemMsgThrottle ) > 1 ) then
                GRM_G.SystemMsgThrottle = time();
                GRM_G.CreationDatePattern = GRM_G.CreationDatePattern or GRM.CreateGuildCreationDatePattern();

                local a , b , c , _ , numUniqueAccounts = string.match ( msg , GRM_G.CreationDatePattern );
                local month , day , year;
                -- a , b , c can be either day, month, or year, depending on the Region formatting for the note.
                -- string.match ( "Guild created 8-31-2019, 4 players, 4 accounts" , GRM_G.CreationDatePattern )
                if GRM_G.Region == "deDE" or GRM_G.Region == "esES" or GRM_G.Region == "esMX" or GRM_G.Region == "frFR" then
                    day = a;
                    month = b;
                    year = c;
                elseif GRM_G.Region == "enUS" or GRM_G.Region == "itIT" or GRM_G.Region == "ptBR" or GRM_G.Region == "ruRU" or GRM_G.Region == "zhCN" or GRM_G.Region == "zhTW" then
                    day = b;
                    month = a;
                    year = c;
                elseif GRM_G.Region == "koKR" then
                    day = c;
                    month = b;
                    year = a;
                end

                -- On first logging in, there can be an error and not be able to load.
                if day ~= "0" then
                    
                    numUniqueAccounts = tonumber ( numUniqueAccounts );
                    
                    -- For auto-main tagging... detect the change here!
                    if numUniqueAccounts > GRM_G.numAccounts and GRM_G.numAccounts ~= 0 then
                        GRM_G.DesignateMain = true;
                        C_Timer.After ( 10.1 , function()
                            GRM_G.DesignateMain = false;
                        end);
                    end
                    
                    GRM_G.numAccounts = numUniqueAccounts;

                    local date = day .. "-" .. month .. "-" .. year;

                    if GRM_G.guildCreationDate == "" or GRM_G.guildCreationDate ~= date then
                        GRM_G.guildCreationDate = date;
                    end

                    if GRM_G.guildCreationDate ~= "" and date ~= GRM_G.guildCreationDate then
                        -- This means the wrong date was set and this is re-changing it.
                        GRM_G.changeHappenedExitScan = true;
                    end

                end
            end
            result = true;

        -- Player Not Found when trying to add to friends list message
        elseif ( GRM_G.MsgFilterDelay or GRM_G.MsgFilterDelay2 ) and ( msg == GRM.L ( "Player not found." ) or string.find ( msg , GRM.L ( "added to friends" ) ) ~= nil or string.find ( msg , GRM.L ( "is already your friend" ) ) ~= nil ) then
            result = true;

        -- Normal System message... Let's add the main tags...
        elseif not GRM_G.MainNameSystemMsgControl and string.find ( msg , GRM.L ( "joined the guild." ) ) == nil and string.find ( msg , GRM.L ( "has Left the guild" ) ) == nil then  -- No need to add a tag if they just joined... as they have no tag, and their profile is not yet generated. Addon will see them as a non-guildie the first instant.
            if ( time() - GRMsyncGlobals.timeAtLogin ) > 5 and ( ( GRM_G.MainTagHexCode ~= "" and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showMainName ) or GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].colorizeNames ) then
                if string.find ( msg , GRM.L ( "has come online." ) ) ~= nil then
                    msg = GRM.AddMainTagToComeOnlineSystemMessage ( msg , false );
                elseif string.find ( msg , GRM.L ( "has gone offline." ) ) ~= nil then
                    msg = GRM.AddMainTagToGoneOfflineSystemMessage ( msg );
                end
            end
        end
    else
        result = true;
    end
    -- Re-evaluate message controls
    GRM.SystemMessageHookControl()

    return result , msg , ... ;
end

-- Method:          GRM.AddMainTagToComeOnlineSystemMessage ( string )
-- What it Does:    Adds the main/alt tags to the system messages for coming online.
-- Purpose:         Alt management feature!
GRM.AddMainTagToComeOnlineSystemMessage = function( msg , isInGuild )
    local name, msgID = select ( 3 , msg:find ("|Hplayer:([^:]*)(.-)|h%[.-%]|h" ) );
    local fullName = GRM.AppendSameServerName ( name );
    local includeMainTag = ( GRM_G.MainTagHexCode ~= "" and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showMainName );
    local result = "";
    
    if GRM_G.guildName ~= "" then
        -- Is Player in the guild?
        if isInGuild or GRM.PlayerIsInGuild ( fullName ) then
            local mainName = "";
            local hexCode = "";
            local mainColoring = "";
            local systemMsgHex = "|CFFFFFF00";  -- For display controls further...
            local mainDisplay = "";
            local necessaryGap = "";
            local hasAlts = false;
            
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].useMainTag then
                -- Just add main tag...
                msg = GRM_G.MainTagHexCode .. mainDisplay .. "|r " .. msg;
            end
            if includeMainTag then
                mainName = GRM.GetMainName ( fullName , false );
                mainColoring , hasAlts = GRM.GetStringClassColorByName ( mainName , false );
                mainDisplay = GRM.GetMainTags ( false , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].mainTagIndex );
                
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].useMainTag or hasAlts then
                    necessaryGap = " ";
                else
                    mainDisplay = "";
                end
            end

            -- Only color it if needed.
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].colorizeNames then
                hexCode , hasAlts = GRM.GetStringClassColorByName ( fullName , false );
            end        

            if mainName ~= "" and mainName ~= fullName then
                mainName = systemMsgHex .. "(|r" .. mainColoring .. GRM.SlimName ( mainName ) .. "|r" .. systemMsgHex .. ")|r " .. GRM_G.MainTagHexCode .. mainDisplay .. "|r";
            elseif mainName == fullName then
                if ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].useMainTag or hasAlts ) then
                    mainName = GRM_G.MainTagHexCode .. mainDisplay .. "|r";
                else
                    mainName = "";
                end
            end

            result = systemMsgHex .. string.format ( "%s|Hplayer:%s|h[%s]|h|r" , hexCode , name , name ) .. necessaryGap .. mainName .. systemMsgHex .. " " .. GRM.L ( "has come online." );
        else
            -- Send it back regular format...
            local notInGuildTag = GRM.GetNonGuildieTags ( false , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].mainTagIndex );
            result = string.gsub ( msg , "(|Hplayer:.-|h%[.-%]|h)" , string.format ( "|Hplayer:%s%s|h[%s]|h" , name , msgID , name .. " " .. GRM_G.MainTagHexCode .. notInGuildTag .. "|r" ) );
        end
    else
        result = msg;
    end

    return result;
end

-- Method:          GRM.AddMainTagToGoneOfflineSystemMessage ( string )
-- What it Does:    Adds the main tag to the system message announce when a player goes offline and colorizes their names
-- Purpose:         Quality of life information.
GRM.AddMainTagToGoneOfflineSystemMessage = function ( msg )
    local breakIndex = string.find ( msg , " " );                  -- This format fits almost all
    if breakIndex == nil then
        breakIndex = string.find ( msg , "下線了。" );              -- Taiwanese
        if breakIndex == nil then
            breakIndex = string.find ( msg , "下线了。" );          -- Mandarin
        end
    end

    local fullName = GRM.AppendSameServerName ( string.sub ( msg , 1 , breakIndex - 1 ) );
    local includeMainTag = ( GRM_G.MainTagHexCode ~= "" and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showMainName );
    local nameWithTag = "";
    local finalNameFormat = "";
    local hexCode = "";
    local necessaryTag = "";
    local hasAlts = false;

    if includeMainTag then
        nameWithTag , hasAlts = GRM.GetNameWithMainTags ( fullName , true , true , false , true );
        

        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].useMainTag or hasAlts then
            necessaryTag = "|r";
        end
    end

    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].colorizeNames then
        hexCode = GRM.GetStringClassColorByName ( fullName , false );
        necessaryTag = "|r";
    end

    if includeMainTag then
        if ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].useMainTag or hasAlts ) then
            finalNameFormat = nameWithTag;
        else
            finalNameFormat = GRM.SlimName ( fullName );
        end
    else
        finalNameFormat = GRM.SlimName ( fullName );
    end

    return hexCode .. finalNameFormat .. necessaryTag .. string.sub ( msg , breakIndex );
end

-- Method:          GRM.ConfigureSystemMessages()
-- What it Does:    Checks all active windows to see if at least one has system messag
-- Purpose:         To be able to auto-manage system messages behind the scenes.
GRM.ConfigureSystemMessages = function()
    local tempNumber = tonumber ( CURRENT_CHAT_FRAME_ID );
    local result = false;
    local nameMatchingID = 0;
    local reportChannels = {};
    if #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel == 0 then
        reportChannels = { DEFAULT_CHAT_FRAME.name };
    else
        reportChannels = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportChannel;
    end
    local name = "";


    for i = 1 , FCF_GetNumActiveChatFrames() do
        name = GetChatWindowInfo ( i );
        CURRENT_CHAT_FRAME_ID = i;
        
        -- If customChannel then
        if nameMatchingID == 0 then
            for j = #reportChannels , 1 , -1 do
                if reportChannels[j] == name then
                    nameMatchingID = i;
                    break;
                end
            end
        end

        if IsListeningForMessageType ( "SYSTEM" ) then
            result = true;
            break;
        end
    end

    -- Ok setting the system messages on at least to configured window.
    if not result and nameMatchingID ~= 0 then
        CURRENT_CHAT_FRAME_ID = nameMatchingID;
        ToggleChatMessageGroup ( true , "SYSTEM" );
    end

    -- Reset back to default
    CURRENT_CHAT_FRAME_ID = tempNumber;
    return result;
end

-- Method:          GRM.SetGuildInfoDetails()
-- Purpose:         Calls the server info on the guild and parses out the number of exact unique accounts are in the guild. It also filters the chat msg to avoid chat spam, then unfilters it immediately after
--                  as a Quality of Life feature so the user can manually continue to call as needed.
-- Purpose:         It is useful information to know how many unique acocunts are in the guild. This particularly is useful when comparing how many "mains" there 
--                  are on the audit window...
GRM.SetGuildInfoDetails = function()
    GRM_G.MsgFilterDelay = true;         -- Resets the 1 second timer upon calling this method for the chat spam blocking. This ensures player manual calls are visual, but code calls are filtered.
    if not GRM_G.MsgFilterEnabled then   -- Gate to ensure this only is registered one time. This is also controlled here so as to not waste resources by being called needlessly if player never checks audit window
        GRM_G.MsgFilterEnabled = true;   -- Establishing boolean gate so it is only registered once.
        ChatFrame_AddMessageEventFilter ( "CHAT_MSG_SYSTEM" , GRM.SetSystemMessageFilter );
    end


    -- Enable System Messages if Disabled.
    GRM.ConfigureSystemMessages();

    GuildInfo();
    -- This should only be blocked momentarily.
    C_Timer.After ( 1 , function()
        GRM_G.MsgFilterDelay = false;
    end);
end

-- Method:          GRM.GetAllGuildiesOnline( boolean )
-- What it Does:    Returns a table of names of all guildies that are currently online in the guild
-- Purpose:         Group management info and reporting. Pretty much some UI features, but possibly will be expanded upon.
GRM.GetAllGuildiesOnline = function( fullNameNeeded )
    if not GRM.IsCalendarEventEditOpen() then
        GRM.GuildRoster();
    end
    local listOfNames = {};
    for i = 1 , GRM.GetNumGuildies() do
        local name , _ , _ , _ , _ , _ , _ , _ , online = GetGuildRosterInfo ( i );
        if online then
            if name ~= nil then
                if not fullNameNeeded then
                    table.insert ( listOfNames , GRM.SlimName ( name) );
                else
                    table.insert ( listOfNames , name );
                end
            end
        end
    end
    return listOfNames;
end

-- Method:          GRM.GetAuditLinePlayervalues ( array )
-- What it Does:    Collections the JoinDateText, the Promo, and the main Status text for use of the audit window
-- Purpose:         Easily callable function for reuse in parsing player data for the audit system.
GRM.GetAuditLinePlayervalues = function ( player , isComplete )
    local joinDate , promoDate , mainStatus = "" , "" , "";
    local classColors = {};
    
    -- Join date
    if #player.joinDate == 0 then
        if player.joinDateUnknown then
            joinDate = GRM.L ( "Unknown" );
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].unknownIsComplete then
                isComplete = false;
            end
        else
            joinDate = GRM.L ( "No Date Set" );
            isComplete = false;
        end
    else
        joinDate = GRM.DateUntrustedTag ( player.verifiedJoinDate ) .. GRM.FormatTimeStamp ( player.joinDate[#player.joinDate] );
    end

    -- Promo Date
    if player.rankHistory[1][3] == 0 then
        if player.promoteDateUnknown then
            promoDate = GRM.L ( "Unknown" );
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].unknownIsComplete then
                isComplete = false;
            end
        else
            promoDate = GRM.L ( "No Date Set" );
            isComplete = false;
        end
    else
        promoDate = GRM.DateUntrustedTag ( player.verifiedPromoteDate ) .. GRM.FormatTimeStamp ( player.rankHistory[#player.rankHistory][2] );
    end

    -- Main or Alt
    if player.isMain then
        mainStatus = ( GRM.L ( "Main" ) );
    else
        -- Ok, they are not the main... do they have alts? If they have alts, we should see if one of them is listed as main.
        if #player.alts > 0 then
            local mainIsFound = false;
            for m = 1 , #player.alts do
                if player.alts[m][5] then
                    mainIsFound = true;
                    break;
                end
            end
            -- No one is listed as "main" in alt grouping.
            if not mainIsFound then
                mainStatus = GRM.L ( "Main or Alt?" ) ;
                isComplete = false;
            else
                mainStatus = GRM.L ( "Alt" );
            end
        else
            mainStatus = GRM.L ( "Main or Alt?" );
            isComplete = false;
        end
    end

    -- Class Color
    classColors = GRM.GetClassColorRGB ( player.class );

    -- Birthdate
    local birthDate = "";

    if player.events[2][1][1] == 0 then
        if player.birthdayUnknown then
            birthDate = GRM.L ( "Unknown" );
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].unknownIsComplete then
                isComplete = false;
            end
        else
            birthDate = GRM.L ( "No Date Set" );
            isComplete = false;
        end
    else
        birthDate = GRM.FormatTimeStamp ( { player.events[2][1][1] , player.events[2][1][2] , player.events[2][1][3] } , false , true  );
    end

    -- Whether to even bother including them in the audit
    if not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeBirthdaysInAudit then
        isComplete = false;
    end
   
    return joinDate , promoDate , mainStatus , isComplete , classColors , birthDate;
end


-- Method:          GRM.GetAllGuildiesInOrder ( boolean , boolean )
-- What it Does:    Returns a sorted string array of all guildies
-- Purpose:         Useful to have an alphabetized list of guildies :)
 GRM.GetAllGuildiesInOrder = function( fullNameNeeded , fromAtoZ )
    if not GRM.IsCalendarEventEditOpen() then
        GRM.GuildRoster();
    end
    local listOfGuildies = {};
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    local joinDate = "";
    local promoDate = "";
    local mainStatus = "";
    local isComplete = true;
    local classColors = {};
    local birthDate = "";
    local name = "";

    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            -- proper name format
            name = player.name;

            if not fullNameNeeded then
                name = GRM.SlimName ( name );
            end

            joinDate , promoDate , mainStatus , isComplete , classColors , birthDate = GRM.GetAuditLinePlayervalues ( player , isComplete );

            table.insert ( listOfGuildies , { name , joinDate , promoDate , mainStatus , classColors , birthDate } );
        end
    end

    sort ( listOfGuildies , function ( a , b ) return a[1] < b[1] end );

    if not fromAtoZ then
        local tempList = {};
        for i = #listOfGuildies , 1 , -1 do
            table.insert ( tempList , listOfGuildies[i] );
        end
        listOfGuildies = tempList;
    end
    return listOfGuildies , isComplete;
end

-- Method:          GRM.GetAllGuildiesInJoinDateOrder ( boolean , boolean )
-- What it Does:    Returns a sorted string array of all guildies in either ascending or descending order of when they joined the guild.
-- Purpose:         For sorting the audit window and keeping track of guildies...
GRM.GetAllGuildiesInJoinDateOrder = function ( fullNameNeeded , newFirst )
    local result = {};
    local listOfGuildiesWithDates = {};
    local listOfGuildiesWithUnknownDates = {};
    local name = "";
    local joinDate = "";
    local promoDate = "";
    local mainStatus = "";
    local isComplete = true;
    local classColors = {};
    local birthDate = "";
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];

    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            joinDate , promoDate , mainStatus , isComplete , classColors , birthDate = GRM.GetAuditLinePlayervalues ( player , isComplete );

            if #player.joinDate > 0 then
                -- find a proper place to sort
                if #listOfGuildiesWithDates == 0 then                               -- the first one can be a straight insert
                    table.insert ( listOfGuildiesWithDates , { player.name , player.joinDateEpoch[#player.joinDateEpoch] , joinDate , promoDate , mainStatus , classColors , birthDate } );
                else
                    -- parse through the dates, new First... (number will be larger)
                    local j = 1;
                    while j <= #listOfGuildiesWithDates and player.joinDateEpoch[#player.joinDateEpoch] < listOfGuildiesWithDates[j][2] do
                        j = j + 1;
                    end
                    if j == #listOfGuildiesWithDates + 1 then
                        table.insert ( listOfGuildiesWithDates , { player.name , player.joinDateEpoch[#player.joinDateEpoch] , joinDate , promoDate , mainStatus , classColors , birthDate } );
                    else
                        table.insert ( listOfGuildiesWithDates , j , { player.name , player.joinDateEpoch[#player.joinDateEpoch] , joinDate , promoDate , mainStatus , classColors , birthDate } );
                    end
                end
            else
                table.insert ( listOfGuildiesWithUnknownDates , { player.name , joinDate , promoDate , mainStatus , classColors , birthDate } );
            end
        end
    end

    -- Sort the unknowns to be added at the end
    sort ( listOfGuildiesWithUnknownDates , function ( a , b ) return a[1] < b[1] end );

    if not newFirst then
        -- need to reverse
        for i = #listOfGuildiesWithDates , 1 , -1 do
            name = listOfGuildiesWithDates[i][1];
            if not fullNameNeeded then
                name = GRM.SlimName ( name );
            end
            
            table.insert ( result , { name , listOfGuildiesWithDates[i][3] , listOfGuildiesWithDates[i][4] , listOfGuildiesWithDates[i][5] , listOfGuildiesWithDates[i][6] , listOfGuildiesWithDates[i][7]} );

        end
    else
        for i = 1 , #listOfGuildiesWithDates do
            name = listOfGuildiesWithDates[i][1];
            if not fullNameNeeded then
                name = GRM.SlimName ( name );
            end

            table.insert ( result , { name , listOfGuildiesWithDates[i][3] , listOfGuildiesWithDates[i][4] , listOfGuildiesWithDates[i][5] , listOfGuildiesWithDates[i][6] , listOfGuildiesWithDates[i][7] } );
        end
    end
    
    -- let's add the sorted unknowns to the end now as well.
    for i = 1 , #listOfGuildiesWithUnknownDates do

        name = listOfGuildiesWithUnknownDates[i][1];
        if not fullNameNeeded then
            name = GRM.SlimName ( name );
        end
        
        table.insert ( result , { name , listOfGuildiesWithUnknownDates[i][2] , listOfGuildiesWithUnknownDates[i][3] , listOfGuildiesWithUnknownDates[i][4] , listOfGuildiesWithUnknownDates[i][5] , listOfGuildiesWithUnknownDates[i][6] } );
    end
    return result , isComplete;
end

-- Method:          GRM.GetAllGuildiesInPromoDateOrder ( boolean , boolean )
-- What it Does:    Returns a sorted string array of all guildies in either ascending or descending order of when they were promoted last in the guild.
-- Purpose:         For sorting the audit window and keeping track of guildies...
GRM.GetAllGuildiesInPromoDateOrder = function ( fullNameNeeded , newFirst )
    local result = {};
    local listOfGuildiesWithDates = {};
    local listOfGuildiesWithUnknownDates = {};
    local name = "";
    local joinDate = "";
    local promoDate = "";
    local mainStatus = "";
    local isComplete = true;
    local classColors = {};
    local birthDate = "";

    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];

    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            joinDate , promoDate , mainStatus , isComplete , classColors , birthDate = GRM.GetAuditLinePlayervalues ( player , isComplete );

            if player.rankHistory[1][3] > 0 then
                -- find a proper place to sort
                local timestamp = GRM.TimeStampToEpoch ( "Joined: " .. player.rankHistory[#player.rankHistory][2] , true );

                if #listOfGuildiesWithDates == 0 then                               -- the first one can be a straight insert
                    table.insert ( listOfGuildiesWithDates , { player.name , timestamp , joinDate , promoDate , mainStatus , classColors , birthDate } );
                else
                    -- parse through the dates, new First... (number will be larger)
                    local j = 1;
                    while j <= #listOfGuildiesWithDates and timestamp < listOfGuildiesWithDates[j][2] do
                        j = j + 1;
                    end
                    if j == #listOfGuildiesWithDates + 1 then
                        table.insert ( listOfGuildiesWithDates , { player.name , timestamp , joinDate , promoDate , mainStatus , classColors , birthDate } );
                    else
                        table.insert ( listOfGuildiesWithDates , j , { player.name , timestamp , joinDate , promoDate , mainStatus , classColors , birthDate } );
                    end
                end
            else
                table.insert ( listOfGuildiesWithUnknownDates , { player.name , joinDate , promoDate , mainStatus , classColors , birthDate } );
            end
        end
    end

    -- Sort the unknowns to be added at the end
    sort ( listOfGuildiesWithUnknownDates , function ( a , b ) return a[1] < b[1] end );

    if not newFirst then
        -- need to reverse
        for i = #listOfGuildiesWithDates , 1 , -1 do

            name = listOfGuildiesWithDates[i][1];
            if not fullNameNeeded then
                name = GRM.SlimName ( name );
            end
            table.insert ( result , { name , listOfGuildiesWithDates[i][3] , listOfGuildiesWithDates[i][4] , listOfGuildiesWithDates[i][5] , listOfGuildiesWithDates[i][6] , listOfGuildiesWithDates[i][7] } );

        end
    else
        for i = 1 , #listOfGuildiesWithDates do

            name = listOfGuildiesWithDates[i][1];
            if not fullNameNeeded then
                name = GRM.SlimName ( name );
            end
            table.insert ( result , { name , listOfGuildiesWithDates[i][3] , listOfGuildiesWithDates[i][4] , listOfGuildiesWithDates[i][5] , listOfGuildiesWithDates[i][6] , listOfGuildiesWithDates[i][7] } );

        end
    end
    
    -- let's add the sorted unknowns to the end now as well.
    for i = 1 , #listOfGuildiesWithUnknownDates do
        name = listOfGuildiesWithUnknownDates[i][1];
        if not fullNameNeeded then
            name = GRM.SlimName ( name );
        end
        
        table.insert ( result , { name , listOfGuildiesWithUnknownDates[i][2] , listOfGuildiesWithUnknownDates[i][3] , listOfGuildiesWithUnknownDates[i][4] , listOfGuildiesWithUnknownDates[i][5] , listOfGuildiesWithUnknownDates[i][6] } );
    end
    return result , isComplete;
end

-- Method:          GRM.GetAllMainsAndAltsInOrder ( boolean )
-- What it Does:    Returns the guild roster sorted with either mains first or alts alphebatized, then alphabetizes the rest.
-- Purpose:         Auditing the roster and sorting!
GRM.GetAllMainsAndAltsInOrder = function ( mainsFirst )
    local result = {};
    local listOfMains = {};
    local listOfAlts = {};
    local listOfNeither = {};
    local joinDate = "";
    local promoDate = "";
    local mainStatus = "";
    local isComplete = true;
    local classColors = {};
    local birthDate = "";

    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];

    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            joinDate , promoDate , mainStatus , isComplete , classColors , birthDate = GRM.GetAuditLinePlayervalues ( player , isComplete );

            if player.isMain then
                table.insert ( listOfMains , { player.name , joinDate , promoDate , mainStatus , classColors , birthDate } );     -- Can just add if main.

            elseif #player.alts > 0 then                       -- if not main, but you do have alts, scan through the alts to see if they are a main.

                local mainFound = false;
                for j = 1 , #player.alts do
                    if player.alts[j][5] then
                        mainFound = true;
                        break;
                    end
                end
                if mainFound then
                    table.insert ( listOfAlts , { player.name , joinDate , promoDate , mainStatus , classColors , birthDate } );
                else
                    table.insert ( listOfNeither , { player.name , joinDate , promoDate , mainStatus , classColors , birthDate } );
                end
            else
                table.insert ( listOfNeither , { player.name , joinDate , promoDate , mainStatus , classColors , birthDate } );
            end
        end
    end

    sort ( listOfMains , function ( a , b ) return a[1] < b[1] end );
    sort ( listOfAlts , function ( a , b ) return a[1] < b[1] end );
    sort ( listOfNeither , function ( a , b ) return a[1] < b[1] end );

    -- Combine the tables...
    if mainsFirst then
        result = listOfMains;
        for i = 1 , #listOfAlts do
            table.insert ( result , listOfAlts[i] );
        end
    else
        result = listOfAlts;
        for i = 1 , #listOfMains do
            table.insert ( result , listOfMains[i] );
        end
    end

    for i = 1 , #listOfNeither do
        table.insert ( result , listOfNeither[i] );
    end
    return result , isComplete;
end

-- Method:          GRM.GetAllGuildiesByBirthdayDateOrder ( boolean , boolean )
-- What it Does:    Returns a sorted string array of all guildies in either ascending or descending order of when their birthday is
-- Purpose:         For sorting the audit window and keeping track of guildies...
GRM.GetAllGuildiesByBirthdayDateOrder = function ( fullNameNeeded , newFirst )
    local result = {};
    local listOfGuildiesWithDates = {};
    local listOfGuildiesWithUnknownDates = {};
    local name = "";
    local joinDate = "";
    local promoDate = "";
    local mainStatus = "";
    local isComplete = true;
    local classColors = {};
    local birthdate = "";

    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];

    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            joinDate , promoDate , mainStatus , isComplete , classColors , birthdate = GRM.GetAuditLinePlayervalues ( player , isComplete );
            
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeBirthdaysInAudit then
                
                if player.events[2][1][1] ~= 0 then
                    -- find a proper place to sort
                    local timestamp = daysBeforeMonthEnum [ tostring ( player.events[2][1][2] ) ] + player.events[2][1][1];

                    if #listOfGuildiesWithDates == 0 then                               -- the first one can be a straight insert
                        table.insert ( listOfGuildiesWithDates , { player.name , timestamp , joinDate , promoDate , mainStatus , classColors , birthdate } );
                    else
                        -- parse through the dates, new First... (number will be larger)
                        local j = 1;
                        while j <= #listOfGuildiesWithDates and timestamp < listOfGuildiesWithDates[j][2] do
                            j = j + 1;
                        end
                        if j == #listOfGuildiesWithDates + 1 then
                            table.insert ( listOfGuildiesWithDates , { player.name , timestamp , joinDate , promoDate , mainStatus , classColors , birthdate } );
                        else
                            table.insert ( listOfGuildiesWithDates , j , { player.name , timestamp , joinDate , promoDate , mainStatus , classColors , birthdate } );
                        end
                    end
                else
                    table.insert ( listOfGuildiesWithUnknownDates , { player.name , joinDate , promoDate , mainStatus , classColors , birthdate } );
                end
            else
                -- Birthdays are disabled...
                table.insert ( listOfGuildiesWithUnknownDates , { player.name , joinDate , promoDate , mainStatus , classColors , birthdate } );
            end
        end
    end

    -- Sort the unknowns to be added at the end
    sort ( listOfGuildiesWithUnknownDates , function ( a , b ) return a[1] < b[1] end );

    if not newFirst then
        -- need to reverse
        for i = #listOfGuildiesWithDates , 1 , -1 do

            name = listOfGuildiesWithDates[i][1];
            if not fullNameNeeded then
                name = GRM.SlimName ( name );
            end
            table.insert ( result , { name , listOfGuildiesWithDates[i][3] , listOfGuildiesWithDates[i][4] , listOfGuildiesWithDates[i][5] , listOfGuildiesWithDates[i][6] , listOfGuildiesWithDates[i][7] } );

        end
    else
        for i = 1 , #listOfGuildiesWithDates do

            name = listOfGuildiesWithDates[i][1];
            if not fullNameNeeded then
                name = GRM.SlimName ( name );
            end
            table.insert ( result , { name , listOfGuildiesWithDates[i][3] , listOfGuildiesWithDates[i][4] , listOfGuildiesWithDates[i][5] , listOfGuildiesWithDates[i][6] , listOfGuildiesWithDates[i][7] } );

        end
    end
    
    -- let's add the sorted unknowns to the end now as well.
    for i = 1 , #listOfGuildiesWithUnknownDates do
        name = listOfGuildiesWithUnknownDates[i][1];
        if not fullNameNeeded then
            name = GRM.SlimName ( name );
        end
        
        table.insert ( result , { name , listOfGuildiesWithUnknownDates[i][2] , listOfGuildiesWithUnknownDates[i][3] , listOfGuildiesWithUnknownDates[i][4] , listOfGuildiesWithUnknownDates[i][5] , listOfGuildiesWithUnknownDates[i][6] } );
    end
    return result , isComplete;
end

-- Method:          GRM.GetListOfGuildies( bool )
-- What it Does:    Returns a list of all current members of the guild in alphabetical order
-- Purpose:         Occasionally you want a list of guild members.
GRM.GetListOfGuildies = function( slimName )
    local list = {};
    local name;

    for i = 1 , GetNumGuildMembers() do
        name = GetGuildRosterInfo ( i );

        if slimName then
            name = GRM.SlimName ( name );
        end

        table.insert ( list , name );
    end
    sort ( list );

    return list;
end

-- Method:          GRM.GetAllCurrentAndFormerGuildies()
-- What it Does:    Collects the names and class of every player currently in the guild, formerly in the guild in database, and sorts the 2D array alphabetically
-- Purpose:         This will be useful when adding people manually to ban list for auto-complete
GRM.GetAllCurrentAndFormerGuildies = function()
    local result = {};
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];

    -- First, grab those currently in the guild
    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            table.insert ( result , { player.name , player.class } );
        end
    end

    -- Nemembert, grab those not in the guild
    guildData = GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            table.insert ( result , { player.name , player.class } );
        end
    end
    -- Now we sort "Complemember" table
    sort ( result , function ( a , b ) return a[1] < b[1] end );    -- Alphabetizing it for easier parsing for buttontemembert updating. - This sorts the first indemember of the 2D array

    return result;
end

-- NOTE: This is assuming 2D arrays and match is at index [i][1]
-- Method:          GRM.GetAutoCompleteMatches ( array , string )
-- What it Does:    This scans through the given list and returns a new list with only the string matched items.
-- Purpose:         Useful for auto-complete filtering.
GRM.GetAutoCompleteMatches = function ( list , key )
    local result = {};
    if key == "" then
        return result;
    end
    key = string.lower ( key );
    
    for i = 1 , #list do
        if string.find ( string.lower ( list[i][1] ) , key , 1 , true ) then
            table.insert ( result , list[i] );
        end
    end

    return result;
end

-- Method:          GRM.GetNumGuildiesOnline()
-- What it Does:    Returns the int number of players currently online, with option to include those only on mobile, but not physically in the game, or not.
-- Purpose:         So on mouseover, the index on the roster call can be determined properly as online people are indexed first.
GRM.GetNumGuildiesOnline = function()
    return select ( 2 , GetNumGuildMembers() );
end

-- Method:          GRM.GetGuildMemberRankID ( string )
-- What it does:    Returns the rank index of the given player's name, or 0 if unable to find player
-- Purpose:         Rank needs to be known in certain circumstances, like knowing if something was a promotion or a demotion.
GRM.GetGuildMemberRankID = function( name )
    local result = -1;
    -- Prevents errors if the other players sends a sync call too early, it will just ignore it.
    if GRM_G.guildName ~= "" and GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name] ~= nil then
        result = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name].rankIndex;
    end
    return result;
end

-- Method:          GRM.GetReputationTextLevel ( int , bool )
-- What it Does:    Returns the text name of a rank, localized
-- Purpose:         For use on the player mouseover window to reveal player guild rank.
GRM.GetReputationTextLevel = function ( index , includeColoring )
    local result = "";
    local color = "";
    if index == 8 then
        if includeColoring then
            color = "|cff00ffff";
        end
        result = color .. GRM.L ( "Exalted" ) .. "|r";
    elseif index == 7 then
        if includeColoring then
            color = "|cff00ffcc";
        end
        result = color .. GRM.L ( "Revered" ) .. "|r";
    elseif index == 6 then
        if includeColoring then
            color = "|cff00ff88";
        end
        result = color .. GRM.L ( "Honored" ) .. "|r";
    elseif index == 5 then
        if includeColoring then
            color = "|cff32CD32";
        end
        result = color .. GRM.L ( "Friendly" ) .. "|r";
    else
        if includeColoring then
            color = "|cffffff00";
        end
        result = color .. GRM.L ( "Neutral" ) .. "|r";
    end
    return result;
end

-- Method:          GRM.RegisterGuildChatPermission()
-- What it Does:    Initiates attempt to determine player has access to proper channel
-- Purpose:         If guild chat channel is restricted then sync cannot be enabled either...
GRM.RegisterGuildChatPermission = function()
    GRM_G.HasAccessToGuildChat = C_GuildInfo.CanSpeakInGuildChat();
    if GRM.CanEditOfficerNote() then
        GRM_G.HasAccessToOfficerChat = true;
    end
end

-- Method:          GRM.AddPlayerStatusCheck ( string , int )
-- What it Does:    Adds a player to the status check, to notify when they come Online!
-- Purpose:         Active tracking of changes within the guild on player status. Easy to notify you when someone comes online!
GRM.AddPlayerStatusCheck = function ( name , checkIndex )
    local isFound = false;
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];
    local needsToActivateNotification = false;

    for i = 1 , #GRM_G.ActiveStatusQue do
        if name == GRM_G.ActiveStatusQue[i][1] and checkIndex == GRM_G.ActiveStatusQue[i][3] then
            isFound = true;
            break;
        end
    end

    -- Good, the notification has not already been set...
    if not isFound then
        -- Only need to activate if at zero loop resting state, not if it is already scanning something.
        if #GRM_G.ActiveStatusQue == 0 then
            needsToActivateNotification = true;
        end

        if player ~= nil then

            table.insert ( GRM_G.ActiveStatusQue , { name , player.isOnline , checkIndex } );
            -- Return from AFK
            if checkIndex == 1 then
                GRM.Report ( "|cffff0000" .. GRM.L ( "Notification Set:" ) .. " |r" .. GRM.L ( "Report When {name} is ACTIVE Again!" , GRM.GetClassifiedName ( name , true ) ) );
            -- Return from Offline
            elseif checkIndex == 2 then
                GRM.Report ( "|cffff0000" .. GRM.L ( "Notification Set:" ) .. " |r" .. GRM.L ( "Report When {name} Comes Online!" , GRM.GetClassifiedName ( name , true ) ) );
            -- Goes Offline
            elseif checkIndex == 3 then
                GRM.Report ( "|cffff0000" .. GRM.L ( "Notification Set:" ) .. " |r" .. GRM.L ( "Report When {name} Goes Offline!" , GRM.GetClassifiedName ( name , true ) ) );            
            end

        end
    else
        GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "Notification Has Already Been Arranged..." ) );
    end

    if needsToActivateNotification then
        GRM.NotificationIndependentChecker();
    end
end

-- Method:          GRM.IsGuildieOnline( string )
-- What it Does:    Lets you know if a guildie is currently online by returning true
-- Purpose:         It is useful to save resources and for knowledge to know if a player is currently online or not. No need to scan certain things wastefully if they are offline.
GRM.IsGuildieOnline = function ( name )
    if not GRM.IsCalendarEventEditOpen() then
        GRM.GuildRoster();
    end
    local result = false;
    for i = 1 , GRM.GetNumGuildies() do
        local fullName , _, _, _, _, _, _, _, online = GetGuildRosterInfo ( i );
        if name == fullName then
            result = online;
            break;
        end
    end
    return result;
end

----------------------------------
----- SOCIAL API -----------------
----------------------------------

-- Method:          GRM.IsOnFriendsList ( string )
-- What it Does:    Returns true if the given player is on your friends list (not battle.net friends, just WOW only)
-- Purpose:         Useful to know as if a player leaves the guild, you can add them to a friends list, and if the player does not exist, it will say
--                  "Player Not Found" thus revealing that the player that left the guild either left the server too, or namechanged after leaving.
GRM.IsOnFriendsList = function ( fullName )
    local result = { false , false , "" , "" };
    for i = 1 , C_FriendList.GetNumFriends() do
        local player = C_FriendList.GetFriendInfoByIndex ( i );
        if player.name == fullName or player.name == GRM.SlimName ( fullName ) then
            result[1] = true;           -- They are on FriendsList
            result[2] = player.connected;       -- if that player happens to be online
            local trigger = GetPlayerInfoByGUID ( player.guid );                   -- you need to query server twice often to trigger it to work.
            trigger = select ( 2 , GetPlayerInfoByGUID ( player.guid ) );
            result[3] = trigger;
            result[4] = player.guid;
            break;
        end
    end
    return result;
end

-- Method:          GRM.SetplayersStillOnServer ( string , bool )
-- What it Does:    Builds the list of players that have left the guild but are still on the server... by triggering the GUID check, when it calls it again it will be refreshed.
-- Purpose:         Reliable way to see if player is on the server -- updated in BFA to be more responsive.
--                  Note -- players who have deleted a toon the GUID remains for restoration purposes, it seems. At least for some period of time.
GRM.SetplayersStillOnServer = function( playerNames , isBanCheck )

    if not isBanCheck then
        GRM_G.playersStillOnServer = {};
    else
        GRM_G.LeftBanPlayersStillOnServer = {};
    end

    for i = 1 , #playerNames do
        GRM.IsPlayerStillOnServerByGUID ( playerNames[i][1] , playerNames[i][2] , isBanCheck )
    end
end

-- Method:          GRM.IsPlayerStillOnServerByGUID ( string , string , bool )
-- What it Does:    Returns true if the player is still on the server by checking their GUID. If no GUID info returned you can know they either deleted or transferred
-- Purpose:         Useful info to know when a player leaves or older list information.
GRM.IsPlayerStillOnServerByGUID = function ( name , guid , isBanCheck )

    if guid ~= nil and guid ~= "" then
        local needToCallOnceToTrigger = GetPlayerInfoByGUID ( guid );

        -- Call Again
        C_Timer.After ( 1 , function()
            if needToCallOnceToTrigger == nil or needToCallOnceToTrigger then
                if GetPlayerInfoByGUID ( guid ) ~= nil then
                    if not isBanCheck then
                        table.insert ( GRM_G.playersStillOnServer , name );
                    else
                        table.insert ( GRM_G.LeftBanPlayersStillOnServer , name );
                    end
                end
            end
        end);
    end

 end

 -- Method:         GRM.ValidateBanGUIDs()
 -- What it Does:   Checks against the given result from a Ban list GUID check and returns a list of the players whose info went missing.
 -- Purpose:        To be able to update the Ban Frames with relevant info on if player is still on the server or not on the fly.
 GRM.ValidateBanGUIDs = function()
    local currentBanList = GRM.GetSortedBanListNamesWithDetails();
    local result = {};
    local isFound = false;

    for i = 1 , #currentBanList do
        if currentBanList[i][9] ~= "" then
            isFound = false;
            for j = 1 , #GRM_G.LeftBanPlayersStillOnServer do
                if GRM_G.LeftBanPlayersStillOnServer[j] == currentBanList[i][1] then
                    isFound = true;
                    table.remove ( GRM_G.LeftBanPlayersStillOnServer , j );         -- Pure efficiency. Shrink the list
                    break;
                end
            end

            if not isFound then
                table.insert ( result , currentBanList[i][1] );
            end
        end

        -- Exist if the list is now empty
        if #GRM_G.LeftBanPlayersStillOnServer == 0 then
            break;
        end
    end
    
    return result;
 end

-- Method:          GRM.MiscCleanupOnLogin()
-- What it Does:    On player reload, it basically does a quick cleanup of any unfinished business
-- Purpose:         To fix actions that might have been unfinished or interrupted, but saved, so they can be cleaned up and restarted 
--                  without residual messes.
GRM.MiscCleanupOnLogin = function()
    if not GRM_G.MsgFilterEnabled then
        GRM_G.MsgFilterEnabled = true;   -- Establishing boolean gate so it is only registered once.
        ChatFrame_AddMessageEventFilter ( "CHAT_MSG_SYSTEM" , GRM.SetSystemMessageFilter );
    end

    -- Friends list actions...
    GRM_G.MsgFilterDelay = true
    if GRM_Misc[GRM_G.addonUser] == nil then
        GRM.ConfigureMiscForPlayer( GRM_G.addonUser );
    end

    for i = 1 , 3 do
        if GRM_Misc[GRM_G.addonUser][i][1] then
            C_Timer.After ( 1 , function()
                for j = 1 , #GRM_Misc[GRM_G.addonUser][i][2] do
                    GRM_G.MsgFilterDelay = true
                    GRM_G.MsgFilterDelay2 = true;
                    C_FriendList.RemoveFriend ( GRM_Misc[GRM_G.addonUser][i][2][j] );
                    C_FriendList.RemoveFriend ( GRM.SlimName ( GRM_Misc[GRM_G.addonUser][i][2][j] ) );          -- On reload of the server, it refreshes the names and purges the server from some...
                end
                GRM_Misc[GRM_G.addonUser][i] = { false , {} };
            end);
        end
    end
    GRM_G.MsgFilterDelay = false;
    C_Timer.After ( 2 , function()
        GRM_G.MsgFilterDelay2 = false;
    end);
    end

-- Method:          GRM.GetPlayersWithoutGUID();
-- What it Does:    Gets a list of names of all players no longer in the guild who do not have GUIDs
-- Purpose:         Necessary info to pull on a player to get their GUID
GRM.GetPlayersWithoutGUID = function()
    local result = {};
    local guildData = GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ];

    for name , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            if player.GUID == "" or player.class == "UNKNOWN" then
                table.insert ( result , name );
            end
        end
    end

    return result;
end

-- Method:          GRM.QueryPlayersGUIDByFriendsList ( array , int , boolean , boolean )
-- What it Does:    Adds players to friends list to determine their proper class and GUID
-- Purpose:         Pre-8.0 it was impossible to determine a player's GUID unless they were currently online. This is no longer the case. GUID is very useful.
GRM.QueryPlayersGUIDByFriendsList = function ( playerNames , initialValue , cleanUpNoLongers , reportClassChange )
    GRM_G.CheckingGUIDThroughFriendsList = true;
    C_Timer.After ( 1 , function()
        if not GRM_G.MsgFilterEnabled then
            GRM_G.MsgFilterEnabled = true;
            ChatFrame_AddMessageEventFilter ( "CHAT_MSG_SYSTEM" , GRM.SetSystemMessageFilter );
        end
        GRM_G.TempBanSystemMessage = true;

        if C_FriendList.GetNumFriends() < 100 then
            -- Add the names!!!
            -- Now, we use the friends list by cheating the server to get around the /who slow callback.
            local isFound = {};
            local startValue = initialValue or 1;       -- For some reason recursively it shows the initialValue as a nil, so reset it to 1
            GRM_G.TempListNamesAddedGUIDCheck = {};              -- This list will be used to determine who to remove from friend's list.
            if #playerNames > 0 then
                GRM_Misc[GRM_G.addonUser][3] = { true , {} };      -- for backup incase player logs off in the middle of adding names...
                local toAddNumber = #playerNames;
                if C_FriendList.GetNumFriends() + #playerNames > 100 then 
                    toAddNumber = 100 - C_FriendList.GetNumFriends();
                end
                for i = 1 , toAddNumber do
                    if initialValue <= #playerNames then
                        isFound = GRM.IsOnFriendsList ( playerNames[initialValue] );

                        if not isFound[1] and C_FriendList.GetNumFriends() < 100 then
                            GRM_G.MsgFilterDelay2 = true;                            
                            C_FriendList.AddFriend ( playerNames[initialValue] );
                            table.insert ( GRM_G.TempListNamesAddedGUIDCheck , playerNames[initialValue] );
                        end
                        initialValue = initialValue + 1;
                    else
                        break;
                    end
                end
                GRM_Misc[GRM_G.addonUser][3][2] = GRM_G.TempListNamesAddedGUIDCheck;
            end

            -- Now we need to pull the data!!!
            C_Timer.After ( 1.5 , function()
                local isFound;
                local guild = GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
                local player;

                for i = startValue , initialValue - 1 do                -- Minus one because it incremented up by 1 in the final C_FriendList.AddFriend for loop.
                    isFound = GRM.IsOnFriendsList ( playerNames[i] );
                    player = guild[playerNames[i]];
                    
                    -- isFound = { isOnFriendsList , isOnline , className , guid }
                    if isFound[1] then
                        if player ~= nil then

                            if reportClassChange and player.name == GRM_G.CurrentBanSelectedName[1] and player.class ~= isFound[3] then
                                GRM.Report ( GRM.L ( "{name} plays the {custom1} class, not {custom2}." , GRM.GetClassColorRGB ( isFound[3] , true ) .. GRM.SlimName ( GRM_G.CurrentBanSelectedName[1] ) .. "|r" , nil , nil , GRM.GetClassColorRGB ( isFound[3] , true ) .. GRM.L ( GRM.FormatInputName( isFound[3] ) ) .. "|r" , GRM.GetClassColorRGB ( player.class , true ) .. GRM.L ( GRM.FormatInputName( player.class ) ) .. "|r" ) );
                            end
                            player.class = isFound[3];               -- Updates Class properly
                            player.GUID = isFound[4];              -- Updates GUID since this was not obtainable until post 8.0 launch
                        end

                        GRM_G.MsgFilterDelay2 = true;
                        C_FriendList.RemoveFriend ( playerNames[i] );
                        C_FriendList.RemoveFriend ( GRM.SlimName ( playerNames[i] ) );   -- Non merged realm will not have the server name, so this avoids the "Player not found" error]

                    else
                        -- NEED TO REMOVE THEM FROM THE DATABASE
                        if cleanUpNoLongers then

                            if player ~= nil then
                                if not player.bannedInfo[1] then
                                    guild[playerNames[i]] = nil;
                                else
                                    player.isUnknown = true
                                    if reportClassChange and player.name == GRM_G.CurrentBanSelectedName[1] then
                                        GRM.Report ( GRM.L ( "Unable to identify {name}. Ensure your spelling is accurate! Otherwise, they may have left the server." , GRM.GetClassColorRGB ( GRM_G.CurrentBanSelectedName[2] , true ) .. GRM_G.CurrentBanSelectedName[1] .. "|r" ) );
                                    end
                                end
                            end
                        end
                    end
                end

                -- In case it finds anything, it updates
                if GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame:IsVisible() then
                    GRM.RefreshBanListFrames();
                end

                -- Do a recursive re-check if necessary...
                if initialValue <= #playerNames then
                    GRM_G.CheckingGUIDThroughFriendsList = true;
                    GRM.QueryPlayersGUIDByFriendsList ( playerNames , initialValue , cleanUpNoLongers , reportClassChange );
                    return;
                else
                    GRM_G.CheckingGUIDThroughFriendsList = false;
                    C_Timer.After ( 2 , function()
                        GRM_G.MsgFilterDelay2 = false;
                        GRM_G.TempBanSystemMessage = false;
                    end);
                    -- FINISHED!!!
                end
                -- For restoring on logging off in middle of group friend adding...
                GRM_Misc[GRM_G.addonUser][3] = { false , {} };
            end);
        else
            C_Timer.After ( 2 , function()
                GRM_G.MsgFilterDelay2 = false;
            end);
            if not GRM_G.TooManyFriendsWarning then
                GRM_G.TooManyFriendsWarning = true;
                GRM.Report ( GRM.L ( "You currently are at {num} non-Battletag friends. To fully take advantage of all of GRM features, please consider clearing some room." , nil , nil , 100 ) );
            end
        end
    end);
end

-- Method:          GRM.DelayMinimapButtonOpen ( boolean )
-- What it Does:    Triggers the core GRM window only to open when hitting the minimap button if the player is out of combat, and then delays open if in combat.
-- Purpose:         To prevent accidental click of the minmap button if player is in combat.
GRM.DelayMinimapButtonOpen = function ( messageDisplayed , message2Displayed )
    
    if not GRM_G.inCombat and GRM_LogReport_Save[GRM_G.F] ~= nil and GRM_LogReport_Save[GRM_G.F][GRM_G.guildName] ~= nil then
        GRM_G.minmapButtonDelay = false;
        GRM_UI.GRM_RosterChangeLogFrame:Show();
    else
        if not message2Displayed and GRM_LogReport_Save[GRM_G.F] ~= nil and GRM_LogReport_Save[GRM_G.F][GRM_G.guildName] == nil then
            GRM.Report ( GRM.L ( "Database Still Loading. GRM will open automatically when finished." ) );
            message2Displayed = true;
        end
        if not messageDisplayed then
            GRM.Report ( GRM.L ( "GRM window will open when combat ends." ) );
            messageDisplayed = true;
        end
        GRM_G.minmapButtonDelay = true;
        C_Timer.After ( 1 , function()
            GRM.DelayMinimapButtonOpen ( messageDisplayed , message2Displayed );
        end);
    end
end


local channelEnum = {
    ["CHAT_MSG_GUILD"] = "Guild",
    ["CHAT_MSG_OFFICER"] = "Officer",
    ["CHAT_MSG_PARTY"] = "Party",
    ["CHAT_MSG_PARTY_LEADER"] = "Party Leader",
    ["CHAT_MSG_RAID"] = "Raid",
    ["CHAT_MSG_RAID_LEADER"] = "Raid Leader",
    ["CHAT_MSG_INSTANCE_CHAT"] = "Instance",
    ["CHAT_MSG_INSTANCE_CHAT_LEADER"] = "Instance Leader",
    ["CHAT_MSG_ACHIEVEMENT"] = "Achievement"
}

-- Method:          GRM.GetChannelType ( string )
-- What it Does:    Returns the type of channel, be it guild, be it anniversary, be it whatever. This returns all social chat channel types
-- Purpose:         A helper to identify new events in relation to chat monitoring.
GRM.GetChannelType = function ( channelName )
    local result = "";
    for key, y in pairs ( channelEnum ) do
        if y == channelName then
            result = key;
            result = string.gsub ( result , "CHAT_MSG_" , "" ); -- Parses out the CHAT_MSG_ and leaves the title.
            break;
        end
    end
    return result;
end

-- Method:          GRM.GetChatRGB ( string )
-- What it Does:    Returns the RGB color code for the given chat channel
-- Purpose:         Being able to create custom channels and match them to the player's settings is extremely useful for downstream plans.
GRM.GetChatRGB = function ( channel )
    local result = {};
    if ChatTypeInfo[ channel ] ~= nil then
        result = { ChatTypeInfo[channel].r , ChatTypeInfo[channel].g , ChatTypeInfo[channel].b , ChatTypeInfo[channel].colorNameByClass };
    end
    return result;
end

-- Method:          GRM.ShowCustomColorPicker ( float , float , float , float , int , function )
-- What it Does:    Established some default values for the colorpicker frame, and then shows it
-- Purpose:         One, to configure the color picker frames, and two, to create a universally recyclable function for all potential future colorpicker options as well.
GRM.ShowCustomColorPicker = function ( r , g , b , a , setting , callback )

    if setting == 98 then
        GRM_G.MainTagColor = true
    elseif setting == 99 then
        GRM_G.GroupInfoIconColor = true;
    else
        GRM_G.CurrentTagColorBox = setting;
    end

    ColorPickerFrame:SetColorRGB ( r , g , b );
    ColorPickerFrame.previousValues = { r , g , b , a };
    ColorPickerFrame.func, ColorPickerFrame.opacityFunc, ColorPickerFrame.cancelFunc = callback, callback, callback;
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatMenu:Hide();
    ColorPickerFrame:Hide(); -- Need to run the OnShow handler.
    ColorPickerFrame:Show();
end

-- Method:          GRM.ColorSelectFrameTextureUpdate()
-- What it Does:    When on the ColorPickerWindow from the Options, this is the logic that updates on the fly and saves the colors as you go.
-- Purpose:         To establish the proper RGB coloring of the text in the General options tab
GRM.ColorSelectFrameTextureUpdate = function()
    local r , g , b = ColorPickerFrame:GetColorRGB();
    -- Texture Box
    if GRM_G.MainTagColor and GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame:IsVisible() then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_ColorSelectOptionsFrame.GRM_OptionsTexture:SetColorTexture ( r , g , b , 1 );
        -- Update the dropdown window color too
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatSelected.GRM_TagText:SetTextColor ( r , g , b , 1 );                  -- color for the box AND the dropdown selection on tag format

    elseif GRM_G.GroupInfoIconColor and GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ModulesFrame:IsVisible() then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ModulesFrame.GRM_GroupInfoColorPickerFrame.GRM_GroupInfoOptionsTexture:SetColorTexture ( r , g , b , 1 );

    elseif GRM_G.CurrentTagColorBox > 0 then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogExtraOptionsFrame[ "colorBoxTexture" .. GRM_G.CurrentTagColorBox ]:SetColorTexture ( r , g , b , 1 );
        GRM_UI.UpdateLogFilterTextColor ( r , g , b , GRM_G.CurrentTagColorBox );
        
    end

    if GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ColorPickerR:IsVisible() then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ColorPickerR:SetText ( math.floor ( r * 255 ) );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ColorPickerR:Show();
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ColorPickerG:SetText ( math.floor ( g * 255 ) );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ColorPickerG:Show();
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ColorPickerB:SetText ( math.floor ( b * 255 ) );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ColorPickerB:Show();
    end
end

-- Method:          GRM.GetNameWithMainTags( ( string , boolean , boolean , boolean , boolean )
-- What it Does:    On refresh or any changes on the CalenderInviteFrame it resets the main/alt tags
-- Purpose:         This needs to be handled and refreshed constantly because any changes, even with the slider, it overwrites all script handlers. So 
--                  tags needs to be reapplied.
GRM.GetNameWithMainTags = function( name , slimName , includeMainOnAlts , includeAltTagIfMain , includeParentheses )
    local hasAlts = false;
    if GRM_G.MainTagHexCode ~= "" then
        local hexCode = GRM_G.MainTagHexCode;
        local mainDisplay = GRM.GetMainTags ( false , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].mainTagIndex );
        local altDisplay = GRM.GetAltTags ( false , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].mainTagIndex );
        local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];

        local par1 , par2 = "" , "";
        if includeParentheses then
            par1 = "|r(";
            par2 = "|r)";
        end
        
        if guildData ~= nil then
            local isInGuild = false;
            local needsSlimming = true;
            local player = guildData[name];
            if player ~= nil then
                isInGuild = true;
                -- Found in the guild database...
                if player.isMain then
                    needsSlimming = false;
                    if slimName then
                        name = ( GRM.SlimName ( name ) .. " " .. hexCode .. mainDisplay .. "|r" );     -- This player is the main
                    else
                        name = ( name .. " " .. hexCode .. mainDisplay .. "|r" );
                    end
                    if #player.alts > 0 then
                        hasAlts = true;
                        GRM_G.IsAltGrouping = true;
                    end
                elseif #player.alts > 0 then
                    hasAlts = true;
                    GRM_G.IsAltGrouping = true;
                    local listOfAlts = player.alts;
                    for r = 1 , #listOfAlts do
                        if listOfAlts[r][5] then
                            needsSlimming = false;
                            -- Formatting...
                            if slimName then
                                name = GRM.SlimName ( name );  -- This player is not the main, but is part of a grouping with a player who is main
                            end
                            -- for alt tag to be added
                            if includeAltTagIfMain then
                                name = name .. " " .. hexCode .. altDisplay .. "|r"; 
                            end
                            if includeMainOnAlts then
                                name = name .. " " .. par1 .. GRM.GetClassifiedName ( listOfAlts[r][1] , slimName ) .. par2 .. " " .. hexCode .. mainDisplay .. "|r";
                            end
                            break;
                        end
                    end
                end
            end

            if needsSlimming and slimName then
                name = GRM.SlimName ( name );
            end
            if not isInGuild then
                -- Add special tag designation for NOT IN GUILD
                local notInGuildTag = GRM.GetNonGuildieTags ( false , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].mainTagIndex );
                name = ( GRM.SlimName ( name ) .. " " .. hexCode .. notInGuildTag .. "|r" );

            end
        end
    end
    return name , hasAlts;
end

-- Method:          GRM.GetMainName ( string )
-- What it Does:    Returns the main name of the given alt from the alt grouping, or returns "" if none is established for the given player.
-- Purpose:         To have a cleaner, simpler way of obtaining the main name.
GRM.GetMainName = function ( fullName , includeParentheses )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][fullName];
    local result = "";
    local playerHasAlts = false;

    if not player then
        player = GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ][fullName];
    end

    if player ~= nil then
        if player.isMain then    -- The player we are searching IS the main...
            result = fullName;
            if #player.alts > 0 then
                playerHasAlts = true;
            end
        elseif #player.alts > 0 then
            playerHasAlts = true;
            -- Player has alts... let's go through the list of alts for main info...
            local listOfAlts = player.alts;
            for j = 1 , #listOfAlts do
                if listOfAlts[j][5] then
                    -- woot main is found!
                    result = listOfAlts[j][1];
                    break;
                end
            end
        end

        if result ~= "" and includeParentheses then
            result = "(" .. result .. ")";
        end
    end

    return result , playerHasAlts;
end

-- Method:          GRM.AddMainToChat ( ... )
-- What it Does:    It adds either a Main tag to the player, or if they are on an alt, includes the name of the main.
-- Purpose:         Easy to see player name in guild chat, for achievments and so on...
GRM.AddMainToChat = function( _ , event , msg , sender , ... )
    local result = false;
    local placeHolderMsg = msg;

    if sender ~= GRM_G.addonUser then
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showMainName then
            local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
            local channelName = channelEnum [ event ];
            local mainDisplay = GRM.GetMainTags ( false , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].mainTagIndex );
            -- Find the player in the guild!

            if guildData ~= nil then
                local player = guildData[sender];

                if player ~= nil then
                    -- Let's see if they are the main. If they are, no need to do anything...
                    if not player.isMain then
                        if #player.alts > 0 then
                            for j = 1 , #player.alts do
                                if player.alts[j][5] then
                                    local classColor = GRM.GetStringClassColorByName ( player.alts[j][1] , false );
                                    if channelName ~= "Achievement" then
                                        msg = GRM_G.MainTagHexCode .. mainDisplay .. "|r(" .. classColor .. GRM.SlimName ( player.alts[j][1] ) .. "|r): " .. msg;
                                    else
                                        msg = classColor .. GRM.SlimName ( player.alts[j][1] ) .. "|r" .. GRM_G.MainTagHexCode .. mainDisplay .. "|r " .. msg;
                                    end
                                    break;
                                end
                            end
                        end
                    else
                        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].useMainTag then
                            -- Just add main tag...
                            msg = GRM_G.MainTagHexCode .. mainDisplay .. "|r " .. msg;
                        end
                    end
                end
            end
        end
    end

    -- This will check if public note needs to be set.
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].noteSetEnabled and event == "CHAT_MSG_GUILD" or event == "CHAT_MSG_OFFICER" then
        GRM.TriggerPlayerNote ( sender , placeHolderMsg );
    end

    GRM.MessageHookControl();

    return result, msg, sender, ...
end

-- Method:          GRM.RefreshMainTagHexCode()
-- What it Does:    Reconverts the RGB values, scales then, then converts to hexcode
-- Purpose:         So, this only needs to be configured one time on load, or when the player updates the settings.
GRM.RefreshMainTagHexCode = function()
    GRM_G.MainTagHexCode = GRM.rgbToHex ( { GRM.ConvertRGBScale ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["mainTagColor"].r , true ) , GRM.ConvertRGBScale ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["mainTagColor"].b , true ) , GRM.ConvertRGBScale ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["mainTagColor"].g , true ) } );
end

-- Method:          GRM.GetMainTags();
-- What it Does:    Returns the table of potential main tags
-- Purpose:         For easy marking of alts/mains in gchat.
GRM.GetMainTags = function( returnTable , index )
    local tags = { "<" .. GRM.L ( "M" ) .. ">" , "(" .. GRM.L ( "M" ) .. ")" , "<" .. GRM.L ( "Main" ) .. ">" , "(" .. GRM.L ( "Main" ) .. ")" , "[" .. GRM.L ( "Main" ) .. "]" , "[" .. GRM.L ( "M" ) .. "]" , "" };
    if returnTable then
        return tags;
    else
        return tags[index];
    end
end

-- Method:          GRM.GetAltTags();
-- What it Does:    Returns the table of potential Alt tags
-- Purpose:         For easy marking of alts/mains in gchat.
GRM.GetNonGuildieTags = function( returnTable , index )
    local tags = { "<" .. GRM.L ( "Friend" ) .. ">" , "(" .. GRM.L ( "Friend" ) .. ")" , "<" .. GRM.L ( "Friend" ) .. ">" , "(" .. GRM.L ( "Friend" ) .. ")" , "[" .. GRM.L ( "Friend" ) .. "]" , "[" .. GRM.L ( "Friend" ) .. "]" , "" };
    if returnTable then
        return tags;
    else
        return tags[index];
    end
end

-- Method:          GRM.GetAltTags();
-- What it Does:    Returns the table of potential main tags
-- Purpose:         For easy marking of alts/mains in gchat.
GRM.GetAltTags = function( returnTable , index )
    local tags = { "<" .. GRM.L ( "A" ) .. ">" , "(" .. GRM.L ( "A" ) .. ")" , "<" .. GRM.L ( "Alt" ) .. ">" , "(" .. GRM.L ( "Alt" ) .. ")" , "[" .. GRM.L ( "Alt" ) .. "]" , "[" .. GRM.L ( "A" ) .. "]" , "" };
    if returnTable then
        return tags;
    else
        return tags[index];
    end
end

-- Method:          GRM.RemoveMainAltTags ( string )
-- What it Does:    takes the name, and parses out both the string coloring and the main tag. There is ALWAYS hexcode, so "|" can be found.
-- Purpose:         Add Alt window has the tags added, this just retroactively removes them.
GRM.RemoveMainAltTags = function ( text )
    local index = string.find ( text , "|" )
    if index ~= nil then
        text = string.sub ( text , 1 , index - 1 );
    end
    return text;
end

-- Method:          GRM.RemoveHexCodeColoringFromString ( string )
-- What it Does:    Removes the hexcopde coloring from any string - Example
-- Purpose:         Useful to parse out names for compatibility reasons for cross addon compatibility with the groupInfo module.
GRM.RemoveHexCodeColoringFromString = function ( text )
    local result = string.gsub ( text , "||r" , "" );    -- Firsr thing, remove the end coloring cuts
    local ind = string.find ( result , "||cff" , 1 , true );

    if not ind then
        ind = string.find ( result , "||CFF" , 1 , true ); 
    end

    while ( ind ~= nil ) do
        result = string.gsub ( result , string.sub ( result , ind , ind + 10 ) , "" );
        ind = string.find ( result , "||" , 1 , true );
    end

    return GRM.Trim ( result );
end

-- Method:          GRM.RemoveStringColoring(string)
-- What it Does:    Removes the HexTag Blizz uses to identify and color the text. Anything that starts with |cffxxxxxx
-- Purpose:         Clean up the texts for export so it is just plain text.
GRM.RemoveStringColoring = function( text )
    local start = string.find ( text , "\000" );
    if start ~= nil then
        text = string.sub ( text , 1 , start - 1 );
    end
    while ( string.find ( string.lower ( text ) , string.char(124) .. "cff" ) ~= nil ) do
        local index = string.find ( string.lower ( text ) , string.char(124) .. "cff" );
        text = string.sub ( text , 1 , index -1 ) .. string.sub ( text , index + 10 );
    end
    text = string.gsub ( text , string.char(124) .. "r" , "" );    -- if I use string.lower, it returns a full lower string. don't want that.
    text = string.gsub ( text , string.char(124) .. "R" , "" );

    return text;
end


-----------------------------------
-------- PROFESSIONS --------------
-----------------------------------


-- Nothing yet


-----------------------------------
--------- Version Tracking --------
--------- Addon User Tracking -----
-----------------------------------

-- Method:          GRM.VersionCheck ( string )
-- What it Does:    Checks player version compared to another player's and recommends updating your version if needed
-- Purpose:         Encourage the player to keep their addon up to date!
GRM.VersionCheck = function( msg )
    -- parse the message
    local version = string.sub ( msg , 1 , string.find ( msg , "?" ) - 1 );
    local time = tonumber ( string.sub ( msg , string.find ( msg , "?" ) + 1 ) );

    -- If the versions are not equal and the received data is larger (more recent) than player's time, player should receive reminder to update!
    if version ~= GRM_G.Version then
        if not GRM_G.VersionChecked and time > GRM_G.PatchDay then
            GRM_G.VersionChecked = true;
            -- Let's report the need to update to the player!
            GRM.Report ( "|cff00c8ff" .. GRM.L ( "GRM:" ) .. " |cffffffff" .. GRM.L ( "A new version of Guild Roster Manager is Available!" ) .. " |cffff0044" .. GRM.L ( "Please Upgrade!" ) );
            -- No need to send comm because he has the update, not you!

        elseif time < GRM_G.PatchDay then
            -- Your version is more up to date! Send comms out!
            C_ChatInfo.SendAddonMessage ( "GRMVER" , GRM_G.Version .. "?" .. GRM_G.PatchDayString , "GUILD" ); -- Remember, patch day is an int in epoch time, so needs to be converted to string for comms
        end
    end
end

-- Method:          GRM.RegisterVersionCheck()
-- What it Does:    Registers the logic for comm talk between addon users to do a version check.
-- Purpose:         Version checking! Encourages the player to upgrade their addon if it is outdated!
GRM.RegisterVersionCheck = function()
    -- Registering comm prefix, establishing event monitoring for comm activity across guild channel.
    C_ChatInfo.RegisterAddonMessagePrefix ( "GRMVER" );
    VersionCheck:RegisterEvent ( "CHAT_MSG_ADDON" );
    -- Register used prefixes!

    -- Setup tracking actions
    VersionCheck:SetScript ( "OnEvent" , function ( _ , event , prefix , msg , channel , sender )
        if event == "CHAT_MSG_ADDON" and prefix == "GRMVER" and channel == "GUILD" then
                -- Gotta filter my own messages out too!
            if sender ~= GRM_G.addonUser then
                -- Just to ensure it only does a check one time from each player with the addon installed.
                local isFound = false;
                for i = 1 , #GRM_G.VersionCheckedNames do
                    if GRM_G.VersionCheckedNames[i] == sender then
                        isFound = true;
                        break;
                    end
                end

                -- Player has never commed on version with you. Add their name, then do a version check!
                if not isFound then
                    table.insert ( GRM_G.VersionCheckedNames , sender );
                    GRM.VersionCheck ( msg );
                end
            end
        end
    end);
end

-- Method:          GRM.RegisterGuildAddonUsersRefresh ()
-- What it Does:    Two uses. One, it checks to see if all the people on the list of users with addon installed are still online, and if not, purges them
--                  and two, requests data from the players again to be updated. This is useful because players may change their settings.
-- Purpose:         To keep the UI up to date. It is necessary to refresh the info occasionally rather than just on login.
GRM.RegisterGuildAddonUsersRefresh = function ()              -- LoadRefresh is just OnShow() for the window, no need to have 10 sec delay as we are not oging to send requests, just purge the offlines.
    -- Purge the players that are no longer online...
    if time() - GRM_G.refreshAddonUserDelay >= 5 then
        GRM_G.refreshAddonUserDelay = time();
        local listOfNames = GRM.GetAllGuildiesOnline( true );
        local notFound = true;

        for i = 1 , #GRM_G.currentAddonUsers do
            notFound = true;
            for j = 1 , #listOfNames do
                if GRM_G.currentAddonUsers[i] ~= nil and listOfNames[j] ~= nil then
                    if listOfNames[j] == GRM_G.currentAddonUsers[i][1] then
                        notFound = false;
                        break;
                    end
                end
            end
            
            -- if notfound, purge em. They're no longer online...
            if notFound then
                table.remove ( GRM_G.currentAddonUsers , i );
            end
        end
        -- Request the updated info!
        C_ChatInfo.SendAddonMessage ( "GRMUSER" , "REQ?" , "GUILD" );
        

        -- Updating the frames. Giving 2 seconds to receive responses!
        if GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame:IsVisible() then
            C_Timer.After ( 2 , function()
                GRM.BuildAddonUserScrollFrame();
            end);
        end
    end

end

-- Method:          GRM.AddonUserRegister ( string , string )
-- What it Does:    Analyzes to see if the addon user communicating with you is capable of syncing with you, as you could filter them or they could filter you.
-- Purpose:         Having a UI showing who has the addon, what version, if you can sync is just useful information. Not necessary for addon functionality, but is good for Quality of Life.
GRM.AddonUserRegister = function( sender , msg )

    if GRM_G.guildName ~= "" and GRM_G.F ~= "" then
        local rankOfSender = GRM.GetGuildMemberRankID ( sender );
        local playerRankID = GRM.GetGuildMemberRankID ( GRM_G.addonUser )
        local banRankRequirement = 0;

        -- If rank call fails.
        if rankOfSender == -1 or playerRankID == -1 then
            return;
        end
        local result = "Ok!";
        local grouped = "";
        local isGrouped = false;

        -- Parsed Data
        local version = string.sub ( msg , 1 , string.find ( msg , "?" ) - 1 );
        msg = string.sub ( msg , string.find ( msg , "?" ) + 1 );
        local epochTimeVersion = tonumber ( string.sub ( msg , 1 , string.find ( msg , "?" ) - 1 ) );
        msg = string.sub ( msg , string.find ( msg , "?" ) + 1 );
        local syncOnlyCurrent = string.sub ( msg , 1 , string.find ( msg , "?" ) - 1 );
        msg = string.sub ( msg , string.find ( msg , "?" ) + 1 );
        local senderRankRequirement = tonumber ( string.sub ( msg , 1 , string.find ( msg , "?" ) - 1 ) );

        if epochTimeVersion >= 1552804021 then          -- data analysis modified with patch 1.45
            msg = string.sub ( msg , string.find ( msg , "?" ) + 1 );
            banRankRequirement = tonumber ( string.sub ( msg , 1 , string.find ( msg , "?" ) - 1 ) );

            if string.find ( string.sub ( msg , string.find ( msg , "?" ) + 1 ) , "?" , 1 , true ) ~= nil then                         -- Pre 1.82
                msg = string.sub ( msg , string.find ( msg , "?" ) + 1 );
                grouped = string.sub ( msg , 1 , string.find ( msg , "?" ) - 1 );
                if grouped == "true" then
                    isGrouped = true;
                end
            else
                result = "Outdated Version";
            end
        end
        
        local syncIsEnabled = string.sub ( msg , string.find ( msg , "?" ) + 1 );

        -- Temporary use til I figure out what to do with it
        if banRankRequirement then
            banRankRequirement = nil;
        end

        -- Useful logic controls.
        -- { "Ok!" , "Their Rank too Low" , "Your Rank too Low" , "Outdated Version" , "You Need Updated Version" , "Player Sync Disabled" }
        -- { sender , syncIsEnabled , syncOnlyCurrent , epochTimeVersion , version , senderRankRequirement , banRankRequirement }
        -- First, determine if the addon user will sync with you.
        if result ~= "Outdated Version" then
            if syncIsEnabled == "true" then
                if isGrouped then
                    result = "Disabled While Player is Grouped";
                    
                elseif rankOfSender > GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank or senderRankRequirement < playerRankID then
                    -- Ranks do not sync, let's get it right.
                    -- For messaging the reason why.
                    if rankOfSender > GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank then
                        result = "Their Rank too Low";
                    else
                        result = "Your Rank too Low";
                    end
                -- Check if versions are outdated as well.
                elseif syncOnlyCurrent == "true" or GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncSameVersion then
                    -- If versions are different. Just filtering out unnecessary computations if verisons are the same.
                    if epochTimeVersion ~= GRM_G.PatchDay then
                        -- If their version is older than yours...
                        if epochTimeVersion < GRM_G.PatchDay and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncSameVersion then
                            result = "Outdated Version";
                        elseif GRM_G.PatchDay < epochTimeVersion and syncOnlyCurrent == "true" then
                            result = "You Need Updated Version";
                        end
                    end
                end 
            else
                result = "Player Sync Disabled";
            end
        end
        
        -- Now, let's see if they are already in the table.
        local isFound = false;
        for i = 1 , #GRM_G.currentAddonUsers do
            if GRM_G.currentAddonUsers[i][1] == sender then
                GRM_G.currentAddonUsers[i][2] = result;
                GRM_G.currentAddonUsers[i][3] = version;
                GRM_G.currentAddonUsers[i][4] = epochTimeVersion;
                GRM_G.currentAddonUsers[i][5] = isGrouped;
                isFound = true;
                break;
            end
        end

        if not isFound then
            table.insert ( GRM_G.currentAddonUsers , { sender , result , version , epochTimeVersion , isGrouped } );
            GRM.RegisterGuildAddonUsersRefresh();
        end
    end    
end

-- Method:          GRM.GetNumAddonUsersOutdated()
-- What it Does:    Returns the integer count of number of addon users with outdated addons
-- Purpose:         For easier reporting when a person tries to sync with outdated version of the addon.
GRM.GetNumAddonUsersOutdated = function()
    local result = 0;

    for i = 1 , #GRM_G.currentAddonUsers do
        if GRM_G.currentAddonUsers[i][4] < GRM_G.PatchDay then
            result = result + 1;
        end
    end

    return result;
end

-- Method           GRM.RegisterGuildAddonUsers()
-- What it Does:    Initiates the event listening for sync'd user addon info
-- Purpose:         So player can see who has the addon installed and if you are good to sync with each other and if not, why not.
GRM.RegisterGuildAddonUsers = function()
    -- Registering frames for event listening.
    C_ChatInfo.RegisterAddonMessagePrefix ( "GRMUSER" );
    AddonUsersCheck:RegisterEvent ( "CHAT_MSG_ADDON" );
    -- Event listening for addon talk on successful event found!
    AddonUsersCheck:SetScript ( "OnEvent" , function ( _ , event , prefix , msg , channel , sender )
        if event == "CHAT_MSG_ADDON" and prefix == "GRMUSER" and channel == "GUILD" and sender ~= GRM_G.addonUser then
            -- parse out the header
            local header = string.sub ( msg , 1 , string.find ( msg , "?" ) - 1 );
            msg = string.sub ( msg , string.find ( msg , "?" ) + 1 );
            if header == "INIT" then
                GRM.AddonUserRegister ( sender , msg );
            elseif header == "REQ" then
                -- player is requesting info again. Sending update!
                C_ChatInfo.SendAddonMessage ( "GRMUSER" , "INIT?" .. GRM_G.Version .. "?" .. GRM_G.PatchDayString .. "?" .. tostring ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncSameVersion ) .. "?" .. tostring ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank ) .. "?" .. tostring ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankBanList ) .. "?" .. tostring ( IsInGroup() ) .. "?" .. tostring ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled ) , "GUILD" );
            end

        end
    end);

    -- Send out initial comms
    -- Name Version , epochTimestamp of update , string version of boolean if player restricts sync only to those with latest version of addon or higher. 
    C_ChatInfo.SendAddonMessage ( "GRMUSER" , "INIT?" .. GRM_G.Version .. "?" .. GRM_G.PatchDayString .. "?" .. tostring ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncSameVersion ) .. "?" .. tostring ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank ) .. "?" .. tostring ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankBanList ) .. "?" .. tostring ( IsInGroup() ) .. "?" .. tostring ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled ) , "GUILD" );
    -- Request for their data.
    GRM.RegisterGuildAddonUsersRefresh();
end

-- Method:          GRM.IsNumInString(string) 
-- What it Does:    Returns true if a numerical value is found in the form of a string.
-- Purpose:         Useful for player name submission, to verify if valid formatting.
GRM.IsNumInString = function( text )
    local numFound = false;
    for i = 1 , #text do
        if tonumber ( string.sub ( text , i , i ) ) ~= nil then
            -- NUM FOUND!
            numFound = true;
            break
        end
    end
    return numFound;
end

-- STILL NEED TO COMPLETE OTHER REGIONS' FONTS!!!!!!!!!!
-- Method:          GRM.IsValidName(string)
-- What it Does:    Returns true if the name only contains valid characters in it... based on ASCII numeric values
-- Purpose:         When player is manually adding someone to the player data, we need ot ensure only proper characters are allowed.
GRM.IsValidName = function ( name )
    local result = true;
    name = GRM.Trim ( name ); -- In case any whitespace before or after...
    for i = 1, #name do
        -- As a stopgap until I scan for all fonts, let's check this.
        local char = string.sub ( name , i , i );
        -- local byteValue = string.byte ( char );
        if tonumber ( char ) ~= nil or char == " " or char == "\\" or char == "\n" or char == ":" or char == "(" or char == "$" or char == "%" or char == "/" then
            return false;
        end
        -- -- Real ASCII limitations for the fonts
        -- if GRM_G.FontChoice == "Fonts\\FRIZQT__.TTF" then
        --     -- if byteValue ~= 127 and ( ( byteValue > 64 and byteValue < 91 ) or 
        --     -- ( byteValue > 96 and byteValue < 123 ) or 
        --     -- ( byteValue > 127 and byteValue < 166 ) or 
        --     -- ( byteValue > 180 and byteValue < 184 ) or 
        --     -- ( byteValue > 197 and byteValue < 200 ) or 
        --     -- ( byteValue > 207 and byteValue < 217 ) or 
        --     -- ( byteValue > 223 and byteValue < 238 ) ) then
        --     --     -- We're good!
        --     -- else
        --     --     result = false;
        --     --     break;
        --     -- end
        -- elseif GRM_G.FontChoice == "Fonts\\FRIZQT___CYR.TTF" then        -- Cyrilic
        
        -- elseif GRM_G.FontChoice == "FONTS\\2002.TTF" then                -- Korean

        -- elseif GRM_G.FontChoice == "Fonts\\ARKai_T.TTF" then             -- Mandarin Chinese

        -- elseif GRM_G.FontChoice == "FONTS\\blei00d.TTF" then             -- Mandarin Taiwanese

        -- elseif GRM_G.FontChoice == "FONTS\\PT_Sans_Narrow.ttf" then      -- ElvUI Default Font

        -- end
    end
    return result;
end

-- Method:          GRM.ByteRulesFollowed ( string , boolean , object [EditBox] )
-- What it Does:    It checks each char of the string for proper ASCII characters and if they are proper send messages and returns false if not, with optional error message
--                  in either the context of a string, or an editBox text
-- Purpose:         Certain characters if sent over Blizz's API can disconnect a character if not proper.
GRM.ByteRulesFollowed = function ( text , displayerErrorMessage , editBox )
    local result = true;
    local charValue;
    local count = 0;
    local warningGiven = false;

    for i = 1 , #text do
        charValue = string.byte ( text , i );
        if charValue < 32 then
            count = count + 1;
            result = false;

            if displayerErrorMessage and not warningGiven then
                warningGiven = true;
                if editBox then
                    GRM.Report ( GRM.L ( "The highlighted character is not valid for messages. Please remove." ) );
                    editBox:HighlightText ( i , i );
                    editBox:SetCursorPosition ( i + 1 );
                else
                    GRM.Report ( GRM.L ( "Not all characters are valid. Please remove any non-text characters." ) );
                end
            end
        end
    end
    return result;
end

-- Method:          GRM.UTF8char ( int , int )
-- What it Does:    Returns the number of bytes used by the UTF8 value
-- Purpose:         So as to maintain character count compatibility for all languages.
GRM.UTF8char = function ( s , i )
	i = i or 1

    local c = string.byte ( s , i );

	-- byte 1
	if c > 0 and c <= 127 then
		-- UTF8-1
		return 1

    -- byte 2
	elseif c >= 194 and c <= 223 then
		return 2

    -- byte 3
	elseif c >= 224 and c <= 239 then
		return 3

    -- byte 4
	elseif c >= 240 and c <= 244 then
		return 4
    end
    
end

-- Method:          GRM.UTF8Len ( string )
-- What it Does:    Returns the length of an input string.
-- Purpose:         So the addon can read the proper character length of text no matter the language.
GRM.UTF8Len = function ( s )

	local pos = 1
	local bytes = string.len (s)
	local len = 0

	while pos <= bytes do
		len = len + 1
		pos = pos + GRM.UTF8char ( s , pos )
	end

	return len
end

-- Method:          GRM.GetNumLetters ( text )
-- What it Does:    Returns the character count of the numLetters of a text string
-- Purpose:         Compatibility with ALL languages    
GRM.GetNumLetters = function ( text )
    local c = 0;

    if text ~= nil then
        c = GRM.UTF8Len ( text );
    end

    return c;
end


-- Method:          GRM.CapitalizeFirst ( string )
-- What it Does:    Formats the string properly to have the first letter of the word/name capitalized
-- Purpose:         Cleanup formatting of a name to prevent human error protection.
GRM.CapitalizeFirst = function( text )
    local count = 1;
    local byteCount = text:byte ( 1 );
    if byteCount == 195 or byteCount == 165 or byteCount == 208 then                -- Special cahracters some can be 2 bytes in length and they are given a value of 195 or 165 in Lua return
        count = 2;
    end
    
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].selectedLang < 9 then
        text = string.upper ( string.sub ( text , 1 , count ) ) .. string.sub ( text , count + 1 );
    end
    return text;
end

-- Method:          GRM.FormatInputName ( string )
-- What it Does:    Formats the name to proper pronoun form, but only if a non Asian character language
-- Purpose:         Huaman error protection on player input.
GRM.FormatInputName = function ( name )

    if string.find ( name , " " ) ~= nil then
        -- space found, multi word!!!
        local tempName = string.lower ( name );
        local result = "";
        local number = select ( 2 , string.gsub ( tempName , "%s" , "" ) );

        for i = 1 , number do
            result = result .. GRM.CapitalizeFirst ( string.sub ( tempName , 1 , string.find ( tempName , " " ) -1 ) .. " " );
            tempName = string.sub ( tempName , string.find ( tempName , " " ) + 1 );
            if i == number then
                result = result .. GRM.CapitalizeFirst( tempName );
            end
        end
        name = result;
    else
        name = GRM.CapitalizeFirst( name );
    end
    local byteCount = name:byte ( 1 );
    local count = 1;
    if byteCount == 195 or byteCount == 165 or byteCount == 208 then                -- Special cahracters some can be 2 bytes in length and they are given a value of 195 or 165 in Lua return
        count = 2;
    end
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].selectedLang < 9 and not string.find ( name , " " ) then
        name = string.sub ( name , 1 , count ) .. string.lower ( string.sub ( name , count + 1 ) );
    end
    return name;
end

-- Method           GRM.Trim ( string )
-- What it Does:    Removes the white space at front and at tail of string.
-- Purpose:         Cleanup strings for ease of logic control, as needed.
GRM.Trim = function ( str )
    if str == nil then
        return nil;
    else
        return ( str:gsub ( "^%s*(.-)%s*$" , "%1" ) );
    end
end

-- Method:          GRM.StringToCharArray ( string [, bool]);
-- What it Does:    Converts a string into a char array, and has the option to remove all indexes of a given char
-- Purpose:         More easily cleanup strings, especially when sending data back and forth using the '?' separator
GRM.StringToCharArray = function( text , removeChar )
    local result = {};
    local temp = "";
    for i = 1 , #text do
        temp = string.sub ( text , i , i );
        -- Remove the char by ignoring it
        if not removeChar or ( removeChar and removeChar ~= temp ) then
            table.insert ( result , string.sub ( text , i , i ) );
        end
    end
    return result;
end

-- Method:          GRM.GetWordArrayFromString ( string , string )
-- What it Does:    Returns an array of words or items that were divided by characters
-- Purpose:         Cleanup code for parsing back the sendAddonMessage strings.
GRM.GetWordArrayFromString = function ( text , divider )
    local result = {};
    while string.find ( text , divider ) ~= nil do
        table.insert ( result , string.sub ( text , 1 , string.find ( text , divider ) - 1 ) );
        text = string.sub ( text , string.find ( text , divider ) + 1 );
    end
    if #text > 0 then
        table.insert ( result , text );
    end
    return result;
end

-- Method:          GRM.ConvertStringNumArrayToBoolArray ( array )
-- What it Does:    Sets an array corresponding index to booleans matching another array. 1 = true, 0 = false
-- Purpose:         Slim the sendaddonMessages a bit by converting bools to 1s and 0s and then converting back on the other end.
GRM.ConvertStringNumArrayToBoolArray = function ( listOfNum )
    local result = {};
    for i = 1 , #listOfNum do
        if listOfNum[i] == "1" then
            result[i] = true;
        else
            result[i] = false;
        end
    end
    return result;
end 

-- Method:          GRM.ResetTempLogs()
-- What it Does:    Empties the arrays of the reporting logs
-- Purpose:         Logs are used to build changes in the guild and then to cleanly report them in order.
GRM.ResetTempLogs = function()
    GRM_G.TempNewMember = {};
    GRM_G.TempInactiveReturnedLog = {};
    GRM_G.TempLogPromotion = {};
    GRM_G.TempLogDemotion = {};
    GRM_G.TempLogLeveled = {};
    GRM_G.TempLogNote = {};
    GRM_G.TempLogONote = {};
    GRM_G.TempRankRename = {};
    GRM_G.TempRejoin = {};
    GRM_G.TempBannedRejoin = {};
    GRM_G.TempLeftGuild = {};
    GRM_G.TempLeftGuildPlaceholder = {};
    GRM_G.TempNameChanged = {};
    GRM_G.TempEventReport = {};
    GRM_G.TempEventRecommendKickReport = {};
    GRM_G.TempEventRecommendPromotionReport = {};
    GRM_G.TempEventRecommendDemotionReport = {};
end

------------------------------------
------ TIME TRACKING TOOLS ---------
--- TIMESTAMPS , TIMEPASSED, ETC. --
------------------------------------

GRM.CalendarGetDate = function()
    return GRM.GetTodaysDate();
end

-- Method:          GRM.IsLeapYear(int)
-- What it Does:    Returns true if the given year is a leapYear
-- Purpose:         For this addon, the calendar date selection, allows it to know to produce 29 days on leap year.
GRM.IsLeapYear = function ( yearDate )
    if ( ( ( yearDate % 4 == 0 ) and ( yearDate % 100 ~= 0 ) ) or ( yearDate % 400 == 0 ) ) then
        return true;
    else
        return false;
    end
end

-- Method:          GRM.GetHoursSinceLastOnline(int)
-- What it Does:    Returns the total numbner of hours since the player last logged in at given index position of guild roster
-- Purpose:         For player management to notify addon user of too much time has passed, for recommendation to kick,
GRM.GetHoursSinceLastOnline = function ( index , isOnline )
    local years , months, days, hours = GetGuildRosterLastOnline ( index );
    local invalidData = false;
    if years == nil then
        years = 0;
    end
    if months == nil then
        months = 0;
    end
    if days == nil then
        days = 0;
    end
    if hours == nil then
        hours = 0;
    end
    if ( years == 0 ) and ( months == 0 ) and ( days == 0 ) and ( hours == 0) then
        if isOnline then
            hours = 0.5;    -- This can be any value less than 1, but must be between 0 and 1, to just make the point that total number of hrs since last login is < 1
        else
            invalidData = true;
        end
    end
    if not invalidData then
        return math.floor ( ( years * 8766 ) + ( months * 730 ) + ( days * 24 ) + hours );
    else
        return 0;
    end
end

-- Method:          GRM.IsValidSubmitDate ( int , int , boolean )
-- What it Does:    Returns true if the submission date is valid (not an untrue day or in the future)
-- Purpose:         Check to ensure the wrong date is not submitted on accident.
GRM.IsValidSubmitDate = function ( daySelected , monthSelected , yearSelected , IsLeapYearSelected )
    local closeButtons = true;
    local month , day , year = select ( 2 , GRM.CalendarGetDate() ); 
    local numDays;

    if monthSelected == 1 or monthSelected == 3 or monthSelected == 5 or monthSelected == 7 or monthSelected == 8 or monthSelected == 10 or monthSelected == 12 then
        numDays = 31;
    elseif monthSelected == 2 and IsLeapYearSelected then
        numDays = 29;
    elseif monthSelected == 2 then
        numDays = 28;
    else
        numDays = 30;
    end
    if daySelected > numDays then
        closeButtons = false;
    end
    
    if closeButtons then
        if ( year < yearSelected ) or ( year == yearSelected and month < monthSelected ) or ( year == yearSelected and month == monthSelected and day < daySelected ) then
            GRM.Report ( GRM.L ( "Player Does Not Have a Time Machine!" ) );
            closeButtons = false;
        end
    end

    if closeButtons == false then
        GRM.Report ( GRM.L ( "Please choose a valid DAY" ) );
    end
    return closeButtons;
end

-- Method:          GRM.TimeStampToEpoch(timestamp)
-- What it Does:    Converts a given timestamp: "22 Mar '17" into Epoch Seconds time (UTC timezone)
-- Purpose:         On adding notes, epoch time is considered when calculating how much time has passed, for exactness and custom dates need to include it.
GRM.TimeStampToEpoch = function ( timestamp , IsStartOfDay )
    -- Parsing Timestamp to useful data.
    if not timestamp then
        return;
    end

    timestamp = GRM.GetCleanTimestamp ( timestamp );
    local year = GRM.GetEventYear ( timestamp );
    local leapYear = GRM.IsLeapYear ( year );
    local month = monthEnum [ GRM.GetEventMonth ( timestamp ) ];
    local day = GRM.GetEventDay ( timestamp );

    -- End timestamp Parsing... 
    local hour , minute , seconds;
    if IsStartOfDay then
        hour = 0;
        minute = 1;
        seconds = 0;
    else
        hour , minute = GetGameTime();
        seconds = date ( '*t' ).sec;
    end

    -- calculate the number of seconds passed since 1970 based on number of years that have passed.
    local totalSeconds = 0;
    for i = year - 1 , 1970 , -1 do
        if GRM.IsLeapYear ( i ) then
            totalSeconds = totalSeconds + ( 366 * 24 * 3600 ); -- leap year = 366 days - 1 extra day
        else
            totalSeconds = totalSeconds + ( 365 * 24 * 3600 ); -- 365 days in normal year
        end
    end
    
    -- Now lets calculate how much time this year...
    local monthDays = daysBeforeMonthEnum [ tostring ( month ) ];
    if month > 2 and leapYear then -- Adding 1 for the leap year
        monthDays = monthDays + 1;
    end
    -- adding month days so far this year to result so far.
    totalSeconds = totalSeconds + ( monthDays * 24 * 3600);

    -- The rest is easy... as of now, I will not import hours/minutes/seconds, but I will leave the calculations in place in case need arises.
    totalSeconds = totalSeconds + ( ( day - 1 ) * 24 * 3600 );  -- days
    totalSeconds = totalSeconds + ( hour * 3600 );
    totalSeconds = totalSeconds + ( minute * 60 );
    totalSeconds = totalSeconds + seconds;
    
    return totalSeconds;
end

-- Method:          GRM.GetTimestamp()
-- What it Does:    Reports the current moment in time in a much more clear, concise, pretty way. Example: "9 Feb '17 1:36pm" instead of 09/02/2017/13:36
-- Purpose:         Just for cleaner presentation of the results. Also, need to report based on server time. In-game API only returns hour/min, not month and day. This resolves that.
GRM.GetTimestamp = function()
    -- Time Variables
    local months = { "Jan" , "Feb" , "Mar" , "Apr" , "May" , "Jun" , "Jul" , "Aug" , "Sep" , "Oct" , "Nov" , "Dec" };
    local hour, minutes = GetGameTime();
    local month, day, year = select ( 2 , GRM.CalendarGetDate() );
    local stampMonth = months [ month ];
    local time = "";
    local array = {};

    year = year - 2000;
    array = { day , month , year , hour , minutes };
    time = ( day .. " " .. stampMonth .. " '" .. year .. " " .. GRM.GetFormatTime ( hour , minutes ) )

    return time , array;
end

-- Method:          GRM.GetTimePassedInZone ( oldTimestamp , int , bool )
-- What it Does:    Reports back how many days, hours, minutes, or seconds has passed since player has been in a zone (days is unrealistic I know but some people might put in a 24hr+ session, so you never know.)
-- Purpose:         Time tracking to keep track of elapsed time in a zone.
GRM.GetTimePassedInZone = function ( oldTimestamp , exactSeconds , includeAll )
    local totalSeconds = exactSeconds or ( time() - oldTimestamp );
    local year = math.floor ( totalSeconds / 31536000 ); -- seconds in a year
    local yearTag = GRM.L ( "Year" );
    local month = math.floor ( ( totalSeconds % 31536000 ) / 2592000 ); -- etc. 
    local monthTag = GRM.L ( "Month" );
    local days = math.floor ( ( totalSeconds % 2592000) / 86400 );
    local dayTag = GRM.L ( "Day" );
    local hours = math.floor ( ( totalSeconds % 86400 ) / 3600 );
    local hoursTag = GRM.L ( "Hour" );
    local minutes = math.floor ( ( totalSeconds % 3600 ) / 60 );
    local minutesTag = GRM.L ( "Minute" );
    local seconds = math.floor ( ( totalSeconds % 60) );
    local secondsTag = GRM.L ( "Second" );
    
    local timestamp = "";
    if year > 1 then
        yearTag = GRM.L ( "Years" );
    end
    if month > 1 then
        monthTag = GRM.L ( "Months" );
    end
    if days > 1 then
        dayTag = GRM.L ( "Days" );
    end
    if hours > 1 then
        hoursTag = GRM.L ( "Hours" );
    end
    if minutes > 1 then
        minutesTag = GRM.L ( "Minutes" );
    end
    if seconds ~= 1 then
        secondsTag = GRM.L ( "Seconds" );
    end

    if year > 0 or month > 0 or days > 0 then

        if year > 0 then
            timestamp = ( GRM.L ( "{num} {custom1}" , nil , nil , year , yearTag ) );
        end
        if month > 0 then
            timestamp = ( timestamp .. " " .. GRM.L ( "{num} {custom1}" , nil , nil , month , monthTag ) );
        end
        if days > 0 then
            timestamp = ( timestamp .. " " .. GRM.L ( "{num} {custom1}" , nil , nil , days , dayTag ) );
        end
    end

    if ( year == 0 and month == 0 and days == 0 ) or includeAll then
        if hours > 0 or minutes > 0 then
            if hours > 0 then
                timestamp = ( timestamp .. " " .. GRM.L ( "{num} {custom1}" , nil , nil , hours , hoursTag ) );
            end
            if minutes > 0 then
                timestamp = ( timestamp .. " " .. GRM.L ( "{num} {custom1}" , nil , nil , minutes , minutesTag ) );
            end
        end
        if ( hours == 0 and minutes == 0 ) or includeAll then
            if not includeAll or #timestamp == 0 then
                timestamp = ( GRM.L ( "{num} {custom1}" , nil , nil , seconds , secondsTag ) );
            else
                timestamp = ( timestamp .. " " .. GRM.L ( "{num} {custom1}" , nil , nil , seconds , secondsTag ) );
            end
        end
    end
    
    return timestamp;
end

-- Method:          GRM.EpochToDateFormat( int )
-- What it Does:    It takes an epoch timestamp and converts it into a string format as desired.
-- Purpose:         Epoch is very exact, to the second. It is nice to store that info than hard to interpret, non-mathematical text, for a computer. \
--                  This is just easy formatting for human consumption
GRM.EpochToDateFormat = function ( epochstamp )
    
    local timeTable = date( "*t" , epochstamp );
    local day = tostring ( timeTable.day );
    local month = monthEnum2 [ '' .. timeTable.month .. '' ];
    local year = string.sub ( tostring ( timeTable.year ) , 3 );    

    return ( day .. " " .. month .. " '" .. year );
end

-- Method:          GRM.EpochToDateConvertedForm( int )
-- What it Does:    It takes an epoch timestamp and converts it into a string format as desired, but also transformed into the timstamp format of the player's settings
-- Purpose:         Epoch is very exact, to the second. It is nice to store that info than hard to interpret, non-mathematical text, for a computer. \
--                  This is just easy formatting for human consumption
GRM.EpochToDateConvertedForm = function ( epochstamp )
    
    local timeTable = date( "*t" , epochstamp );
    local day = timeTable.day
    local month = timeTable.month
    local year = ( timeTable.year - 2000 ) -- only need last 2 numbers 

    return GRM.FormatTimeStamp ( { day , month , year } , false , false );
end

-- Method:          GRM.GetFullDate ( int , int , int , int )
-- What it Does:    Returns the properly formatted date by Blizz
-- Purpose:         Useful to get the full date for many obvious reasons I'd think
GRM.GetFullDate = function ( weekday , month , day , year )
	local weekdayName = CALENDAR_WEEKDAY_NAMES[weekday];
	local monthName = CALENDAR_FULLDATE_MONTH_NAMES[month];
	return weekdayName, monthName, day, year, month;
end

-- Method:          GRM.GetHoursSinceTimestamp( int )
-- What it Does:    Returns the number of hours that has passed since as a flat integer. Anything less than 1hr returns zero
-- Purpose:         To be able to return the number of hours since the given time.
GRM.GetHoursSinceTimestamp = function ( epochStamp )
    local totalSeconds = time() - epochStamp;

    return math.floor ( totalSeconds / 3600 );
end

-- Method:          GRM.GetTimePassedUsingStringStamp()
-- What it Does:    Returns the Years, hours, and days that have passed since the given timestamp ( In format "day mon 'year")
-- Purpose:         Honestly, simpler solution than build a solution to parse through epoch time, since I don't need hours, minutes, seconds.
GRM.GetTimePassedUsingStringStamp = function ( timestamp )

    local startYear = tonumber ( string.sub ( string.match ( timestamp , "'%d%d" ) , 2 ) ) + 2000;
    local startMonth = monthEnum [ string.match ( timestamp , "%a+" ) ];
    local startDay = tonumber ( string.match ( timestamp , "%d+" ) );
    local month , day , year = select ( 2 , GRM.CalendarGetDate() );
    local LeapYear = GRM.IsLeapYear ( year );
    local result = { 0 , 0 , 0 , "" , 0 };           -- resultYear, resultMonth , resultDay;

    -- Narrow down the year!
    if year > startYear then                -- If this event happened in a previous year.
        result[1] = year - startYear;
        if month < startMonth then          -- Event is less than a year!
            result[1] = result[1] - 1;
        elseif month == startMonth then
            -- Need to check the day!
            if day < startDay then
                result[1] = result[1] - 1;
            else
                result[1] = year - startYear;   -- If >= then it counts as 1 year.
            end
        else                                -- month > start meaning it's been a year.
            result[1] = year - startYear;
        end
    else
        result[1] = 0;
    end

    -- Ok, now let's get the month! Much easier!
    if month < startMonth then
        result[2] = month + ( 12 - startMonth );
        if day < startDay then          -- Not quite 1 month
            result[2] = result[2] - 1;
        end
    elseif month == startMonth then
        if startYear == year then
            result[2] = 0;
        else
            if day < startDay then
                result[2] = 11;
            else
                result[2] = 0;
            end
        end
    else                        -- month > start 
        if day < startDay then
            result[2] = ( month - startMonth ) - 1;
        else
            result[2] = month - startMonth;
        end
    end

    -- Finally, let's do the day!
    if day < startDay then
        -- Gonna have to take leap year into account now!
        local tempMonth = month;
        if tempMonth == 12 then
            tempMonth = 1;
        end
        result[3] = day + ( daysInMonth [ tostring ( tempMonth ) ] - startDay );
        if LeapYear then
            result[3] = result[3] + 1;
        end
    else
        result[3] = day - startDay;
    end

    --Final text report
    if result[1] > 0 then
        if result[1] == 1 then
            
            result[4] = result[4] .. GRM.L ( "{num} year" , nil , nil , result[1] ) .. " ";
        else
            result[4] =  result[4] .. GRM.L ( "{num} years" , nil , nil , result[1] ) .. " ";
        end
    end
    if result[2] > 0 then
        if result[2] == 1 then
            result[4] = result[4] .. GRM.L ( "{num} month" , nil , nil , result[2] ) .. " ";
        else
            result[4] = result[4] .. GRM.L ( "{num} months" , nil , nil , result[2] ) .. " ";
        end
    end
    if result[3] > 0 and result[1] == 0 then            -- To avoid including days if you have more than 1 year just need year and months
        if result[3] == 1 then
            result[4] = result[4] .. GRM.L ( "{num} day" , nil , nil , result[3] ) .. " ";
        else
            result[4] = result[4] .. GRM.L ( "{num} days" , nil , nil , result[3] ) .. " ";
        end
    end
    -- Clear off any white space.
    if result[1] == 0 and result[2] == 0 and result[3] == 0 then
        result[4] = GRM.L ( "< 1 day" );
    else
        result[4] = GRM.Trim ( result[4] );
    end
    return result;
end

-- Method:          GRM.GetTimePassedUsingEpochTime ( int )
-- What it Does:    Returns a table with the time that has passed
-- Purpose:         Easily know how much time has passed since a given event based on the epochstamp
GRM.GetTimePassedUsingEpochTime = function ( epochSeconds )
    local result = {};
    local epochTimeStamp = GRM.EpochToDateFormat ( epochSeconds );
    local details = GRM.GetTimePassedUsingStringStamp ( epochTimeStamp );

    result.Years = details[1];
    result.Months = details[2];
    result.Days = details[3];
    result.timestamp = details[4];
    
    return result;
end

-- Method:          GRM.GetTimePlayerHasBeenMember ( string )
-- What it does:    Parses the string of the player date they joined the guild the most recent, and then obtains how long they have been a member.
-- Purpose:         To display useful info on how long the player has been a member of the guild.
GRM.GetTimePlayerHasBeenMember = function ( name )
    local player = GRM_GuildMemberHistory_Save[GRM_G.F][GRM_G.guildName][name];
    local result = "";
    
    if player ~= nil then
        if #player.joinDate > 0 then
            result = GRM.GetTimePassedUsingStringStamp ( string.sub ( player.joinDate[#player.joinDate] , 1 , string.find ( player.joinDate[#player.joinDate] , "'" ) + 2 ) );
            result = result[4];
        end
    end
    return result;
end

-- Method:          GRM.HoursReport(int)
-- What it Does:    Reports as a string the time passed since player last logged on.
-- Purpose:         Cleaner reporting to the log, and it just reports the lesser info, no seconds and so on.
GRM.HoursReport = function ( hours )
    local result = ""

    if hours ~= nil then
        local years = math.floor ( hours / 8760 );
        local months = math.floor ( ( hours % 8760 ) / 730 );
        local days = math.floor ( ( hours % 730 ) / 24 );

        -- Continue calculations.
        local hours = math.floor ( ( ( hours % 8760 ) % 730 ) % 24 );

        if years >= 1 then
            if years > 1 then
                result = result .. "" .. GRM.L ( "{num} yrs" , nil , nil , years ) .. " ";
            else
                result = result .. "" .. GRM.L ( "{num} yr" , nil , nil , years ) .. " ";
            end
        end

        if months >= 1 then
            if years > 0 then
                result = GRM.Trim ( result ) .. ", ";
            end
            if months > 1 then
                result = result .. "" .. GRM.L ( "{num} mos" , nil , nil , months ) .. " ";
            else
                result = result .. "" .. GRM.L ( "{num} mo" , nil , nil , months ) .. " ";
            end
        end

        if days >= 1 then
            if months > 0 then
                result = GRM.Trim ( result ) .. ", ";
            end
            if days > 1 then
                result = result .. "" .. GRM.L ( "{num} days" , nil , nil , days ) .. " ";
            else
                result = result .. "" .. GRM.L ( "{num} day" , nil , nil , days ) .. " ";
            end
        end

        if hours >= 1 and years < 1 and months < 1 then  -- No need to give exact hours on anything over than a month, just the day is good enough.
            if days > 0 then
                result = GRM.Trim ( result ) .. ", ";
            end
            if hours > 1 then
                result = result .. "" .. GRM.L ( "{num} hrs" , nil , nil , hours ) .. " ";
            else
                result = result .. "" .. GRM.L ( "{num} hr" , nil , nil , hours ) .. " ";
            end
        end
    end

    if result == "" or result == nil then
        result = GRM.L ( "< 1 hour" );
    end
    return GRM.Trim ( result );
end

-- Method:          GRM.GetNumHoursTilRecommend( int )
-- What it Does:    Returns the number of hours need to match the given numMonths time passed
-- Purpose:         Useful for checking if the player has been, for example, offline X number of months, if the time has passed, since the server gives time in hours since last online.
GRM.GetNumHoursTilRecommend = function( numMonths )
    local month , day , year = select ( 2 , GRM.CalendarGetDate() );
    -- Error protection
    if month == 0 or day == 0 then
        return nil;
    end
    local totalDays = 0;
    local numYears = math.floor ( numMonths / 12 );
    numMonths = numMonths % 12;
    local monthReference = month - numMonths;

    -- ok let's calculate the month index
    if monthReference < 1 then
        monthReference = 12 + month - numMonths;
    end

    -- Add up the total days...
    if numMonths > 0 then
        totalDays = day;               -- This sets the initial number, which is this month.
        if numMonths >= month then
            totalDays = totalDays + daysBeforeMonthEnum[ tostring ( month ) ];                                      -- Counts all the days of this year
            totalDays = totalDays + ( 365 - daysBeforeMonthEnum[ tostring ( monthReference ) ] ) - day;             -- Counts all of the days from the reference month X months ago til end of the year

            -- Check Leap Year
            if month > 2 and GRM.IsLeapYear ( year ) and numYears == 0 then -- Adding 1 for the leap year   -- If the year > 1 then the end of this function will tally it auto for each year, if not it is calculated here.
                totalDays = totalDays + 1;
            end

        else                                                                                                        -- Ex: if today is May, 11 months ago, reference month is June last year
            totalDays = totalDays + ( daysBeforeMonthEnum[ tostring ( month ) ] - daysBeforeMonthEnum[ tostring ( monthReference ) ] ) - day;
            if monthReference <= 2 and month > 2 and GRM.IsLeapYear ( year ) and numYears == 0  then
                totalDays = totalDays + 1;
            end
        end
    end
    for i = 0 , numYears - 1 do
        if GRM.IsLeapYear ( year - i ) then
            totalDays = totalDays + 1;
        end
    end
    return ( totalDays + ( 365 * numYears ) ) * 24
end

-- Method:          GRM.GetTimestampBasedOnTimePassed ( array )
-- What it Does:    Returns an array that contains a string timestamp of the date based on the timepassed, as well as the epochstamp corresponding to that date
-- Purpose:         Incredibly necessary for join date and promo date tagging with proper dates for display and for sync.
GRM.GetTimestampBasedOnTimePassed = function ( dateInfo )
    local stampYear = dateInfo[3];
    local stampMonth = dateInfo[2];
    local stampDay = dateInfo[1];
    local stampHour = dateInfo[4];
    local hour, minutes = GetGameTime();
    local month , day , year = select ( 2 , GRM.CalendarGetDate() );
    local LeapYear = GRM.IsLeapYear ( year );
    local time = "";                     -- Generic stamp placeholder
    if not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].twentyFourHrScale then
        time = "12:01am";
    else
        time = "00:01" .. GRM.L ( "24HR_Notation" );
    end
    
    -- Adjust the year back for how many years passed
    year = year - stampYear - 2000;
    
    -- The month now... Must be a number for 1-12 for corresponding month index
    if month - stampMonth > 0 then
        month = month - stampMonth;
    else
        month = 12 - ( stampMonth - month );
        year = year - 1;
    end

    -- Day
    if day - stampDay > 0 then
        day = day - stampDay;
    else
        local daysInSelectedMonth = daysInMonth[ tostring ( month ) ];
        if LeapYear and month == 2 then
            daysInSelectedMonth = daysInSelectedMonth + 1;
        end
        day = daysInSelectedMonth - ( stampDay - day );
        month = month - 1;
        if month == 0 then
            month = 12;
            year = year - 1;
        end
    end

    -- Hour
    if hour - stampHour > 0 then
        hour = hour - stampHour;
    else
        hour = 24 - ( stampHour - hour );
        day = day - 1;
        if day == 0 then
            -- First, need to determine month now.
            month = month - 1;
            if month == 0 then
                month = 12;
                year = year - 1;
            end
            local dim = daysInMonth[ tostring ( month ) ];      -- Days In Month = dim
            if LeapYear and month == 2 then
                dim = dim + 1;
            end
            day = dim - ( stampDay - day );
        end
    end

    -- We know that it is within hours now.
    if ( stampYear == 0 and stampMonth == 0 and stampDay == 0 ) then
        -- It's the same day! Use current timestamp!!!!

        time = GRM.GetFormatTime ( hour , minutes );
    end
    
    local timestamp = day .. " " .. monthEnum2[ tostring ( month ) ] .. " '" .. year;
    local arrayFormat = { day , month , year , hour , minutes };
    return { timestamp .. " " .. time , GRM.TimeStampToEpoch ( " " .. timestamp , true )  , arrayFormat };
end
 
-- Method:          GRM.FormatTimeStamp( string , int )
-- What it Does:    Returns the timestamp in a format designated by the player
-- purpose:         Give player proper timestamp format options.
GRM.FormatTimeStamp = function ( timestamp , includeHour , removeYear )

    local day = 0;
    local monthNum = 0;
    local year = 0;
    local typeForm = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].dateFormat;
    local month = "";

    if type ( timestamp ) == "string" then
        timestamp = GRM.GetCleanTimestamp ( timestamp ); -- ensure proper formatting
        -- Default format = 12 Mar '18
        day = string.match ( timestamp , "%d+" );
        if #day == 1 then
            day = "0" .. day;
        end
        month = GRM.GetEventMonth ( timestamp );
        monthNum = tostring ( monthEnum [ month ] ); 
        if #monthNum == 1 then
            monthNum = "0" .. monthNum;
        end
        year = string.sub ( string.match ( timestamp , "'%d%d" ) , 2 );

    elseif type ( timestamp ) == "table" then

        day = timestamp[1];
        monthNum = timestamp[2];
        year = timestamp[3];
        if year < 10 then
            year = "0" .. tostring ( year );
        end
        month = monthEnum2[ tostring ( monthNum ) ];

    end
    local result = "";  

    if typeForm == 1 then                               -- 12 Mar '18
        if removeYear then
            result = day .. " " .. GRM.L ( month );
        else
            result = day .. " " .. GRM.L ( month ) .. " '" .. year;
        end
    elseif typeForm == 2 then                           -- 12 Mar 18
        if removeYear then
            result = day .. " " .. GRM.L ( month );
        else
            result = day .. " " .. GRM.L ( month ) .. " " .. year; 
        end
    elseif typeForm == 3 then                           -- 12-Mar-2018
        if removeYear then
            result = day .. "-" .. GRM.L ( month );
        else
            result = day .. "-" .. GRM.L ( month ) .. "-20" .. year;
        end
    elseif typeForm == 4 then                          -- 03-12-18
        if removeYear then
            result = day .. "-" .. monthNum
        else
            result = day .. "-" .. monthNum .. "-" .. year;
        end
    elseif typeForm == 5 then                          -- 03/12/18
        if removeYear then
            result = day .. "/" .. monthNum;
        else
            result = day .. "/" .. monthNum .. "/" .. year;
        end
    elseif typeForm == 6 then                          -- 03.12.18
        if removeYear then
            result = day .. "." .. monthNum;
        else
            result = day .. "." .. monthNum .. "." .. year;
        end
    elseif typeForm == 7 then                          -- 03.12.2018
        if removeYear then
            result = day .. "." .. monthNum;
        else
            result = day .. "." .. monthNum .. ".20" .. year;
        end
    elseif typeForm == 8 then                           -- Mar 12 '18
        if removeYear then
            result = GRM.L ( month ) .. " " .. day;
        else
            result = GRM.L ( month ) .. " " .. day .. " '" .. year;
        end
    elseif typeForm == 9 then                           -- Mar 12 18
        if removeYear then
            result = GRM.L ( month ) .. " " .. day;
        else
            result = GRM.L ( month ) .. " " .. day .. " " .. year;
        end
    elseif typeForm == 10 then                          -- Mar-12-2018
        if removeYear then
            result = GRM.L ( month ) .. "-" .. day;
        else
            result = GRM.L ( month ) .. "-" .. day .. "-20" .. year;
        end
    elseif typeForm == 11 then                           -- 12-03-18
        if removeYear then
            result = monthNum .. "-" .. day;
        else
            result = monthNum .. "-" .. day .. "-" .. year;
        end
    elseif typeForm == 12 then                           -- 12/03/18
        if removeYear then
            result = monthNum .. "/" .. day;
        else
            result = monthNum .. "/" .. day .. "/" .. year;
        end
    elseif typeForm == 13 then                           -- 12.3.18
        if removeYear then
            result = monthNum .. "." .. day;
        else
            result = monthNum .. "." .. day .. "." .. year;
        end
    elseif typeForm == 14 then                           -- 12.3.2018
        if removeYear then
            result = monthNum .. "." .. day;
        else
            result = monthNum .. "." .. day .. ".20" .. year;
        end
    elseif typeForm == 15 then                           -- 2018-03-12
        if removeYear then
            result = monthNum .. "-" .. day;
        else
            result = "20" .. year .. "-" .. monthNum .. "-" .. day;
        end    
    end

    if includeHour then
        if type ( timestamp ) == "string" then
            result = result .. " " .. string.sub ( timestamp , string.find ( timestamp , "'" ) + 4 );
        else
            if timestamp[4] ~= nil and timestamp[5] ~= nil then
                result = result .. " " .. GRM.GetFormatTime ( timestamp[4] , timestamp[5] );
            end
        end
    end
    return result;
end

-- Method:          GRM.GetFormatTime ( string , string )
-- What it Does:    Returns the time of day in the proper 24hr or 12hr format
-- Purpose:         To give players the option for time display formatting, but also to ensure 24hr/12hr standards are there as typically in the EU people often use the 24hr clock, whilst in the US it is the 12hr clock.
GRM.GetFormatTime = function ( hour , min )
    local morning = true;
    local amOrpm = GRM.L ( "pm" );
    local result = "";

    if hour ~= nil and min ~= nil then
        -- Swap from military time if set to 12hr        
        if ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser] ~= nil and not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].twentyFourHrScale ) then
            if hour > 12 then
                hour = hour - 12;
                morning = false;
            elseif hour == 12 then
                morning = false;
            elseif hour == 0 then
                hour = 12;
            end

            if morning then
                amOrpm = GRM.L ("am" );
            end 
        else
            amOrpm = GRM.L ( "24HR_Notation" );
        end

        -- Formatting...
        if min < 10 then
            min = ( "0" .. min ); -- Example, if it was 6:09, the minutes would only be "9" not "09" - so this looks better.
        end 
        if hour < 10 then
            hour = ( "0" .. hour );
        end

        result = hour .. GRM.L ( "HourBreak" ) .. min .. amOrpm;
    end

    return result;
end

-- Method:          GRM.ConvertGenericTimestampToIntValues(string)
-- What it Does:    Returns an array with the day , month , year values set
-- Purpose:         To be used in converting old method of parsing string timestamps to int values for easier conversion and less processing
GRM.ConvertGenericTimestampToIntValues = function ( timeStamp )
    local tempStamp = GRM.GetCleanTimestamp ( timeStamp ); -- ensure proper formatting
    local day = GRM.GetEventDay ( tempStamp );         -- set the day
    local month = monthEnum [ GRM.GetEventMonth ( tempStamp ) ];      -- set the month
    local year = GRM.GetEventYear ( tempStamp );                    -- set the year

    if day ~= nil and month ~= nil and year ~= nil then
        return { day , month , year };
    else
        return nil;
    end
end

-- Method:          GRM.GetDefaultTimestampUsingIntValues ( int , int , int )
-- What it Does:    Converts int indexes to proper default timestamp format
-- Purpose:         For easy comparison check of a string.
GRM.GetDefaultTimestampUsingIntValues = function ( day , month , year )
    if year < 10 then
        year = "0" .. tostring ( year );
    end
    return day .. " " .. monthEnum2 [ tostring ( month ) ] .. " '" .. year;
end

------------------------------------
------ END OF TIME METHODS ---------
------------------------------------

------------------------------------
------ UI FORMATTING HELPERS -------
------------------------------------

-- Method:          GRM.AllignTwoColumns ( array , int )
-- What it Does:    Takes the array of strings and then alligns them by fontsize and width...
-- Purpose:         For UI aesthetics and allignment purposes
GRM.AllignTwoColumns = function ( listOfStrings , spacing )
    -- First, determine longest string width of headers
    local result = "\n";
    local modifier = 7;
    local size = 17.08;
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showLineNumbers then
        modifier = 8.5;
    end

    GRM_UI.GRM_GroupInfo.InvisFontStringWidthCheck:SetFont ( GRM_G.FontChoice , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logFontSize + size - modifier ); -- Reset it in case it has changed.
    GRM_UI.GRM_GroupInfo.InvisFontStringWidthCheck:SetText( listOfStrings[1][1] );         -- need to set string to measurable value
    local longestW = GRM_UI.GRM_GroupInfo.InvisFontStringWidthCheck:GetWidth();
    for i = 2 , #listOfStrings do
        GRM_UI.GRM_GroupInfo.InvisFontStringWidthCheck:SetText( listOfStrings[i][1] );
        local tempW = GRM_UI.GRM_GroupInfo.InvisFontStringWidthCheck:GetWidth();
        if tempW > longestW then
            longestW = tempW;
        end
    end

    -- Now, establish the total necessary width - We are setting spacing at 5.
    longestW = longestW + spacing;
    for i = 1 , #listOfStrings do
        GRM_UI.GRM_GroupInfo.InvisFontStringWidthCheck:SetText( listOfStrings[i][1] );
        while GRM_UI.GRM_GroupInfo.InvisFontStringWidthCheck:GetWidth() < longestW do
            GRM_UI.GRM_GroupInfo.InvisFontStringWidthCheck:SetText ( GRM_UI.GRM_GroupInfo.InvisFontStringWidthCheck:GetText() .. " " );       -- Keep adding spaces until it matches
        end
        if listOfStrings[i][2] ~= nil then
            result = result .. GRM_UI.GRM_GroupInfo.InvisFontStringWidthCheck:GetText() .. listOfStrings[i][2];
        else
            result = result .. GRM_UI.GRM_GroupInfo.InvisFontStringWidthCheck:GetText() .. GRM.L ( "Unknown" ); -- Error protection if importing nil value
        end
        if i ~= #listOfStrings then
            result = result .. "\n";
        end
    end
    return result;
end


------------------------------------
------ END OF FORMATTING HELPERS ---
------------------------------------


------------------------------------
---- MousOver Methods --------------
------------------------------------

-- Method:          GRM.GetRosterName ( button , boolean )
-- Method:          To return the current mouseover name of the given button, with the server appended.
-- Purpose:         Need the full name-server, untruncated, to be able to correctly identify the player in database, in case 2 players with same name, but diff. servers
GRM.GetRosterName = function ( button , isMouseClick )
    local name = "";
    if not GRM_G.pause or isMouseClick then
        local memberInfo = button.memberInfo;
        if memberInfo ~= nil then
            name = memberInfo.name;
            if name ~= nil and memberInfo.guid ~= nil then
                if string.find ( name , "-" ) == nil then
                    name = name .. "-" .. GRM_G.realmName;
                end
            end
        end
    end
    return name;
end

-- Method:          GRM.InitializeRosterButtons()
-- What it Does:    Initializes, one time, the script handlers for the roster frames
-- Purpose:         So main player popup window appears properly 
GRM.InitializeRosterButtons = function()
    local cFrame = CommunitiesFrame;
    local memberFrame = cFrame.MemberList;
    local buttons = memberFrame.ListScrollFrame.buttons;
    for i = 1 , #buttons do
        buttons[i]:HookScript ( "OnEnter" , function ( self )
            
            if cFrame:GetSelectedClubId() == GRM_G.gClubID and GetMouseFocus() == self then
                local name = GRM.GetRosterName ( buttons[i] , false  );
                if name ~= "" and name ~= nil then

                    local memberInfo = self.memberInfo
                    GRM.MemberListBlizTooltip_Update ( buttons[i] , false , memberInfo.classID , memberInfo.name , memberInfo.guildRank , memberInfo.race , memberInfo.level , memberInfo.presence , memberInfo.zone , memberInfo.memberNote , memberInfo.officerNote );
                    GRM_G.currentName = name;
                    GRM.SubFrameCheck();

                    if cFrame:GetSelectedClubId() == GRM_G.gClubID then
                        GRM.PopulateMemberDetails ( name , memberInfo );
                        if not GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
                            if not GRM_G.CurrentPinCommunity then
                                if GRM_UI.MemberDetailFrame:IsVisible() then
                                    GRM_UI.GRM_MemberDetailMetaData:SetPoint ( "TOPLEFT" , GRM_UI.MemberDetailFrame , "TOPRIGHT" , -4 , 5 );
                                else
                                    GRM_UI.GRM_MemberDetailMetaData:SetPoint ( "TOPLEFT" , CommunitiesFrame , "TOPRIGHT" , 22.5 , 5 );
                                end
                                GRM_G.CurrentPinCommunity = true;
                            end
                            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showMouseoverRetail then
                                if GRM_G.guildName ~= "" then
                                    GRM_UI.GRM_MemberDetailMetaData:Show();
                                end
                            end
                        end
                    end

                end
            end
        end);
        
        buttons[i]:HookScript ( "OnClick" , function ( self , button )
            if button == "LeftButton" then
                local nameCopy = false;
                if IsShiftKeyDown() then
                    nameCopy = true;
                end

                local name = GRM.GetRosterName ( self , true );
                if name ~= "" and name ~= nil then
                    if not nameCopy then
                        if name ~= GRM_G.currentName and StaticPopup1:IsVisible() and GRM_UI.GRM_PopupWindow:IsVisible() then
                            StaticPopup1:Hide();
                        end
                        GRM_G.currentName = name;
                        GRM.SubFrameCheck();
                        if cFrame:GetSelectedClubId() == GRM_G.gClubID then
                            GRM.PopulateMemberDetails ( name , self.memberInfo );
                            GRM_G.pause = true;
                            if not GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
                                if not GRM_G.CurrentPinCommunity then
                                    if GRM_UI.MemberDetailFrame:IsVisible() then
                                        GRM_UI.GRM_MemberDetailMetaData:SetPoint ( "TOPLEFT" , GRM_UI.MemberDetailFrame , "TOPRIGHT" , -4 , 5 );
                                    else
                                        GRM_UI.GRM_MemberDetailMetaData:SetPoint ( "TOPLEFT" , CommunitiesFrame , "TOPRIGHT" , 22.5 , 5 );
                                    end
                                    GRM_G.CurrentPinCommunity = true;
                                end
                                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showMouseoverRetail then
                                    if GRM_G.guildName ~= "" then
                                        GRM_UI.GRM_MemberDetailMetaData:Show();
                                    end
                                end
                            end
                        end
                    else
                        GRM.GR_Roster_Click ( name );
                    end
                end
            end
        end);

        buttons[i]:HookScript ( "OnUpdate" , GRM.RosterButton_OnUpdate );
    end
end


-- Method:          GRM.RosterButton_OnUpdate ( buttonObject , int )
-- What it Does:    On each frame refresh it does a check (at max 20FPS), and checks to see if the name has changed, and then updates
--                  the player mouseover popup window if so.
-- Purpose:         The main purpose is this allows the frames to be updated if the mouse is stationary over the hybrid scrollframe button, but the mousewheels still scrolls
--                  This is important because the core logic happens "OnEnter" rather than on update.
GRM.RosterButton_OnUpdate = function ( self , elapsed )
    GRM_G.ButtonRosterTimer = GRM_G.ButtonRosterTimer + elapsed;
    if GRM_G.ButtonRosterTimer > 0.05 and GetMouseFocus() == self and CommunitiesFrame:GetSelectedClubId() == GRM_G.gClubID then
        local cFrame = CommunitiesFrame;
        local name = GRM.GetRosterName ( self , true );

        if name ~= "" and name ~= nil and name ~= GRM_G.currentName and not GRM_G.pause then

            GRM_G.currentName = name;
            GRM.SubFrameCheck();

            if cFrame:GetSelectedClubId() == GRM_G.gClubID and GRM_G.F ~= "" then
                GRM.PopulateMemberDetails ( name  , self.memberInfo );
                if not GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
                    if not GRM_G.CurrentPinCommunity then
                        if GRM_UI.MemberDetailFrame:IsVisible() then
                            GRM_UI.GRM_MemberDetailMetaData:SetPoint ( "TOPLEFT" , GRM_UI.MemberDetailFrame , "TOPRIGHT" , -4 , 5 );
                        else
                            GRM_UI.GRM_MemberDetailMetaData:SetPoint ( "TOPLEFT" , cFrame , "TOPRIGHT" , 22.5 , 5 );
                        end
                        GRM_G.CurrentPinCommunity = true;
                    end
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showMouseoverRetail then
                        if GRM_G.guildName ~= "" then
                            GRM_UI.GRM_MemberDetailMetaData:Show();
                        end
                    end
                end
            end
        end

        GRM_G.ButtonRosterTimer = 0;
    end
end

-- Method:          GRM.UpdateMemberDetailNameClassColor()
-- What it Does:    Changes the default Yellow/Gold coloring in Classic to Class colors
-- Purpose:         To enable class coloring in Classic, which is a very useful feature
GRM.UpdateMemberDetailNameClassColor = function()
    
    if GRM_G.BuildVersion < 23000 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].colorizeClassicRosterNames then
        local class = select ( 11 , GetGuildRosterInfo ( GRM_G.RosterSelection ) );
        local colors = GRM.GetClassColorRGB ( class , false );

        GuildMemberDetailName:SetTextColor ( colors[1] , colors[2] , colors[3] );
    end

end

-- Method:          GRM.InitializeOldRosterButtons( string )
-- What it Does:    Initializes, one time, the script handlers for the roster frames
-- Purpose:         So main player popup window appears properly 
GRM.InitializeOldRosterButtons = function( classicSpecific )
    local buttonString = "";
    if classicSpecific then
        buttonString = classicSpecific;
    else
        buttonString = GRM_UI.OldRosterButtonName;
    end

    for i = 1 , GRM_UI.ContainerButtonCount do
        local button = _G[ buttonString .. i ];

        button:HookScript ( "OnEnter" , function ( self )
            if not GRM_G.pause and GetMouseFocus() == self then
                local name , rank , _ , level , _ , zone , memberNote , officerNote , _ , _ , classFile , _ , _ , isMobile , _ , _ , guid = GetGuildRosterInfo ( button.guildIndex );
                
                if name ~= "" and name ~= nil  then
                    if GRM_G.BuildVersion >= 80000 then
                        local presence = 1;
                        if isMobile then
                            presence = 2;
                        end
                        GRM.MemberListBlizTooltip_Update ( button , true , classFileIDEnum[classFile] , name , rank , raceIDEnum [ select ( 4 , GetPlayerInfoByGUID ( guid ) ) ] , level , presence , zone , memberNote , officerNote );
                    end
                    
                    GRM_G.currentName = name;
                    GRM_G.RosterSelection = button.guildIndex;

                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showMouseoverOld then    
                        GRM.SubFrameCheck();
                        GRM.PopulateMemberDetails ( name );
                        if not GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
                            if GRM_G.CurrentPinCommunity then
                                if GRM_UI.MemberDetailFrame:IsVisible() then
                                    GRM_UI.GRM_MemberDetailMetaData:SetPoint ( "TOPLEFT" , GRM_UI.MemberDetailFrame , "TOPRIGHT" , -4 , 5 );
                                else
                                    GRM_UI.GRM_MemberDetailMetaData:SetPoint ( "TOPLEFT" , GRM_UI.GuildRosterFrame , "TOPRIGHT" , -4 , 5 );
                                end
                                GRM_G.CurrentPinCommunity = false;            
                            end
                            if GRM_G.guildName ~= "" then
                                GRM_UI.GRM_MemberDetailMetaData:Show();
                            end
                        end
                    end
                end
            end
        end);

        button:HookScript ( "OnClick" , function ( self , clickButton )
            if clickButton == "LeftButton" then
                local nameCopy = false;
                if IsShiftKeyDown() then
                    nameCopy = true;
                end

                local name = GetGuildRosterInfo ( self.guildIndex );
                if name ~= "" and name ~= nil then
                    if not nameCopy then

                        if name ~= GRM_G.currentName and StaticPopup1:IsVisible() and GRM_UI.GRM_PopupWindow:IsVisible() then
                            StaticPopup1:Hide();
                        end
                        GRM_G.currentName = name;
                        GRM_G.RosterSelection = self.guildIndex;
                        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showMouseoverOld then
                            GRM.SubFrameCheck();
                            GRM.PopulateMemberDetails ( name );
                            if GRM_UI.MemberDetailFrameClassic:IsVisible() then
                                GRM_G.pause = true;
                            else
                                GRM_G.pause = false;
                            end
                            if not GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
                                if GRM_G.CurrentPinCommunity then
                                    if GRM_UI.MemberDetailFrameClassic:IsVisible() then
                                        GRM_UI.GRM_MemberDetailMetaData:SetPoint ( "TOPLEFT" , GRM_UI.MemberDetailFrameClassic , "TOPRIGHT" , -4 , 5 );
                                    else
                                        GRM_UI.GRM_MemberDetailMetaData:SetPoint ( "TOPLEFT" , GRM_UI.GuildRosterFrame , "TOPRIGHT" , -4 , 5 );
                                    end
                                    GRM_G.CurrentPinCommunity = false;
                                end
                                if GRM_G.guildName ~= "" then
                                    GRM_UI.GRM_MemberDetailMetaData:Show();
                                end
                            end
                        end

                        GRM.UpdateMemberDetailNameClassColor();

                    else
                        GRM.GR_Roster_Click ( name );
                    end
                end
            end
        end);

        button:HookScript ( "OnUpdate" , GRM.OldRosterButton_OnUpdate );

        button:HookScript ( "OnLeave" , function()
            if GameTooltip:IsVisible() then
                GameTooltip:Hide();
            end
        end);

        -- if GRM_G.BuildVersion < 40000 then
        --     button:HookScript ( "OnValueChanged" , GRM.RecolorText );
        -- end
    end
end

-- Method:          GRM.RecolorText()
-- What it Does:    Changes the color of the roster to the default Classic color to the player's class
-- Purpose:         Modernizes the chat a bit
GRM.RecolorText = function( button )
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].colorizeClassicRosterNames then
        if button.guildIndex ~= nil then
            local isOnline , _ , class = select ( 9 , GetGuildRosterInfo ( button.guildIndex ) );
            if isOnline then
                local colors= GRM.GetClassColorRGB ( class , false );

                _G[ button:GetName() .. "Name" ]:SetTextColor ( colors[1] , colors[2] , colors[3] );
            end
        end
    end
end

-- Method:          GRM.OldRosterButton_OnUpdate ( buttonObject , int )
-- What it Does:    On each frame refresh it does a check (at max 20FPS), and checks to see if the name has changed, and then updates
--                  the player mouseover popup window if so.
-- Purpose:         The main purpose is this allows the frames to be updated if the mouse is stationary over the hybrid scrollframe button, but the mousewheels still scrolls
--                  This is important because the core logic happens "OnEnter" rather than on update.
GRM.OldRosterButton_OnUpdate = function ( self , elapsed )
    GRM_G.ButtonRosterTimer2 = GRM_G.ButtonRosterTimer2 + elapsed;
    if GRM_G.ButtonRosterTimer2 > 0.05 then
        local name = GetGuildRosterInfo ( self.guildIndex );
        
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showMouseoverOld and self:IsMouseOver() then

            if name ~= "" and name ~= nil and name ~= GRM_G.currentName and not GRM_G.pause then

                GRM_G.currentName = name;
                GRM_G.RosterSelection = self.guildIndex;
                GRM.SubFrameCheck();
                GRM.PopulateMemberDetails ( name );
                if not GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
                    if GRM_G.CurrentPinCommunity then
                        if GRM_UI.MemberDetailFrame:IsVisible() then
                            GRM_UI.GRM_MemberDetailMetaData:SetPoint ( "TOPLEFT" , GRM_UI.MemberDetailFrame , "TOPRIGHT" , -4 , 5 );
                        else
                            GRM_UI.GRM_MemberDetailMetaData:SetPoint ( "TOPLEFT" , GRM_UI.GuildRosterFrame , "TOPRIGHT" , -4 , 5 );
                        end
                        GRM_G.CurrentPinCommunity = false;
                    end
                    if GRM_G.guildName ~= "" then
                        GRM_UI.GRM_MemberDetailMetaData:Show();
                    end
                end
            end
        end

        GRM_G.ButtonRosterTimer2 = 0;
    end
end

-- Method:          GRM.GetAllTooltipText()
-- What it Does:    Grabs all parts of the Gametooltip to clone them
-- Purpose:         Universal compatibility of GRM with other addons that modify communityframe tooltip
GRM.GetAllTooltipText = function()
    local result = {};
    if GameTooltip:IsVisible() then
        local count;

        if #GRM_G.ToolTipTextLeft < 24 then
            count = #GRM_G.ToolTipTextLeft;
            
            -- This might seem better to be placed into a table or array, the ultimate issue is by placing it into an array it decalres it as a global, so I first need to check if it exits.
            -- There is no way to call a fontstring with a known API (like GetClickFrame()) and as such they need to be manually selected.
            if 1 > count and GameTooltipTextLeft1 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft1 );
            elseif 2 > count and GameTooltipTextLeft2 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft2 );
            elseif 3 > count and GameTooltipTextLeft3 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft3 );
            elseif 4 > count and GameTooltipTextLeft4 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft4 );
            elseif 5 > count and GameTooltipTextLeft5 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft5 );
            elseif 6 > count and GameTooltipTextLeft6 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft6 );
            elseif 7 > count and GameTooltipTextLeft7 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft7 );
            elseif 8 > count and GameTooltipTextLeft8 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft8 );
            elseif 9 > count and GameTooltipTextLeft9 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft9 );
            elseif 10 > count and GameTooltipTextLeft10 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft10 );
            elseif 11 > count and GameTooltipTextLeft11 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft11 );
            elseif 12 > count and GameTooltipTextLeft12 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft12 );
            elseif 13 > count and GameTooltipTextLeft13 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft13 );
            elseif 14 > count and GameTooltipTextLeft14 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft14 );
            elseif 15 > count and GameTooltipTextLeft15 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft15 );
            elseif 16 > count and GameTooltipTextLeft16 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft16 );
            elseif 17 > count and GameTooltipTextLeft17 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft17 );
            elseif 18 > count and GameTooltipTextLeft18 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft18 );
            elseif 19 > count and GameTooltipTextLeft19 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft19 );
            elseif 20 > count and GameTooltipTextLeft20 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft20 );
            elseif 21 > count and GameTooltipTextLeft21 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft21 );
            elseif 22 > count and GameTooltipTextLeft22 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft22 );
            elseif 23 > count and GameTooltipTextLeft23 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft23 );
            elseif 24 > count and GameTooltipTextLeft24 ~= nil then
                table.insert ( GRM_G.ToolTipTextLeft , GameTooltipTextLeft24 );
            end
        end

        count = #GRM_G.ToolTipTextRight;
        if #GRM_G.ToolTipTextRight < 24 then
            if 1 > count and GameTooltipTextRight1 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight1 );
            elseif 2 > count and GameTooltipTextRight2 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight2 );
            elseif 3 > count and GameTooltipTextRight3 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight3 );
            elseif 4 > count and GameTooltipTextRight4 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight4 );
            elseif 5 > count and GameTooltipTextRight5 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight5 );
            elseif 6 > count and GameTooltipTextRight6 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight6 );
            elseif 7 > count and GameTooltipTextRight7 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight7 );
            elseif 8 > count and GameTooltipTextRight8 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight8 );
            elseif 9 > count and GameTooltipTextRight9 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight9 );
            elseif 10 > count and GameTooltipTextRight10 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight10 );
            elseif 11 > count and GameTooltipTextRight11 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight11 );
            elseif 12 > count and GameTooltipTextRight12 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight12 );
            elseif 13 > count and GameTooltipTextRight13 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight13 );
            elseif 14 > count and GameTooltipTextRight14 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight14 );
            elseif 15 > count and GameTooltipTextRight15 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight15 );
            elseif 16 > count and GameTooltipTextRight16 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight16 );
            elseif 17 > count and GameTooltipTextRight17 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight17 );
            elseif 18 > count and GameTooltipTextRight18 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight18 );
            elseif 19 > count and GameTooltipTextRight19 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight19 );
            elseif 20 > count and GameTooltipTextRight20 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight20 );
            elseif 21 > count and GameTooltipTextRight21 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight21 );
            elseif 22 > count and GameTooltipTextRight22 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight22 );
            elseif 23 > count and GameTooltipTextRight23 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight23 );
            elseif 24 > count and GameTooltipTextRight24 ~= nil then
                table.insert ( GRM_G.ToolTipTextRight , GameTooltipTextRight24 );
            end
        end

        local tempHolder = {};
        local colors1, colors2
        local goodToAdd = false;
        for i = 1 , #GRM_G.ToolTipTextLeft do

            colors1 , colors2 = nil , nil;
            goodToAdd = false;
            colors1 = { GRM_G.ToolTipTextLeft[i]:GetTextColor() };
            colors2 = { GRM_G.ToolTipTextLeft[i]:GetTextColor() };
            tempHolder = { { GRM_G.ToolTipTextLeft[i]:GetText() , 0 , 0 , 0 } , { GRM_G.ToolTipTextRight[i]:GetText() , 0 , 0 , 0 } };
            if ( tempHolder[1][1] ~= nil and GRM.Trim ( tempHolder[1][1] ) ~= "" ) or ( tempHolder[2][1] ~= nil and GRM.Trim ( tempHolder[2][1] ) ~= "" ) then

                if tempHolder[1][1] ~= nil and colors1 ~= nil then
                    tempHolder[1][2] = colors1[1];
                    tempHolder[1][3] = colors1[2];
                    tempHolder[1][4] = colors1[3];
                    goodToAdd = true;
                end
                if tempHolder[2][1] ~= nil and colors2 ~= nil then
                    tempHolder[2][2] = colors2[1];
                    tempHolder[2][3] = colors2[2];
                    tempHolder[2][4] = colors2[3];
                    goodToAdd = true;
                end
                if goodToAdd then
                    table.insert ( result , tempHolder );
                end
            end
        end
    end
    return result;
end

-- Method:          GRM.RebuildToolTip ( ToolTip , table )
-- What it Does:    Looks at the stored data in the array and if index 1 and index 2 of 2D array both have values, it adds a double line to the tooltip, otherwise a single
-- Purpose:         Blizz seems to have broken the tooltip in BFA on the guild community frame and it has never fully been re-implemented properly. This is so my rebuilding
--                  of the tooltip maintains compatibility with other addons that affect the in-game roster tooltip, notably addons like Raider.io
GRM.RebuildToolTip = function( tTip , leftAndRightTable )
    tTip:AddLine ( " " );
    for i = 6 , #leftAndRightTable do
        if leftAndRightTable[i][2][1] ~= nil then
            if leftAndRightTable[i][1][1] ~= nil and leftAndRightTable[i][1][1] ~= "" then
                tTip:AddDoubleLine ( leftAndRightTable[i][1][1] , leftAndRightTable[i][2][1] , leftAndRightTable[i][1][2] , leftAndRightTable[i][1][3] , leftAndRightTable[i][1][4] , leftAndRightTable[i][2][2] , leftAndRightTable[i][2][3] , leftAndRightTable[i][2][4] );
            else
                tTip:AddDoubleLine ( " " , leftAndRightTable[i][2][1] , leftAndRightTable[i][2][2] , leftAndRightTable[i][2][3] , leftAndRightTable[i][2][4] );
            end
        elseif leftAndRightTable[i][1][1] ~= nil then
            tTip:AddLine ( leftAndRightTable[i][1][1] , leftAndRightTable[i][1][2] , leftAndRightTable[i][1][3] , leftAndRightTable[i][1][4] );
        end
    end
end

-- Method:          GRM.MemberListBlizTooltip_Update ( buttonObject , bool )
-- What it Does:    It rebuilds the mouseover tooltip on the community frame for the GUILD ONLY
-- Purpose:         This only occurs during the OnUpdate of the community frame hybrid scrollframe member list button.
--                  This is to make up for a deficiency where Blizz does not update the tooltip if cursor is stationary
GRM.MemberListBlizTooltip_Update = function( self , isOldRoster , classID , name , guildRank , race , level , presence , zone , memberNote , officerNote )
    local cFrame = CommunitiesFrame;
    if isOldRoster or cFrame:GetSelectedClubId() == GRM_G.gClubID then
        local characterFormat = COMMUNITY_MEMBER_CHARACTER_INFO_FORMAT;
        local characterPresenceMobile = COMMUNITIES_PRESENCE_MOBILE_CHAT;
        local memberNoteFormat = COMMUNITY_MEMBER_NOTE_FORMAT;
        local toolTipMergeInfo = {};

        if classID then
            if GameTooltip:IsVisible() then
                toolTipMergeInfo = GRM.GetAllTooltipText();         -- Raider.io compatibility, and other addons...
            end
            GameTooltip:SetOwner ( self );
            local classInfo = "";
            local color = NORMAL_FONT_COLOR;
            if classID then
                classInfo = C_CreatureInfo.GetClassInfo ( classID );
                color = ( classInfo and RAID_CLASS_COLORS[classInfo.classFile] ) or NORMAL_FONT_COLOR;
            end
            GameTooltip:AddLine ( name , color.r , color.g , color.b );
            GameTooltip:AddLine ( guildRank or "" );
            
            if level and race and classID then
                local raceInfo = C_CreatureInfo.GetRaceInfo ( race );
                if raceInfo and classInfo then
                    GameTooltip:AddLine ( characterFormat:format ( level , raceInfo.raceName , classInfo.className ) , HIGHLIGHT_FONT_COLOR.r , HIGHLIGHT_FONT_COLOR.g , HIGHLIGHT_FONT_COLOR.b , true );
                end
            end
            
            if presence == Enum.ClubMemberPresence.OnlineMobile then
                GameTooltip:AddLine ( characterPresenceMobile , HIGHLIGHT_FONT_COLOR.r , HIGHLIGHT_FONT_COLOR.g , HIGHLIGHT_FONT_COLOR.b , true );
            elseif zone then
                GameTooltip:AddLine ( zone , HIGHLIGHT_FONT_COLOR.r , HIGHLIGHT_FONT_COLOR.g , HIGHLIGHT_FONT_COLOR.b , true);
            end
            
            if memberNote and #memberNote > 0 then
                GameTooltip:AddLine ( memberNoteFormat:format ( memberNote ) , NORMAL_FONT_COLOR.r , NORMAL_FONT_COLOR.g , NORMAL_FONT_COLOR.b , true );
            end

            if officerNote and #officerNote > 0 then
                GameTooltip:AddLine ( memberNoteFormat:format ( officerNote ) , NORMAL_FONT_COLOR.r , NORMAL_FONT_COLOR.g , NORMAL_FONT_COLOR.b , true );
            end

            if #toolTipMergeInfo > 6 then
                GRM.RebuildToolTip ( GameTooltip , toolTipMergeInfo );
            end
            
            GameTooltip:Show();
        end
    end
end

-- Method:          GRM.BuildGuildRosterHotkeyAndMacro()
-- What it Does:    Adds tooltip and creates macro compatibiltiy to hotkey the CTRL-J just like live servers
-- Purpose:         Continuity in experience with GRM from latest expansion live to Classic live
GRM.BuildGuildRosterHotkeyAndMacro = function()

    local hotkeyTemp = select ( 3 , GetBinding(197) );
    local listOfKeybinds = { "J" , ";" };
    local keybinds = "";

    if C_VoiceChat.GetPushToTalkBinding() == nil then
        C_Timer.After ( 0.5 , GRM.BuildGuildRosterHotkeyAndMacro );
        return;
    else

        local PushToTalkHotKey = C_VoiceChat.GetPushToTalkBinding()[1];

        -- Initialize the hook on the GuildMicroButton
        GuildMicroButton:HookScript ( "OnClick" , function ()
            if ( IsControlKeyDown() and IsInGuild() ) or GRM_G.BuildVersion < 80000 then
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showMouseoverOld then
                    GRM_G.CurrentPinCommunity = false;
                    GRM_UI.GRM_MemberDetailMetaData:SetPoint ( "TOPLEFT" , GRM_UI.GuildRosterFrame , "TOPRIGHT" , -4 , 5 );
                end
                if GRM_G.BuildVersion < 80000 then
                    FriendsFrame:Show();
                else
                    CommunitiesFrame:Hide();
                    GuildFrame_Toggle();
                    GuildFrame_TabClicked ( GuildFrameTab2 );
                end
            else
                GRM_G.CurrentPinCommunity = true;
                GRM_UI.GRM_MemberDetailMetaData:SetPoint ( "TOPLEFT" , CommunitiesFrame , "TOPRIGHT" , 22.5 , 5 );
            end
        end);

        GuildMicroButton:SetScript ( "OnEnter" , function( self )
            local hotkey = select ( 3 , GetBinding(197) );                  -- This is the hotkey to open guild and community interface.
            local tooltipTopLine = GRM.L ( "Guild & Communities" );

            if hotkey ~= nil then
                tooltipTopLine = tooltipTopLine .. " |CFFFFD100(" .. hotkey .. ")";
            end

            GameTooltip:SetOwner ( self , "ANCHOR_NONE" );
            GameTooltip:SetPoint( "BOTTOMLEFT" , GuildMicroButton , "TOPRIGHT" , 0 , 0 );   
            GameTooltip:AddLine( tooltipTopLine , 1 , 1 , 1 );
            if IsInGuild()then
                GameTooltip:AddLine ( GRM.L ( "|CFFE6CC7FCtrl-Click|r to open the Old Guild Roster Window" ) , 1 , 1 , 1 );
            end
            GameTooltip:Show();
        end);

        for i = 1 , #listOfKeybinds do

            -- First, determine if keybind has been set. If it has, let's just replicate
            if hotkeyTemp ~= nil and string.find ( hotkeyTemp , "-" ) == nil then       -- I don't want a triple action keybind
                keybinds = ( "CTRL-" .. hotkeyTemp );
            else
                keybinds = ( "CTRL-" .. listOfKeybinds[i] );
            end

            if GetBindingByKey ( keybinds ) ~= nil or keybinds == PushToTalkHotKey then
                keybinds = "";
            else
                break;
            end

        end

        -- MAX_ACCOUNT_MACROS 
        if GetNumMacros() < MAX_ACCOUNT_MACROS or GetMacroIndexByName ( "GRM_Roster" ) ~= 0 then

            local factionIcon = "inv_bannerpvp_01";
            if GRM_G.faction == "Alliance" then
                factionIcon = "inv_bannerpvp_02";
            end

            GRM.CreateMacro (
                "/run GuildFrame_Toggle()\n/run GuildFrame_TabClicked ( GuildFrameTab2 )" , 
                "GRM_Roster" , 
                factionIcon , 
                keybinds,
                false
            );
        else
            GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "Unable to create GRM hotkey macro. You currently are at the cap of {num} macros." , nil , nil , GetNumMacros() ) )
        end
    end
end

-- Method:          GRM.BuildGuildRosterHotkeyAndMacroCLASSIC()
-- What it Does:    Adds tooltip and creates macro compatibiltiy to hotkey the CTRL-J just like live servers
-- Purpose:         Continuity in experience with GRM from latest expansion live to Classic live
GRM.BuildGuildRosterHotkeyAndMacroCLASSIC = function()
    local keyBind = select ( 3 , GetBinding(184) );
    local listOfKeybinds = { "J" , ";" };

    if C_VoiceChat.GetPushToTalkBinding() == nil then
        C_Timer.After ( 0.5 , GRM.BuildGuildRosterHotkeyAndMacroCLASSIC );
        return;
    else
        local PushToTalkHotKey = C_VoiceChat.GetPushToTalkBinding()[1];
        if keyBind == nil then
            -- No keybind set, let's verify the first keybind is not in use anywhere...
            for i = 1 , #listOfKeybinds do
                if GetBindingByKey ( listOfKeybinds[i] ) == nil and listOfKeybinds[i] ~= PushToTalkHotKey then
                    SetBinding( listOfKeybinds[i] , "TOGGLEGUILDTAB" );
                    break;
                end
            end
        end
        
        SocialsMicroButton:SetScript ( "OnEnter" , function( self )
            local hotkeySocial = select ( 3 , GetBinding ( 181 ) );                  -- This is the hotkey to open guild and community interface.
            local hotkeyRoster = select ( 3 , GetBinding ( 184 ) ); 
            local tooltipTopLine = GRM.L ( "Social" );

            if hotkeySocial ~= nil then
                tooltipTopLine = tooltipTopLine .. " |CFFFFD100(" .. hotkeySocial .. ")|r"
            end

            if IsInGuild() and hotkeyRoster ~= nil then
                tooltipTopLine = tooltipTopLine .. " " .. GRM.L ( "Roster" ) .. " |CFFFFD100(" .. hotkeyRoster .. ")|r";
            end

            GameTooltip:SetOwner ( self , "ANCHOR_NONE" );
            GameTooltip:SetPoint( "BOTTOMLEFT" , SocialsMicroButton , "TOPRIGHT" , 0 , 0 );   
            GameTooltip:AddLine( tooltipTopLine , 1 , 1 , 1 );
            GameTooltip:AddLine ( NEWBIE_TOOLTIP_SOCIAL , 1 , 0.82 , 0 , 1 , true );
            GameTooltip:Show();
        end);
    end
end

-- Method:          GRM.ConfigureOnlineStatusText()
-- What it Does:    So you don't need to reprocess this over and over, it translates it all just one time.
-- Purpose:         Just resource saving.
GRM.ConfigureOnlineStatusText = function()
    local list = { "( Active )" , "( AFK )" , "( Busy )" , "( Mobile )" , "( Offline )" };

    for i = 1 , #list do
        GRM_G.Status[i] = GRM.L ( list[i] );
    end

end

-- Method:              GRM.RosterFrame()
-- What it Does:        Acts as the OnUpdate handler for hiding the frame when necessary and keeping the player presence status and time in zone up to date.
-- Purpose:             Quality of Life UI controls!
GRM.RosterFrame = function()
    if GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ] ~= nil then

        if GRM_G.pause and not GRM_UI.GRM_MemberDetailMetaData:IsVisible() and ( ( GRM_G.CurrentPinCommunity and not GRM_UI.MemberDetailFrame:IsVisible() ) or ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showMouseoverOld and not GRM_G.CurrentPinCommunity and not GuildFrame:IsVisible() ) ) then
            if not GRM_UI.MemberDetailFrame:IsVisible() then
                GRM_G.pause = false;
                if GRM_G.BuildVersion < 80000 then
                    GRM.ClearRosterHighlights();
                end
            end
        end

        -- Ensure pinned to correct window.
        if ( ( GRM_G.CurrentPinCommunity and not CommunitiesFrame.MemberList.ListScrollFrame:IsMouseOver ( 4 , -20 , -4 , 30 ) ) or ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showMouseoverOld and not GRM_G.CurrentPinCommunity and not GRM_UI.GuildRosterContainer:IsMouseOver ( 4 , -20 , -4 , 30 ) ) ) and not GRM_G.pause then
            if ( ( ( GRM_G.CurrentPinCommunity and not CommunitiesFrame.MemberList.ListScrollFrame:IsMouseOver ( 4 , -20 , -4 , 30 ) ) or ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showMouseoverOld and not GRM_G.CurrentPinCommunity and not GRM_UI.GuildRosterContainer:IsMouseOver ( 4 , -20 , -4 , 30 ) ) ) and not DropDownList1MenuBackdrop:IsMouseOver ( 2 , -2 , -2 , 2 ) and not StaticPopup1:IsMouseOver ( 2 , -2 , -2 , 2 ) and not GRM_UI.GRM_MemberDetailMetaData:IsMouseOver ( 1 , -1 , -30 , 1 ) and not GRM_UI.GRM_CoreAltFrame:IsMouseOver( 5 , 5 , 5 , 35 ) and not GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrameSlider:IsMouseOver ( 1 , 1 , 1 , 3 ) ) or 
                ( not GRM_UI.GRM_MemberDetailMetaData:IsVisible() ) then  -- If player is moused over side window, it will not hide it!
                if GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
                    GRM.ClearAllFrames( true );
                end
            end
        end

        if GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName] ~= nil then

            -- updates zone change info on the fly, as well as online status.
            if ( time() - GRM_G.CommunitiesUpdateTimer ) > 5 then
                local fullName , zone , isOnline , _;

                for i = 1 , GRM.GetNumGuildies() do
                    fullName, _, _, _, _, zone, _, _, isOnline = GetGuildRosterInfo ( i );
                    if fullName == GRM_G.currentName then
                        GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].isOnline = isOnline;

                        if GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].zone ~= zone then
                            GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].zone = zone;
                            GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].timeEnteredZone = time();    -- Resets the time
                        end

                        break;
                    end
                end

                GRM_G.CommunitiesUpdateTimer = time();
            end

            -- Keep this data onUpdate handled...
            if GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].isOnline or GRM_G.currentName == GRM_G.addonUser then

                if not GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButton:IsVisible() then
                    if GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].zone ~= nil then
                        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoZoneText:SetText ( GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].zone );
                        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText2:SetText ( GRM.GetTimePassedInZone ( GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].timeEnteredZone ) );
                    end
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoText:Show();
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoZoneText:Show();
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText1:Show();
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText2:Show();
                end


                if GRM_G.currentName == GRM_G.addonUser or GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].status == 0 then
                    if GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:GetText() ~= GRM_G.Status[1] then
                        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:SetTextColor ( 0.12 , 1.0 , 0.0 , 1.0 );
                        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:SetText ( GRM_G.Status[1] );
                    end
                elseif GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].status == 1 then
                    if GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:GetText() ~= GRM_G.Status[2] then
                        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:SetTextColor ( 1.0 , 0.96 , 0.41 , 1.0 );
                        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:SetText ( GRM_G.Status[2] );
                    end
                elseif GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:GetText() ~= GRM_G.Status[3] then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:SetTextColor ( 0.77 , 0.12 , 0.23 , 1.0 );
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:SetText ( GRM_G.Status[3] );
                end
                if not GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:IsVisible() then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:Show();
                end
                
            elseif GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].isMobile then
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:SetTextColor ( 0.87 , 0.44 , 0.0 , 1.0 );
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:SetText ( GRM_G.Status[4] );
                if not GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:IsVisible() then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:Show();
                end

            elseif not GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].isOnline then
                if GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:GetText() ~= GRM_G.Status[5] then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:SetTextColor ( 0.5 , 0.5 , 0.5 , 1.0 );
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:SetText ( GRM_G.Status[5] );
                end
                if not GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:IsVisible() then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:Show();
                end

            else
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:Hide();    -- Don't know what to do!
            end
        else
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoText:Hide();
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoZoneText:Hide();
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText1:Hide();
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText2:Hide();
        end

        if ( GRM_G.CurrentPinCommunity and not CommunitiesFrame:IsVisible() ) or ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showMouseoverOld and not GRM_G.CurrentPinCommunity and not GuildFrame:IsVisible() ) then
            GRM.ClearAllFrames( true );         -- Reset frames and hide metadata frame...
        end
    end

end

-- Method:          GRM.AltButtonPos(int)
-- What it Does:    Returns the horizontal and vertical coordinates for the button position on frame
-- Purpose:         To adjust the position of the AddAlt button based on the number of alts.
GRM.AltButtonPos = function ( index )
    local result;
    if index == 0 then
        result = { 2 , -16 };
    elseif index == 1 then
        result = { 32 , -20 };
    elseif index == 2 then
        result = { -32 , -37 };
    elseif index == 3 then
        result = { 32 , -37 };
    elseif index == 4 then
        result = { -32 , -54 };
    elseif index == 5 then
        result = { 32 , -54 };
    elseif index == 6 then
        result = { -32 , -71 };
    elseif index == 7 then
        result = { 32 , -71 };
    elseif index == 8 then
        result = { -32 , -88 };
    elseif index == 9 then
        result = { 32 , -88 };
    elseif index == 10 then
        result = { -32 , -103 };
    elseif index == 11 then
        result = { 32 , -103 };
    else -- is 12+ alts
        result = { -32 , -120 };
    end
    return result;
end

-- Method:          GRM.GetLevelRange()
-- What it Does:    Returns a text string of the current level range
-- Purpose:         Clean and clear reporting to the player what levels will be reported to the log.
GRM.GetLevelRange = function()
    local result = "";
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].recordLevelUp then
        result = "|cffffd100" .. GRM.L ( "Reporting:" ) .. "|r|cff00ccff";
        local initialNumber = "";
        local caps = { 10 , 20 , 30 , 40 , 50 , 60 };
        local addon = "";

        local atLeastOne = false;
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].levelReportMin == GRM_G.LvlCap and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].levelFilters[#GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].levelFilters] then
            initialNumber = tostring ( GRM_G.LvlCap );
            atLeastOne = true;
        elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].levelReportMin < GRM_G.LvlCap then
            initialNumber = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].levelReportMin .. "-" .. GRM_G.LvlCap;
            atLeastOne = true;
        end

        local count = 0;
        for i = 1 , #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].levelFilters do
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].levelFilters[i] and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].levelReportMin > caps[i] then
                -- This is a standalone number...
                if count == 0 then
                    addon = caps[i];
                    count = 1;
                else
                    addon = addon .. " , " .. caps[i];
                end
            elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].levelReportMin <= caps[i] then
                break;
            end
        end
        if addon ~= "" then
            result = result .. " " .. addon;
            if atLeastOne then
                result = result .. " , " .. initialNumber;
            end
        elseif atLeastOne then
            result = result .. " " .. initialNumber;
        else
            result = GRM.L ( "No Levels to Report to Log" );
        end
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ScanningOptionsFrame.GRM_RosterMinLvlOverlayNoteText:SetTextColor ( 0 , 0.82 , 1 , 1 );
    else
        result = GRM.L ( "No Levels to Report to Log" );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ScanningOptionsFrame.GRM_RosterMinLvlOverlayNoteText:SetTextColor ( 1 , 0 , 0 , 1 );
    end

    return result;
end

-- Method:          GRM.CopyFromJoinDate()
-- What it Does:    Imports the promo date from a verified Join Date
-- Purpose:         Just another tool to ease player abilities for setting dates
GRM.CopyFromJoinDate = function()
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName];

    if player ~= nil then
        -- Set it to rank history
        player.rankHistory[#player.rankHistory][1] = player.rankName;
        player.rankHistory[#player.rankHistory][2] = player.verifiedJoinDate[1];
        player.rankHistory[#player.rankHistory][3] = GRM.TimeStampToEpoch ( player.verifiedJoinDate[1] );

        -- Verify it!
        player.verifiedPromoteDate[1] = player.rankHistory[#player.rankHistory][2];
        player.verifiedPromoteDate[2] = time();

        player.promoteDateUnknown = false;

        if player.isOnline then
            if player.zone ~= nil then
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoZoneText:SetText ( player.zone );                                     -- Zone
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText2:SetText ( GRM.GetTimePassedInZone ( player.timeEnteredZone ) );              -- Time Passed
            end
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoText:Show();
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoZoneText:Show();
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText1:Show();
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText2:Show();
        end

        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:SetText ( GRM.L ( "Promoted:" ) .. " " .. GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:GetText() );
        GRM_UI.GRM_MemberDetailMetaData.GRM_SetPromoDateButton:Hide();
        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:Show();

        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled then
            local syncRankFilter = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank;
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportAllRanks then
                syncRankFilter = GuildControlGetNumRanks() - 1;
            end
            GRMsync.SendMessage ( "GRM_SYNC" , GRM_G.PatchDayString .. "?GRM_PD?" .. syncRankFilter .. "?" .. player.name .. "?" .. GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:GetText() .. "?" .. tostring( player.verifiedPromoteDate[2] ) , "GUILD");
        end

        
        GRM_UI.Unpause();
        -- Update Audit Frames.
        if GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame:IsVisible() then
            GRM.RefreshAuditFrames ( true , true );
        end
    end

    GRM_UI.GRM_altDropDownOptions:Hide();
end

-- Method:          GRM.CopyFromPromoDate()
-- What it Does:    Imports the join date from a verified Promo Date
-- Purpose:         Just another tool to ease player abilities for setting dates
GRM.CopyFromPromoDate = function()
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName];
    local showBdayText = false;
    local rankButton = false;
    local formatBdayStamp = "";

    if player ~= nil then
        if #player.joinDate == 0 then
            table.insert ( player.joinDate , player.verifiedPromoteDate[1] );
            table.insert ( player.joinDateEpoch , GRM.TimeStampToEpoch ( player.verifiedPromoteDate[1] ) );
        else
            player.joinDate[#player.joinDate] = player.verifiedPromoteDate[1];
            player.joinDateEpoch[#player.joinDateEpoch] = GRM.TimeStampToEpoch ( player.verifiedPromoteDate[1] );
        end

        -- Verify it!
        player.verifiedJoinDate[1] = player.joinDate[#player.joinDate];
        player.verifiedJoinDate[2] = time();

        player.joinDateUnknown = false;

        if player.events[2][1][1] ~= 0 then
            showBdayText = true;
            formatBdayStamp = GRM.FormatTimeStamp ( player.events[2][3] , false , true );
        end

        local actualDate = GRM.Trim ( string.gsub ( GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:GetText() , GRM.L ( "Promoted:" ) , "" ) );
        local joinDate = ( "Joined: " .. actualDate );

        -- Update timestamp to officer note.
        local noteDestination = "none";
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].addTimestampToNote then
            for h = 1 , GRM.GetNumGuildies() do
                local guildieName ,_,_,_,_,_, note , oNote = GetGuildRosterInfo( h );
                if name == guildieName then

                    local noteDate = "";
                    local t = GRM.FormatTimeStamp ( player.verifiedJoinDate[1] , false );
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeTag then
                        noteDate = GRM_G.customHeaderJoin .. " " .. t;
                    else
                        noteDate = t;
                    end

                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 1 and GRM.CanEditOfficerNote() and ( oNote == "" or oNote == nil ) then
                        noteDestination = "officer";
                        GuildRosterSetOfficerNote( h , noteDate );
                        GRM_UI.GRM_MemberDetailMetaData.GRM_noteFontString2:SetText ( noteDate );
                        GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerOfficerNoteEditBox:SetText ( noteDate );
                    elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 2 and CanEditPublicNote() and ( note == "" or note == nil ) then
                        noteDestination = "public";
                        GuildRosterSetPublicNote ( h , noteDate );
                        GRM_UI.GRM_MemberDetailMetaData.GRM_noteFontString1:SetText ( noteDate );
                        GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerNoteEditBox:SetText ( noteDate );
                    elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 3 then
                        noteDestination = "custom";
                        GRM.SetJoinDateToCustomNote ( player.name , noteDate );
                    end
                    break;
                end
            end
        end


        -- Gotta update the event tracker date too!
        local date = GRM.ConvertGenericTimestampToIntValues ( string.sub ( joinDate , 9 ) );
        player.events[1][1][1] = date[1];
        player.events[1][1][2] = date[2];
        player.events[1][1][3] = date[3];
        player.events[1][2] = false;  -- Gotta Reset the "reported already" boolean!
        GRM.RemoveFromCalendarQue ( player.name , 1 , nil );

        -- Should definitely refresh the frame here if necessary... prevent mouse tooltip errors
        if GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame:IsVisible() then
            GRM.RefreshAddEventFrame()
        end

        if player.rankHistory[1][3] == 0 and not player.promoteDateUnknown then
            rankButton = true;
        end

        -- Need player index to get this info.
        if player.isOnline then
            if player.zone ~= nil then
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoZoneText:SetText ( player.zone );                                       -- Zone
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText2:SetText ( GRM.GetTimePassedInZone ( player.timeEnteredZone ) );                 -- Time Passed
            end
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoText:Show();
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoZoneText:Show();
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText1:Show();
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText2:Show();
        end

        -- Let's send the changes out as well!
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled then
            local syncRankFilter = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank;
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportAllRanks then
                syncRankFilter = GuildControlGetNumRanks() - 1;
            end
            GRMsync.SendMessage ( "GRM_SYNC" , GRM_G.PatchDayString .. "?GRM_JD?" .. syncRankFilter .. "?" .. player.name .. "?" .. joinDate .. "?" .. player.verifiedJoinDate[1] .. "?" .. tostring ( player.verifiedPromoteDate[2] ) .. "?" .. tostring ( player.verifiedJoinDate[2] ) .. "?" .. noteDestination , "GUILD");
        end

        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailJoinDateButton:Hide();
        GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:SetText ( actualDate )
        GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:Show();
        if rankButton then
            GRM_UI.GRM_MemberDetailMetaData.GRM_SetPromoDateButton:Show();
        else
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:Show();
        end

        -- Birthday Logic restoration
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showBDay and not GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:IsVisible() and not GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:IsVisible() then
            if showBdayText then
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:Hide();
                GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:SetText ( formatBdayStamp );
                GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:Show();
            else
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:Show();
                GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:Hide();
            end
        end

        GRM_UI.Unpause();
        -- Update the Audit Frames!
        if GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame:IsVisible() then
            GRM.RefreshAuditFrames ( true , true );
        end
        
    end

    GRM_UI.GRM_altDropDownOptions:Hide();
end

-- Method:          GRM.PopulateAltFrames( string )
-- What it Does:    This generates the alt frames in the main addon metadata detail frame
-- Purpose:         Clean formatting of the alt frames.
GRM.PopulateAltFrames = function ( playerName )
    -- let's start by prepping the frames.
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    local player = guildData[playerName];
    local listOfAlts = player.alts;
    local numAlts = #listOfAlts

    GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame:Show();
    GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame:Show();
    local scrollHeight = 0;
    local scrollWidth = 128;
    local buffer = 1;

    GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons = GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons or {};  -- Create a table for the Buttons.
    -- populating the window correctly.
    for i = 1 , numAlts do
        -- if font string is not created, do so.
        if not GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons[i] then
            local tempButton = CreateFrame ( "Button" , "GRM_AltAdded" .. i , GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame ); -- Names each Button 1 increment up
            GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons[i] = { tempButton , tempButton:CreateFontString ( "GRM_AltAddedText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) };
        end

        if i == numAlts and #GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons > numAlts then
            for j = numAlts + 1 , #GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons do
                GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons[j][1]:Hide();
            end
        end

        local AltButtons = GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons[i][1];
        local AltButtonsText = GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons[i][2];
        AltButtons:SetSize ( 65 , 15 );
        AltButtons:RegisterForClicks( "RightButtonDown" , "LeftButtonDown" );

        -- Check if main
        local name = GRM.SlimName ( player.alts[i][1] );
        if i == 1 then
            if player.alts[i][5] == true then  --- this person is the main!
                name = name .. "\n|cffff0000" .. GRM.L ( "(main)" );
                AltButtonsText:SetWordWrap ( true );
            end
        else
            AltButtonsText:SetWordWrap ( false );
        end
        AltButtonsText:SetText ( name );
        AltButtonsText:SetTextColor ( player.alts[i][2] , player.alts[i][3] , player.alts[i][4] , 1.0 );
        AltButtonsText:SetWidth ( 60 );

        if i == 1 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].fontModifier > 0 and string.find ( name , GRM.L ( "(main)" ) ) ~= nil then
            AltButtonsText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 7.5 );
        else
            AltButtonsText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 7.5 );
        end
        
        AltButtonsText:SetPoint ( "CENTER" , AltButtons );
        AltButtonsText:SetJustifyH ( "CENTER" );

        -- Logic
        AltButtons:SetScript ( "OnClick" , function ( self , button )
            if button == "RightButton" then
                -- Parse the button number, so the alt position can be identified...
                local altNum;
                local isMain = false;
                local width = 70;
                local height = 85;
                
                if tonumber ( string.sub ( self:GetName() , #self:GetName() - 1 ) ) ~= nil then
                    altNum = tonumber ( string.sub ( self:GetName() , #self:GetName() - 1 ) );
                else
                    altNum = tonumber ( string.sub ( self:GetName() , #self:GetName() ) );
                end

                -- Ok, populate the buttons properly...
                GRM_G.pause = true;
                GRM_UI.GRM_altDropDownOptions:ClearAllPoints();
                GRM_UI.GRM_altDropDownOptions:SetPoint( "TOPRIGHT" , self , "BOTTOM" );

                if string.find ( AltButtonsText:GetText() , GRM.L ( "(main)" ) ) == nil then
                    GRM_UI.GRM_altSetMainButtonText:SetText ( GRM.L ( "Set as Main" ) );
                    GRM_UI.GRM_altDropDownOptions.GRM_altOptionsText:SetText ( AltButtonsText:GetText() );
                else -- player IS the main... place option to Demote From Main rahter than set as main.
                    GRM_UI.GRM_altSetMainButtonText:SetText ( GRM.L ( "Set as Alt" ) );
                    isMain = true;
                    GRM_UI.GRM_altDropDownOptions.GRM_altOptionsText:SetText ( string.sub ( AltButtonsText:GetText() , 1 , string.find ( AltButtonsText:GetText() , "\n" ) - 1 ) );
                end

                if GRM_UI.GRM_altDropDownOptions.GRM_altOptionsText:GetStringWidth() + 15 > width then       -- For scaling the frame based on size of player name.
                    width = GRM_UI.GRM_altDropDownOptions.GRM_altOptionsText:GetStringWidth() + 15;
                end
                if GRM_UI.GRM_altSetMainButtonText:GetStringWidth() + 15 > width then
                    width = GRM_UI.GRM_altSetMainButtonText:GetStringWidth() + 15;
                end
                if GRM_UI.GRM_altRemoveButtonText:GetStringWidth() + 15 > width then
                    width = GRM_UI.GRM_altRemoveButtonText:GetStringWidth() + 15;
                end

                GRM_UI.GRM_altDropDownOptions:SetSize ( width , height );
                GRM_UI.GRM_altRemoveButton:SetSize ( 80 , 20 );
                GRM_UI.GRM_altSetMainButton:SetSize ( 80 , 20 );
                GRM_UI.DisableVerifyButton();
                GRM_UI.GRM_altRemoveButtonText:SetText ( GRM.L ( "Remove" ) );
                GRM_UI.GRM_ConfirmDropdownButton:Hide();
                GRM_UI.GRM_altDropDownOptions.GRM_MergeDateOverButton:Hide();
                GRM_UI.GRM_altRemoveButton:Show();
                GRM_UI.GRM_altDropDownOptions:Show();

                -- Set the Global info now!
                local currentPlayer = guildData[GRM_G.currentName];
                if currentPlayer ~= nil then
                    GRM_G.selectedAlt = { GRM_G.currentName , currentPlayer.alts[altNum][1] , GRM_G.guildName , isMain };
                end
            elseif button == "LeftButton" then
                if not IsShiftKeyDown() then
                    if GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailServerNameToolTip:IsVisible() and not GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailNameText:IsMouseOver ( 2 , -2 , -2 , 2 ) then
                        -- This makes the main window the alt that was clicked on! TempAltName is saved when mouseover action occurs.
                        if GRM_G.tempAltName ~= "" then
                            GRM.SelectPlayerOnRoster ( GRM_G.tempAltName );
                        end
                    end
                else
                    if GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailNameText:IsMouseOver ( 2 , -2 , -2 , 2 ) then
                        GRM_G.tempAltName = GRM_G.currentName;
                    end
                    if GRM_G.tempAltName ~= "" then
                        GRM.GR_Roster_Click ( GRM_G.tempAltName );
                        GRM_G.tempAltName = "";
                    end
                end
            end
        end);
        
        -- Now let's pin it!
        if i == 1 then
            AltButtons:SetPoint( "TOPLEFT" , GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame , 0 , - 1 );
            scrollHeight = scrollHeight + AltButtons:GetHeight();
        elseif i == 2 then
            AltButtons:SetPoint( "TOPLEFT" , GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons[i - 1][1] , "TOPRIGHT" , 1 , 0 );
        else
            AltButtons:SetPoint( "TOPLEFT" , GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons[i - 2][1] , "BOTTOMLEFT" , 0 , - buffer );
            if i % 2 ~= 0 then
                scrollHeight = scrollHeight + AltButtons:GetHeight() + buffer;
            end
        end
        AltButtons:Show();
    end

    -- Pin the button
    if numAlts == 0 then
        GRM_UI.GRM_AddAltButton2:ClearAllPoints();
        GRM_UI.GRM_AddAltButton2:SetPoint ( "TOP" , GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame , "TOP" , 0 , - 1 );
    elseif numAlts == 1 then
        GRM_UI.GRM_AddAltButton2:ClearAllPoints();
        GRM_UI.GRM_AddAltButton2:SetPoint ( "TOPLEFT" , GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons[1][1] , "TOPRIGHT" , 1 , 0 );
    else
        GRM_UI.GRM_AddAltButton2:ClearAllPoints();
        GRM_UI.GRM_AddAltButton2:SetPoint ( "TOP" , GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons[numAlts - 1][1] , "BOTTOM" , 0 , - buffer);
        if numAlts % 2 == 0 and numAlts > 12 then
            scrollHeight = scrollHeight + GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons[numAlts - 1][1]:GetHeight() + buffer;
        end
    end
    GRM_UI.GRM_AddAltButton2:Show();

    if numAlts > 12 then
        if GRM_UI.GRM_MemberDetailMetaData.GRM_GroupInviteButton:IsVisible() then
            GRM_UI.GRM_MemberDetailMetaData.GRM_SafeFromRulesButton:SetPoint ( "LEFT" , GRM_UI.GRM_MemberDetailMetaData.GRM_GroupInviteButton , "RIGHT" , 5 , 0 );
        end
    end

    -- Update the size -- it either grows or it shrinks!
    GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame:SetSize ( scrollWidth , scrollHeight );

    --Set Slider Parameters ( has to be done after the above details are placed )
    local scrollMax = ( scrollHeight - GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame:GetHeight() ) + ( buffer * .5 );
    if scrollMax < 0 then
        scrollMax = 0;
    end
    
    GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrameSlider:SetMinMaxValues ( 0 , scrollMax );
    -- Mousewheel Scrolling Logic
    GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame:EnableMouseWheel( true );
    GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame:SetScript( "OnMouseWheel" , function( _ , delta )
        local current = GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrameSlider:GetValue();
        
        if IsShiftKeyDown() and delta > 0 then
            GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrameSlider:SetValue ( 0 );
        elseif IsShiftKeyDown() and delta < 0 then
            GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrameSlider:SetValue ( scrollMax );
        elseif delta < 0 and current < scrollMax then
            GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrameSlider:SetValue ( current + 20 );
        elseif delta > 0 and current > 1 then
            GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrameSlider:SetValue ( current - 20 );
        end
    end);
    GRM_UI.GRM_CoreAltFrame:Show();
end

-- Method:          GRM.GetPlayerClass ( string )
-- What it Does:    Returns the string name of the class that is also region compatible
-- Purpose:         Useful in UI design to pull the class so you can pull the class RGB colors, but there can be other uses too.
GRM.GetPlayerClass = function ( playerName )
    local class = "";
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][playerName];

    if player ~= nil then
        class = player.class;
    else
        player = GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ][playerName]
        if player ~= nil then
            class = player.class;
        end
    end

    return class;
end

-- Method:          GRM.GetClassColorRGB ( string )
-- What it Does:    Returns the 0-1 RGB color scale for the player class
-- Purpose:         Easy class color tagging for UI feature.
GRM.GetClassColorRGB = function ( className , getHex )
    -- Defaults to color white if unable to identify.
    local result = { 1 , 1 , 1 };
    className = string.upper ( string.gsub ( className , " " , "" ) ); -- just to ensure formatitng properly
    if className == "DEATHKNIGHT" then
        if getHex then
            result = "|CFFC41F3B";
        else
            result = { 0.77 , 0.12 , 0.23 };
        end
    elseif className == "DEMONHUNTER" then
        if getHex then
            result = "|CFFA330C9";
        else
            result = { 0.64 , 0.19 , 0.79 };
        end
    elseif className == "DRUID" then
        if getHex then
            result = "|CFFFF7D0A";
        else
            result = { 1.00 , 0.49 , 0.04 };
        end
    elseif className == "HUNTER" then
        if getHex then
            result = "|CFFABD473";
        else
            result = { 0.67 , 0.83 , 0.45 };
        end
    elseif className == "MAGE" then
        if getHex then
            result = "|CFF69CCF0";
        else
            result = { 0.41 , 0.80 , 0.94 };
        end
    elseif className == "MONK" then
        if getHex then
            result = "|CFF00FF96";
        else
            result = { 0.00 , 1.00 , 0.59 };
        end
    elseif className == "PALADIN" then
        if getHex then
            result = "|CFFF58CBA";
        else
            result = { 0.96 , 0.55 , 0.73 };
        end
    elseif className == "PRIEST" then
        if getHex then
            result = "|CFFFFFFFF";
        else
            result = { 1.00 , 1.00 , 1.00 };
        end
    elseif className == "ROGUE" then
        if getHex then
            result = "|CFFFFF569";
        else
            result = { 1.00 , 0.96 , 0.41 };
        end
    elseif className == "SHAMAN" then
        if getHex then
            result = "|CFF0070DE";
        else
            result = { 0.00 , 0.44 , 0.87 };
        end
    elseif className == "WARLOCK" then
        if getHex then
            result = "|CFF9482C9";
        else
            result = { 0.58 , 0.51 , 0.79 };
        end
    elseif className == "WARRIOR" then
        if getHex then
            result = "|CFFC79C6E";
        else
            result = { 0.78 , 0.61 , 0.43 };
        end
    end
    return result;
end

-- Method:          GRM.GetClassByRGB ( table )
-- What it Does:    Returns the string name of a class based on their RGB colors.
-- Purpose:         Useful when mousing over a string to determine its class.
GRM.GetClassByRGB = function ( r , g , b )
    local result = "";
    if r == 0.77 and g == 0.12 and b == 0.23 then
        result = "DEATHKNIGHT";
    elseif r == 0.64 and g == 0.19 and b == 0.79 then
        result = "DEMONHUNTER";
    elseif r == 1.00 and g == 0.49 and b == 0.04 then
        result = "DRUID";
    elseif r == 0.67 and g == 0.83 and b == 0.45 then
        result = "HUNTER";
    elseif r == 0.41 and g == 0.80 and b == 0.94 then
        result = "MAGE";
    elseif r == 0.00 and g == 1.00 and b == 0.59 then
        result = "MONK";
    elseif r == 0.96 and g == 0.55 and b == 0.73 then
        result = "PALADIN";
    elseif r == 1.00 and g == 1.00 and b == 1.00 then
        result = "PRIEST";
    elseif r == 1.00 and g == 0.96 and b == 0.41 then
        result = "ROGUE";
    elseif r == 0.00 and g == 0.44 and b == 0.87 then
        result = "SHAMAN";
    elseif r == 0.58 and g == 0.51 and b == 0.79 then
        result = "WARLOCK";
    elseif r == 0.78 and g == 0.61 and b == 0.43 then
        result = "WARRIOR";
    elseif r == 0.50 and g == 0.50 and b == 0.50 then
        result = "OFFLINE";
    end
    return result
end

-- Method:          GRM.GetClassHex ( string )
-- What it Does:    Returns the WOW formatted Lua coloring hexcode string
-- Purpose:         Easier pull of just the hexcode
-- Why?             This is just a quicker lookup for things already in expected formatting, save on resources.
GRM.GetClassHex = function ( className )
    local result = "";
    if className == "DEATHKNIGHT" then
        result = "|CFFC41F3B";
    elseif className == "DEMONHUNTER" then
        result = "|CFFA330C9";
    elseif className == "DRUID" then
        result = "|CFFFF7D0A";
    elseif className == "HUNTER" then
        result = "|CFFABD473";
    elseif className == "MAGE" then
        result = "|CFF69CCF0";
    elseif className == "MONK" then
        result = "|CFF00FF96";
    elseif className == "PALADIN" then
        result = "|CFFF58CBA";
    elseif className == "PRIEST" then
        result = "|CFFFFFFFF";
    elseif className == "ROGUE" then
        result = "|CFFFFF569";
    elseif className == "SHAMAN" then
        result = "|CFF0070DE";
    elseif className == "WARLOCK" then
        result = "|CFF9482C9";
    elseif className == "WARRIOR" then
        result = "|CFFC79C6E";
    end
    return result;
end

-- Method:          GRM.GetStringClassColorByName ( string )
-- What it Does:    Returns the RGB Hex code of the given class of the player named
-- Purpose:         Useful for carrying over class name with tagged colors into a string, without needing to change the hwole string's color
GRM.GetStringClassColorByName = function ( name )
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    local hasAlts = false;
    local result = "";
    local player;

    -- Append the server name to it if necessary
    if string.find ( name , "-" ) == nil then
        name = name .. "-" .. GRM_G.realmName;
    end
    
    if guildData ~= nil then

        player = guildData[name];
        if player ~= nil then
            result = GRM.GetClassHex ( player.class );
            if #player.alts > 0 then
                hasAlts = true;
            end
        else
            -- If note found maybe they have already left
            player = GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];
            if player ~= nil then
                result = GRM.GetClassHex ( player.class );
                if #player.alts > 0 then
                    hasAlts = true;
                end
            end
        end
    end
    return result , hasAlts;
end

-- Method:          GRM.GetClassifiedName ( string , boolean )
-- What it Does:    Returns the player's name as a string, with the proper class coloring
-- Purpose:         Nice, simple UI feature for ease of knowing person's class by name color.
GRM.GetClassifiedName = function ( playerFullName , serverFree )
    local result = GRM.GetStringClassColorByName ( playerFullName );
    if result == "" then
        if serverFree then 
            result = GRM.SlimName ( playerFullName );
        else
            result = playerFullName;
        end
    else
        if serverFree then
            result = result .. GRM.SlimName ( playerFullName ) .. "|r";
        else
            result = result .. playerFullName .. "|r";
        end
    end
    return result
end

-- Method:          GRM.rgbToHex ( array )
-- What it Does:    Returns the hexadecimal code in modified string format for WOW addons to display the string that given rgb color
-- Purpose:         UI feature for easy fontstring coloring management.
GRM.rgbToHex = function ( rgbTable )
    local hexadec = ""
	for i = 1 , #rgbTable do
		local hex = "";

        -- Hexadecimal algorithm
		while ( rgbTable[i] > 0 ) do
			local index = math.fmod ( rgbTable[i] , 16 ) + 1;
			rgbTable[i] = math.floor ( rgbTable[i] / 16);
			hex = string.sub ( "0123456789ABCDEF" , index , index ) .. hex;	
		end

		if #hex == 0 then
			hex = "00";
        elseif #hex == 1 then
			hex = "0" .. hex;
		end

		hexadec = hexadec .. hex
    end
    -- add the |CFF so the warcraft game knows to acknowledge the hex code
    return "|CFF" .. hexadec;
end

-- Method:          ConvertRGBScale ( float , boolean )
-- What it Does:    Converts any RGB values on 1.0 scale to 255 scale, or the other way around
-- Purpose:         255 is standard RGB scaling, which I am personally comfortable with, but Blizz's internal system uses 1.0 scale. This is just QoL
GRM.ConvertRGBScale = function ( value , to255 )
    -- 1.0 scale to 255 scale
    if to255 then
        value = math.floor ( ( value * 255 ) + 0.5 );
    else
        value = value / 255;
    end
    return value
end

-----------------------------------
--- Player Window Edit Logic ------
-----------------------------------

-- Method:          GRM.IsMouseOverAltButton()
-- What it Does:    Returns true if players is over any of the alt listing names
-- Purpose:         So when a player right-clicks the mouseOver screen, it can either let you know you are already over an AltName Button, so no need to do more work
--                  Or, if you aren't, then you can check to see if you are moused over the other fontstrings.
GRM.IsMouseOverAltButton = function()
    local result = false;

    for i = 1 , #GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons do
        if GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons[i][1]:IsMouseOver() then
            result = true;
            break;
        end
    end
    return result;
end

-- Method:          GRM.GetCoreFontStringClicked()
-- What it Does:    Returns a table with the name of the player, the altName, and the guild.
-- Puspose:         To easily pass the info on without having to use a global variable, and set one function to all 12 alt frames.
GRM.GetCoreFontStringClicked = function()
    local altName = nil;
    local focusName = GRM_G.currentName;
    local isMain = false;
    local isAlt1 = false;

    if ( GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:IsVisible() and GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:IsMouseOver ( 2 , -2 , -2 , 2 ) ) or               -- Over Promotion date text
        ( GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:IsVisible() and GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:IsMouseOver ( 2 , -2 , -2 , 2 ) ) or                                    -- Over Join Date Text
            ( GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:IsVisible() and GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:IsMouseOver ( 2 , -2 , -2 , 2 ) ) or        -- Over player online status text
                ( GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:IsVisible() and GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:IsMouseOver ( 2 , -2 , -2 , 2 ) ) or                            -- Over Birthday date text
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailNameText:IsMouseOver ( 2 , -2 , -2 , 2 ) or                                                                                     -- Over player name text
                        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailDateJoinedTitleTxt:IsMouseOver ( 2 , -2 , -2 , 2 ) then                                                                     -- Over Join Title text (for syncing alts)
        
        altName = focusName;        -- altName is the actual current player, so not really alt name...
    end

    -- Check again!
    if altName == nil then
        -- MOUSE WAS NOT OVER, EVEN ON A RIGHT CLICK OF THE FRAME!!!
        focusName = nil;
        altName = nil;
    end

    if ( isAlt1 and altName ~= nil and string.find ( GRM_UI.GRM_CoreAltFrame.GRM_AltName1:GetText() , GRM.L ( "(main)" ) ) ~= nil ) then        -- This is the main! Let's parse main out of the name!
        isMain = true;
    elseif altName == focusName and GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMainText:IsVisible() then
        isMain = true;
    end
    return { focusName , altName , isMain };
end

-- Method:          GRM.ConfigureRightClickPlayerWindow()
-- What it Does:    Configures the right click menus for editing the player details and so on
-- Purpose:         Add further right click functionality and edit controls to the player. Need to know what their mouse is over.
GRM.ConfigureRightClickPlayerWindow = function()
    local mouseOverFrame , left, right;
    if GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:IsVisible() and GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:IsMouseOver ( 2 , -2 , -2 , 2 ) then
        GRM_G.editPromoDate = true;
        GRM_G.editJoinDate = false;
        GRM_G.editFocusPlayer = false;
        GRM_G.editStatusNotify = false;
        GRM_G.editOnlineStatus = false;
        GRM_G.editBirthday = false;
        mouseOverFrame = GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt;
        left = "TOPRIGHT";
        right = "BOTTOM";

    elseif GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:IsVisible() and GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:IsMouseOver ( 2 , -2 , -2 , 2 ) then
        GRM_G.editJoinDate = true;
        GRM_G.editPromoDate = false;
        GRM_G.editFocusPlayer = false;
        GRM_G.editStatusNotify = false;
        GRM_G.editOnlineStatus = false;
        GRM_G.editBirthday = false;
        mouseOverFrame = GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText;
        left = "TOPRIGHT";
        right = "BOTTOM";

    elseif GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailNameText:IsMouseOver ( 2 , -2 , -2 , 2 ) then
        GRM_G.editFocusPlayer = true;
        GRM_G.editJoinDate = false;
        GRM_G.editPromoDate = false;
        GRM_G.editStatusNotify = false;
        GRM_G.editOnlineStatus = false;
        GRM_G.editBirthday = false;
        mouseOverFrame = GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailNameText;
        left = "TOPRIGHT";
        right = "BOTTOM";

    elseif GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:IsMouseOver ( 2 , -2 , -2 , 2 ) and ( GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:GetText() == GRM_G.Status[2] or GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:GetText() == GRM_G.Status[3] or GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:GetText() == GRM_G.Status[1] or GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:GetText() == GRM_G.Status[4] or GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:GetText() == GRM_G.Status[5] ) then
        if GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:GetText() == GRM_G.Status[5] or GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus:GetText() == GRM_G.Status[1] then
            GRM_G.editOnlineStatus = true;
            GRM_G.editStatusNotify = false;
        else
            GRM_G.editStatusNotify = true;
            GRM_G.editOnlineStatus = false;
        end
        GRM_G.editFocusPlayer = false;
        GRM_G.editJoinDate = false;
        GRM_G.editPromoDate = false;
        GRM_G.editBirthday = false;
        mouseOverFrame = GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailPlayerStatus;
        left = "TOPLEFT";
        right = "BOTTOM";

    elseif GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:IsVisible() and GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:IsMouseOver ( 2 , -2 , -2 , 2 ) then
        GRM_G.editBirthday = true;
        GRM_G.editFocusPlayer = false;
        GRM_G.editJoinDate = false;
        GRM_G.editPromoDate = false;
        GRM_G.editStatusNotify = false;
        GRM_G.editOnlineStatus = false;
        mouseOverFrame = GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText;
        left = "TOPRIGHT";
        right = "BOTTOM";

    end

    return mouseOverFrame , left, right;
end

-- Method:          GRM.SetAltAsMainDropDownMenuLogic ( table )
-- What it Does:    using the right click dropdown menu, it sets the given chosen name as the main
-- Purpose:         Bring in customizability and flexibility and ease of configuration to the player
GRM.SetAltAsMainDropDownMenuLogic = function ( altDetails )
    if altDetails[1] ~= altDetails[2] then
        GRM.SetMain ( altDetails[1] , altDetails[2] , false , 0 );

        -- Now send Comm to sync details.
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled then
            local syncRankFilter = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank;
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportAllRanks then
                syncRankFilter = GuildControlGetNumRanks() - 1;
            end
            GRMsync.SendMessage ( "GRM_SYNC" , GRM_G.PatchDayString .. "?GRM_MAIN?" .. syncRankFilter .. "?" .. altDetails[1] .. "?" .. altDetails[2] , "GUILD");
        end
    else
        -- No need to set as main yet... let's set player to main here.
        local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][altDetails[1]];
        if player ~= nil then
            if #player.alts > 0 then
                GRM.SetMain ( player.alts[1][1] , altDetails[1] , false , 0 );
                if not GRM_UI.GRM_DropDownList1AttachmentFrame.pausedPreviously then
                    GRM_UI.Unpause();
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled then
                    local syncRankFilter = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank;
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportAllRanks then
                        syncRankFilter = GuildControlGetNumRanks() - 1;
                    end
                    GRMsync.SendMessage ( "GRM_SYNC" , GRM_G.PatchDayString .. "?GRM_MAIN?" .. syncRankFilter .. "?" .. player.alts[1][1] .. "?" .. altDetails[1] , "GUILD");
                end
            else
                player.isMain = true;
                player.mainStatusChangeTime = time();
                if not GRM_UI.GRM_DropDownList1AttachmentFrame.pausedPreviously then
                    GRM_UI.Unpause();
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled then
                    local syncRankFilter = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank;
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportAllRanks then
                        syncRankFilter = GuildControlGetNumRanks() - 1;
                    end
                    GRMsync.SendMessage ( "GRM_SYNC" , GRM_G.PatchDayString .. "?GRM_MAIN?" .. syncRankFilter .. "?" .. altDetails[1] .. "?" .. altDetails[2] , "GUILD");
                end
            end
            -- Now send Comm to sync details.
            
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMainText:Show();
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailAltText:Hide();
        end
    end
    if GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMainText:IsVisible() and GRM_G.currentName ~= altDetails[2] then
        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMainText:Hide();
        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailAltText:Show();
    end                
    if GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame:IsVisible() then
        GRM.RefreshAuditFrames ( true , true );
    end
    if GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame:IsVisible() then
        GRM.BuildAltGroupingScrollFrame( GRM_G.currentName );
    end
    GRM.Report ( GRM.L ( "{name} is now set as \"main\"" , GRM.GetClassifiedName ( altDetails[2] , true ) ) );
end

-- Method:          GRM.DemoteMainToAltDropDownMenuLogic ( table )
-- What it Does:    Demotes a player from main and sets them as alt and configures the UI to reflect it
-- Purpose:         Flexibility and quality of life for the player
GRM.DemoteMainToAltDropDownMenuLogic = function ( altDetails )
    if altDetails[1] ~= altDetails[2] then
        GRM.DemoteFromMain ( altDetails[1] , altDetails[2] , false , 0 );

        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled then
            local syncRankFilter = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank;
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportAllRanks then
                syncRankFilter = GuildControlGetNumRanks() - 1;
            end
            GRMsync.SendMessage ( "GRM_SYNC" , GRM_G.PatchDayString .. "?GRM_RMVMAIN?" .. syncRankFilter .. "?" .. altDetails[1] .. "?" .. altDetails[2] , "GUILD");
        end
    else
        -- No need to set as main yet... let's set player to main here.
        local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][ altDetails[1] ];
        if player ~= nil then
            if #player.alts > 0 then
                GRM.DemoteFromMain ( player.alts[1][1] , altDetails[1] , false , 0 );
                if not GRM_UI.GRM_DropDownList1AttachmentFrame.pausedPreviously then
                    GRM_UI.Unpause();
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled then
                    local syncRankFilter = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank;
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportAllRanks then
                        syncRankFilter = GuildControlGetNumRanks() - 1;
                    end
                    GRMsync.SendMessage ( "GRM_SYNC" , GRM_G.PatchDayString .. "?GRM_RMVMAIN?" .. syncRankFilter .. "?" .. player.alts[1][1] .. "?" .. altDetails[1] , "GUILD");
                end
            else
                player.isMain = false;
                player.mainStatusChangeTime = time();
                if not GRM_UI.GRM_DropDownList1AttachmentFrame.pausedPreviously then
                    GRM_UI.Unpause();
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled then
                    local syncRankFilter = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank;
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportAllRanks then
                        syncRankFilter = GuildControlGetNumRanks() - 1;
                    end
                    GRMsync.SendMessage ( "GRM_SYNC" , GRM_G.PatchDayString .. "?GRM_RMVMAIN?" .. syncRankFilter .. "?" .. altDetails[1] .. "?" .. altDetails[2] , "GUILD");        -- both alt details will be same name...
                end
            end
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMainText:Hide();
        end
    end
    if GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame:IsVisible() then
        GRM.RefreshAuditFrames ( true , true );
    end
    if GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame:IsVisible() then
        GRM.BuildAltGroupingScrollFrame( GRM_G.currentName );
    end
    GRM.Report ( GRM.L ( "{name} is no longer set as \"main\"" , GRM.GetClassifiedName ( altDetails[2] , true ) ) );
end


-----------------------------------
--------- ALT MANAGEMENT ----------
-----------------------------------

-- Method:          GRM.PlayerHasAltsOrIsMain ( string )
-- What it Does:    Returns true if the player has at least 1 alt
-- Purpose:         Useful to save resources to know if a person has alts. No need to do unnecessary alt maintenance or checks otherwise.
GRM.PlayerHasAltsOrIsMain = function ( playerName )
    local result = false
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][playerName];
    if player ~= nil then
        if player.isMain or #player.alts > 0 then
            result = true;
        end
    end
    return result;
end

-- Method:          GRM.PlayerIsAnAlt ( table )
-- What it Does:    Returns true if the given player is considered an "alt" not a main, or no designation
-- Purpose:         To inform if the player is an alt more easily.
GRM.PlayerIsAnAlt = function ( player )
    local result = false

    if #player.alts > 0 then
        for i = 1 , #player.alts do
            if player.alts[i][5] then
                result = true;  -- main found. Since main is found, player can be determined to be an alt in the grouping.
                break;
            end
        end
    end

    return result;
end

-- Method:          GRM.RemoveAlt(string , string , boolean , int , boolean )
-- What it Does:    Detags the given altName to that set of toons.
-- Purpose:         Alt management, so whoever has addon installed can tag player.
GRM.RemoveAlt = function ( playerName , altName , isSync , syncTimeStamp , errorProtection , recursiveRemove )

    -- To protect the data if someone is sending you corrupted, broken, or nefarious alt info...
    if not GRM_G.AltRmvInProgress or recursiveRemove then
        GRM_G.AltRmvInProgress = true;
        local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
        local player = guildData[playerName];

        if errorProtection then
            if player ~= nil then

                for i = 1 , #player.alts do
                    if player.alts[i][1] == altName then
                        table.remove ( player.alts , i );
                        break;
                    end
                end
                
            end
            return
        end

        local isRemoveMain = false;
        local epochTime;
        if isSync then
            epochTime = syncTimeStamp;
        else
            epochTime = time();
        end

        if playerName ~= altName then

            local playerAlt = guildData[altName];    -- Set playerData to the altName
            player = guildData[playerName];

            if playerAlt ~= nil then
                if #playerAlt.alts > 0 then
                    GRM_G.selectedAltList = playerAlt.alts;
                end
                if #playerAlt.alts > 1 and playerAlt.isMain then -- No need to report if the person is removing the last alt. No need to set oneself as main.
                    isRemoveMain = true;
                end
            end

            if player == nil then
                -- Erroenous data, abort...
                GRM.RemoveAlt ( altName , playerName , isSync , syncTimeStamp , errorProtection , true );
                return
            end
            -- For protections, in case the player is trying to send you bad data... 
            if playerAlt == nil then
                return
            end
            
            -- Removing the alt from all of the player's alts.'
            local listOfAlts = player.alts;
            if #listOfAlts > 0 then                                                                                                     -- There is more than 1 alt for new alt to be added to
                for i = 1 , #listOfAlts do  
                    if listOfAlts[i][1] ~= altName then                                                                                 -- Cycle through previously known alt names to add new on each, one by one.

                        local tempAlt = guildData[ listOfAlts[i][1] ];
                        if tempAlt ~= nil then
                            -- Now, we have the list!
                            for m = 1 , #tempAlt.alts do
                                if tempAlt.alts[m][1] == altName then
                                    -- verify it is not Found already first...
                                    local isFound = false;
                                    for r = 1 , #tempAlt.removedAlts do
                                        if tempAlt.removedAlts[r][1] == tempAlt.alts[m][1] then
                                            isFound = true;
                                            break;
                                        end
                                    end
                                    if not isFound then
                                        table.insert ( tempAlt.removedAlts , tempAlt.alts[m] ) -- Adding the alt to removed alts list
                                        tempAlt.removedAlts[ #tempAlt.removedAlts ][6] = epochTime;
                                    end
                                    table.remove ( tempAlt.alts , m );     -- removing the alt
                                    break;
                                end
                            end
                        end
                    end
                end
            end
            -- Remove the alt name from the current focus
            for i = #player.alts , 1 , -1 do
                if player.alts[i][1] == altName then
            
                    -- verify it is not Found already first...
                    local isFound = false;
                    for r = 1 , #player.removedAlts do
                        if player.removedAlts[r][1] == player.alts[i][1] then
                            isFound = true;
                            break;
                        end
                    end
                    if not isFound then
                        table.insert ( player.removedAlts , player.alts[i] ) -- Adding the alt to removed alts list
                        player.removedAlts[ #player.removedAlts ][6] = epochTime;
                    end
                    table.remove ( player.alts , i );
                    break;
                end
            end
            -- Resetting the alt's list
            if isRemoveMain then 
                playerAlt.isMain = false;
            end
            playerAlt.alts = nil;
            playerAlt.alts = {};
            -- Insta update the frames!
            if GRM_UI.GRM_MemberDetailMetaData ~= nil and GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
                local altFound = false;
                if #GRM_G.selectedAltList > 0 then
                    for m = 1 , #GRM_G.selectedAltList do
                        if GRM_G.selectedAltList[m][1] == GRM_G.currentName then
                            -- Alt is found! Let's update the alt frames!
                            altFound = true;
                        
                            if guildData[GRM_G.selectedAltList[m][1]] ~= nil then
                                -- woot! Now have the index of the alt and can successfully populate the alt frames.
                                GRM.PopulateAltFrames ( GRM_G.selectedAltList[m][1] );
                            end
                            break;
                        end
                    end
                end
                -- If it is just the player's same frame, then update it!
                if not altFound and playerName == GRM_G.currentName then
                    GRM.PopulateAltFrames ( playerName );
                end
            end       
        end
    end
    GRM_G.AltRmvInProgress = false;
end

-- Method:          GRM.RemovePlayerFromRemovedAltTable( string )
-- What it Does:    When a player removes an alt, it stores that removal in a special table for syncing purposes.
--                  If the alt is re-added, it removes the player from the removed list
-- Purpose:         Syncing data needs timestamps and thus needs good table management of the metadata of add/remove alts lists.
GRM.RemovePlayerFromRemovedAltTable = function ( name , altName )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][altName];

    if #player.removedAlts > 0 then
        for i = 1 , #player.removedAlts do
            if name == player.removedAlts[i][1] then
                table.remove ( player.removedAlts , i );
                break;
            end
        end
    end
end

-- Method:          GRM.isAltAlreadyAdded ( string , table )
-- What it Does:    Checks if player has already been added, and if so, doesn't do anything.
-- Purpose:         Save a lot of resources and logic processing if it is determined this player has already been added.
GRM.isAltAlreadyAdded = function ( altName , player )
    local result = false;

    for i = 1 , #player.alts do
        if player.alts[i][1] == altName then
            result = true;
            break;
        end
    end
    
    return result;
end

-- Method:          GRM.AddAlt (string,string,boolean,int)
-- What it Does:    Tags toon to a player's set of alts. It will tag them not just to the given player, but reverse tag itself to all of the alts.
-- Purpose:         Organizing a player and their alts.
GRM.AddAlt = function ( playerName , altName , isSync , syncTimeStamp , recursiveAltAdd )

    if not GRM_G.AltAddInProgress or recursiveAltAdd then

        GRM_G.AltAddInProgress = true;
        local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];

        if playerName ~= altName then

            -- First, let's identify player index, then identify the classColor of the alt
            local classAlt = "";
            local classMain = "";
            local classColorsAlt , classColorsMain , classColorsTemp;
            local isMain = false;
            local timeEpochAdd;
            if isSync then
                timeEpochAdd = syncTimeStamp;
            else
                timeEpochAdd = time();
            end

            local player = guildData[playerName];
            local alt = guildData[altName];

            if alt ~= nil then
                if #alt.alts > 0 then
                    GRM_G.selectedAltList = alt.alts;
                end
                classAlt = alt.class;
            else
                return;
            end

            -- Determine if player/alt are valid
            if player ~= nil then
                classMain = player.class;
            else
                GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "Failed to add alt for unknown reason. Try closing Roster window and retrying!" ) );
                return
            end

            -- No need to re-add if already added.
            if GRM.isAltAlreadyAdded ( altName , player ) then
                return
            end
            
            -- NEED TO VERIFY IT IS NOT AN ALT FIRST!!! it is removing and re-adding if it is same person.
            local isFound = false;
            if #alt.alts > 0 then
                local listOfAlts = alt.alts;
                
                for m = 1 , #listOfAlts do                                              -- Let's quickly verify that this is not a repeat alt add.
                    if listOfAlts[m][1] == playerName and not isSync then
                        GRM.Report ( GRM.L ( "{name} is Already Listed as an Alt." , GRM.SlimName ( altName ) ) );
                        isFound = true;
                        break;
                    end
                end
            end
            -- If player is trying to add this toon to a list that is already on a list then it adds it in reverse
            if #alt.alts > 0 and #player.alts > 0 and not isFound then  -- Oh my! Both players have current lists!!! Remove the alt from his list, add to this new one.
                GRM.RemoveAlt ( alt.alts[1][1] , alt.name , isSync , syncTimeStamp , false );
            end

            -- Main Status check
            isMain = false;
            if #player.alts > 0 then
                
                for s = 1 , #player.alts do
                    if s == 1 then
                        if player.isMain then
                            isMain = true;
                        end
                    end
                    if not isMain then
                        if guildData[player.alts[s][1]] ~= nil then
                            -- Ok, let's see if the alt is main...
                            if guildData[player.alts[s][1]].isMain then
                                isMain = true;
                            end
                        end
                    end
                    if isMain then
                        alt.isMain = false;
                        break;
                    end
                end
            end

            -- if the alt has a list... then reverse
            if #alt.alts > 0 then

                if not isFound then
                    -- if the player is main, but the alt has a grouping, let's check if any alts on the list are main. If they are, demote oneself to alt as the group takes priority...
                    if player.isMain then                       -- Ok, the playerName is a main trying to add the alt who is part of a list - if any in the list are also main, demote oneseful then continue
                        isMain = false;
                        for s = 1 , #alt.alts do
                            if alt.isMain then            -- The alt is a main, so both myself and the alt is a main, but the alt is a list - demote oneseful
                                isMain = true;
                            end
                            if not isMain then

                                if guildData[alt.alts[s][1]] ~= nil then
                                    -- Ok, let's see if the alt is main...
                                    if guildData[alt.alts[s][1]].isMain then                -- A toon in the alt grouping is a main, so demote myself and move on.
                                        isMain = true;
                                    end
                                end
                            end
                            if isMain then
                                GRM.DemoteFromMain ( playerName , playerName , false , 0 ) -- Can't just demote myself, I need to remove DemoteFromMain among all my alts as well, just in case..
                                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMainText:Hide();
                                break;
                            end
                        end
                    end
                    -- Just in case, let's remove MAIN status if needed.
                    GRM.AddAlt ( altName , playerName , isSync , syncTimeStamp , true );
                    GRM.SyncBirthdayWithNewAlt ( playerName );
                    return;
                end
            else

                -- add altName to each of the previously
                local isFound2 = false;
                classColorsAlt = GRM.GetClassColorRGB ( classAlt );
                local listOfAlts = player.alts;
                local tempAlt;
                if #listOfAlts > 0 then                                                                 -- There is more than 1 alt for new alt to be added to
                    for i = 1 , #listOfAlts do                                                          -- Cycle through previously known alt names to add new on each, one by one.
                        tempAlt = guildData[listOfAlts[i][1]];
                        if tempAlt ~= nil then

                            -- Now, make sure it is not a repeat add!
                            for m = 1 , #listOfAlts do                                              -- Let's quickly verify that this is not a repeat alt add.
                                if listOfAlts[m][1] == altName then
                                    if not isSync then
                                        GRM.Report ( GRM.L ( "{name} is Already Listed as an Alt." , GRM.SlimName ( altName ) ) );
                                    end
                                    isFound2 = true;
                                    break;
                                end
                            end
                            if not isFound2 then

                                classColorsTemp = GRM.GetClassColorRGB ( tempAlt.class );
                                table.insert ( tempAlt.alts , { altName , classColorsAlt[1] , classColorsAlt[2] , classColorsAlt[3] , alt.isMain , timeEpochAdd } ); -- altName is added to a currentFocus previously added alt.
                                GRM.RemovePlayerFromRemovedAltTable ( altName , tempAlt.name );

                                -- Double check to not double add
                                local isFound3 = false;
                                for r = 1 , #alt.alts do
                                    if alt.alts[r][1] == tempAlt.name then
                                        isFound3 = true;
                                        break;
                                    end
                                end
                                if not isFound3 then
                                    table.insert ( alt.alts , { tempAlt.name , classColorsTemp[1] , classColorsTemp[2] , classColorsTemp[3] , tempAlt.isMain , timeEpochAdd } );
                                    GRM.RemovePlayerFromRemovedAltTable ( tempAlt.name , altName );
                                end
                            end
                        end
                        if isFound2 then
                            break;
                        end
                    end
                else
                    if player.isMain and alt.isMain then
                        alt.isMain = false;
                    end
                end

                if not isFound2 then
                    -- Add all of the CurrentFocus player's alts to the new alt
                    -- then add the currentFocus player as well
                    classColorsMain = GRM.GetClassColorRGB ( classMain );
                    if player.isMain then
                        table.insert ( alt.alts , 1 , { playerName , classColorsMain[1] , classColorsMain[2] , classColorsMain[3] , player.isMain , timeEpochAdd } );
                    else
                        table.insert ( alt.alts , { playerName , classColorsMain[1] , classColorsMain[2] , classColorsMain[3] , player.isMain , timeEpochAdd } );
                    end
                    GRM.RemovePlayerFromRemovedAltTable ( playerName , altName );
                    -- Finally, let's add the alt to the player's currentFocus.

                    -- Double check to not double add
                    local isFound3 = false;
                    for r = 1 , #player.alts do
                        if player.alts[r][1] == altName then
                            isFound3 = true;
                            break;
                        end
                    end
                    if not isFound3 then
                        table.insert ( player.alts , { altName , classColorsAlt[1] , classColorsAlt[2] , classColorsAlt[3] , alt.isMain , timeEpochAdd } );
                        GRM.RemovePlayerFromRemovedAltTable ( altName , playerName );
                    end
                end
                -- Insta update the frames!
                if GRM_UI.GRM_MemberDetailMetaData ~= nil and GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
                    -- For use with syncing UI LIVE
                    local altFound = false;
                    if #GRM_G.selectedAltList > 0 then
                        for m = 1 , #GRM_G.selectedAltList do
                            if GRM_G.selectedAltList[m][1] == GRM_G.currentName then
                                -- Alt is found! Let's update the alt frames!
                                altFound = true;

                                if guildData[GRM_G.selectedAltList[m][1]] ~= nil then
                                    -- woot! Now have the index of the alt and can successfully populate the alt frames.
                                    GRM.PopulateAltFrames ( GRM_G.selectedAltList[m][1] );
                                end
                                if #player.alts > 0 and alt.isMain then
                                    GRM.SetMain ( GRM_G.currentName , alt.name , false , 0 );
                                end
                                break;
                            end
                        end
                    end

                    if not altFound then
                        local frameName = GRM_G.currentName;
                        if playerName == frameName then
                            GRM.PopulateAltFrames ( playerName );
                        elseif altName == frameName then
                            GRM.PopulateAltFrames ( altName );
                        end
                        if #player.alts > 0 and alt.isMain then
                            GRM.SetMain ( GRM_G.currentName , alt.name , false , 0 );
                        end
                    end
                end
            end
        else
            GRM.Report ( GRM.L ( "{name} cannot become their own alt!" , GRM.SlimName ( playerName ) ) );
        end
    end

    GRM_G.AltAddInProgress = false;
end

-- Method:          GRM.AddPlayerToOwnAltList()
-- What it Does:    For the first time a player logs on that toon, or joins a guild with that toon, it adds them to their own alt list.
-- Purpose:         For easy alt management. AUTO adds alt info for a guild :D
GRM.AddPlayerToOwnAltList = function()
    -- Ok, now let's add the player to an alt list...
    -- First, find the player in member save and determine if they are the main, if not, check his alt list, determine who is main.
    -- if no main, first person on list can add.
    -- if main, then main will add this player.
    local playerIsFound = false;
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.addonUser];

    if player ~= nil then
        playerIsFound = true;
        -- Ok, adding the player!
        GRM_PlayerListOfAlts_Save[GRM_G.F][GRM_G.guildName][GRM_G.addonUser] = {};


        -- if the player already is on a list, let's not add them automatically.
        if #player.alts == 0 then
            -- Ok, good, let's check the alt list!

            if GRM.GetNumKeyedEntries( GRM_PlayerListOfAlts_Save[GRM_G.F][GRM_G.guildName] ) > 1 then    -- No need if it is just myself - if number of entries == 1 then it is just me
                -- Ok great, there is already another player in the guild! Let's put them into the same alt grouping!

                local isAdded = false;
                for altName in pairs ( GRM_PlayerListOfAlts_Save[GRM_G.F][GRM_G.guildName] ) do
                    -- Make sure it is not the player.
                    if altName ~= GRM_G.addonUser then

                        if GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][altName] ~= nil then
                            if GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][altName].isMain then
                                -- ADD ALT HERE!!!!!!
                                GRM.AddAlt ( altName , GRM_G.addonUser , false , 0 );
                                GRM.SyncBirthdayWithNewAlt ( GRM_G.addonUser );
                                isAdded = true;
                                break;
                            end
                        end
                    end
                end

                -- if it was not added, then add it here! No alt was set as main.
                if not isAdded then
                    for altName in pairs ( GRM_PlayerListOfAlts_Save[GRM_G.F][GRM_G.guildName] ) do
                        -- Make sure it is not the player.
                        if altName ~= GRM_G.addonUser then
                            GRM.AddAlt ( GRM_G.addonUser , altName , false , 0 );
                            GRM.SyncBirthdayWithNewAlt ( altName );
                            break;
                        end
                    end
                end
            end
        end
    end
    -- Player was just invited, and his metadata details have not been populated as of yet. Let's retry in a moment.
    if not playerIsFound then
        C_Timer.After ( 5 , function()
            GRM.AddPlayerToOwnAltList ();
        end);
    end
end

-- Method:              GRM.SortMainToTop ( string )
-- What it Does:        Sorts the alts list and sets the main to the top.
-- Purpose:             To keep the main as the first name in the list of alts.
GRM.SortMainToTop = function ( name )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];

    -- Ok, now, let's grab the list and do some sorting!
    if player.isMain ~= true then                               -- no need to attempt sorting if they are all alts, none are the main.
        for i = 1 , #player.alts do                           -- scanning through the list of alts
            if player.alts[i][5] then                         -- if one of them equals the main!
                local tempList = player.alts[i];              -- Saving main's info to temp holder
                table.remove ( player.alts , i );             -- removing
                table.insert ( player.alts , 1 , tempList );  -- Re-adding it to the front and done!
                break
            end
        end
    end
end

-- Method:              GRM.SetMain ( string , string , boolean , int )
-- What it Does:        Sets the player as main, as well as updates that status among the alt grouping.
-- Purpose:             Main/alt management control.
GRM.SetMain = function ( playerName , mainName , isSync , syncTimeStamp )

    if playerName == nil then
        return;
    end

    local timeEpochMain;
    if isSync then
        timeEpochMain = syncTimeStamp;
    else
        timeEpochMain = time();
    end

    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    local player = guildData[playerName];
    local main = guildData[mainName];

    if player ~= nil then
        -- Establishing list of alts...
        if #player.alts > 0 then
            GRM_G.selectedAltList = player.alts;
        end
    else
        return;
    end

    if main == nil then
        return;
    end

    local listOfAlts = player.alts;
    local tempAlt
    if #listOfAlts > 0 then
        -- Need to tag each alt's list with who is the main.
        for i = 1 , #listOfAlts do
            tempAlt = guildData[listOfAlts[i][1]];
            if tempAlt ~= nil then
                -- Now need to find the name of the alt to tag it.
                if tempAlt.name == mainName then                            -- this alt is the main!
                    if not tempAlt.isMain then
                        tempAlt.isMain = true;                              -- Setting toon as main!
                        tempAlt.mainStatusChangeTime = timeEpochMain;       -- Setting timeStampOfChange!
                    end
                    for m = 1 , #tempAlt.alts do                             -- making sure all their alts are listed as notMain
                        tempAlt.alts[m][5] = false;
                    end
                else
                    if tempAlt.isMain then
                        tempAlt.isMain = false;                             -- ensure alt is not listed as main
                        tempAlt.mainStatusChangeTime = timeEpochMain;
                    end
                    for m = 1 , #tempAlt.alts do                             -- identifying who is to be tagged as main
                        if tempAlt.alts[m][1] == mainName then
                            tempAlt.alts[m][5] = true;
                        else
                            tempAlt.alts[m][5] = false;                      -- tagging everyone not the main as false
                        end
                    end
                end

                -- Now, let's sort
                GRM.SortMainToTop ( listOfAlts[i][1] );
            end            
        end
        -- Do one last pass to set your own alts list proper.
        for i = 1 , #player.alts do
            player.alts[i][5] = false;
        end
    end

    -- Let's ensure the main is the main!
    if playerName ~= mainName then
        if player.isMain then
            player.isMain = false;
            player.mainStatusChangeTime = timeEpochMain;
        end
        if not main.isMain then
            main.isMain = true;
            main.mainStatusChangeTime = timeEpochMain;
        end
        for m = 1 , #player.alts do               -- identifying who is to be tagged as main
            if player.alts[m][1] == mainName then
                player.alts[m][5] = true;
            else
                player.alts[m][5] = false;        -- tagging everyone not the main as false
            end
        end
        GRM.SortMainToTop ( playerName );
    else
        if not player.isMain then
            player.isMain = true;
            player.mainStatusChangeTime = timeEpochMain;
        end
    end
    -- Insta update the frames!
    if GRM_UI.GRM_MemberDetailMetaData ~= nil and GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
        local altFound = false;
        if #GRM_G.selectedAltList > 0 then
            for m = 1 , #GRM_G.selectedAltList do
                if GRM_G.selectedAltList[m][1] == GRM_G.currentName then
                    -- Alt is found! Let's update the alt frames!
                    altFound = true;

                    if guildData[GRM_G.selectedAltList[m][1]] ~= nil then
                        -- woot! Now have the index of the alt and can successfully populate the alt frames.
                        GRM.PopulateAltFrames ( GRM_G.selectedAltList[m][1] );
                    end
                    break;
                end
            end
        end
        
        if not altFound then
            local frameName = GRM_G.currentName;
            if playerName == frameName then
                GRM.PopulateAltFrames ( playerName );
            elseif mainName == frameName then
                GRM.PopulateAltFrames ( mainName );
            end
        end
    end
end

-- Method:              GRM.DemoteFromMain ( string , string , boolean , int )
-- What it Does:        If the player is "main" then it removes the main tag to false
-- Purpose:             User Experience (UX) and alt management!
GRM.DemoteFromMain = function ( playerName , mainName , isSync , syncTimeStamp )
    local RMVtimeEpochMain;
    if isSync then
        RMVtimeEpochMain = syncTimeStamp;
    else
        RMVtimeEpochMain = time();
    end

    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    local player = guildData[playerName];
    local main = guildData[mainName];
    
    if player ~= nil then
        -- Establishing list of alts...
        if #player.alts > 0 then
            GRM_G.selectedAltList = player.alts;
        end
    end

    local listOfAlts = player.alts;
    local tempAlt;

    if #listOfAlts > 0 then
        -- Need to tag each alt's list with who is the main.
        for i = 1 , #listOfAlts do
            tempAlt = guildData[listOfAlts[i][1]];
            if tempAlt ~= nil then
                -- Now need to find the name of the alt to tag it.
                if tempAlt.name == mainName then                -- this alt is the main!
                    if tempAlt.isMain then
                        tempAlt.isMain = false;                       -- Demoting the toon from main!
                        tempAlt.mainStatusChangeTime = RMVtimeEpochMain;
                    end
                    for m = 1 , #tempAlt.alts do               -- making sure all their alts are listed as notMain
                        tempAlt.alts[m][5] = false;
                    end
                else
                    for m = 1 , #tempAlt.alts do               -- identifying who is to be tagged as main
                        if tempAlt.alts[m][1] == mainName then
                            tempAlt.alts[m][5] = false;
                        else
                            tempAlt.alts[m][5] = false;        -- tagging everyone not the main as false
                        end
                    end
                end

                -- Now, let's sort
                GRM.SortMainToTop ( listOfAlts[i][1] );
            end            
        end
    end

    -- Let's ensure the main is the main!
    if playerName ~= mainName then
        if player.isMain then
            player.isMain = false;
            player.mainStatusChangeTime = RMVtimeEpochMain;
        end
        if main.isMain then
            main.isMain = false;
            main.mainStatusChangeTime = RMVtimeEpochMain;
        end
        for m = 1 , #player.alts do               -- identifying who is to be tagged as main
            if player.alts[m][1] == mainName then
                player.alts[m][5] = false;
            else
                player.alts[m][5] = false;        -- tagging everyone not the main as false
            end
        end
        GRM.SortMainToTop ( playerName );
    else
        if player.isMain then
            player.isMain = false;
            player.mainStatusChangeTime = RMVtimeEpochMain;
        end
    end
    -- Insta update the LIVE frames for sync, if player is on a diff. frame.
    if GRM_UI.GRM_MemberDetailMetaData ~= nil and GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
        local altFound = false;
        if #GRM_G.selectedAltList > 0 then
            for m = 1 , #GRM_G.selectedAltList do
                if GRM_G.selectedAltList[m][1] == GRM_G.currentName then
                    -- Alt is found! Let's update the alt frames!
                    altFound = true;

                    if guildData[GRM_G.selectedAltList[m][1]] ~= nil then
                        -- woot! Now have the index of the alt and can successfully populate the alt frames.
                        GRM.PopulateAltFrames ( GRM_G.selectedAltList[m][1] );
                    end
                    break;
                end
            end
        end

        if not altFound then
            local frameName = GRM_G.currentName;
            if playerName == frameName then
                GRM.PopulateAltFrames ( playerName );
            elseif mainName == frameName then
                GRM.PopulateAltFrames ( mainName );
            end
        end
    end
end

-- Method:          GRM.GetAltTag ( int )
-- What it Does:    Returns the hex value/colored string with the alt or main tag
-- Purpose:         For taggin the autocomplete names to make it easier to see who is and isn't and alt/main
GRM.GetAltTag = function ( value )
    local result = "";
    local mainDisplay = GRM.GetMainTags ( false , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].mainTagIndex );
    local altDisplay = GRM.GetAltTags ( false , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].mainTagIndex );
    if value == 1 then
        result = "|cffab0000 " .. mainDisplay;
    elseif value == 2 then
        result = "|cffab0000 " .. altDisplay;
    end
    return result;
end

-- Method:          GRM.AddAltAutoComplete()
-- What it Does:    Takes the entire list of guildies, then sorts them as player types to be added to alts list
-- Purpose:         Eliminates the possibility of a person entering a fake name of a player no longer in the guild.
GRM.AddAltAutoComplete = function()
    local partName = GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltEditBox:GetText();
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    GRM_G.listOfGuildies = nil;
    GRM_G.listOfGuildies = {};

    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            if player.name ~= GRM_G.currentName then   -- no need to go through player's own window
                -- Determine alt/main tag
                local tag = 0;
                -- 0 = no tag, 1 = main, 2 = alt
                if player.isMain then
                    tag = 1;
                else
                    for j = 1 , #player.alts do
                        if player.alts[j][5] then
                            tag = 2;
                            break;
                        end
                    end
                end
                table.insert ( GRM_G.listOfGuildies , { player.name , player.class , tag } );
            end
        end
    end
    -- Need to sort "Complex" table
    sort ( GRM_G.listOfGuildies , function ( a , b ) return a[1] < b[1] end );    -- Alphabetizing it for easier parsing for buttontext updating. - This sorts the first index of the 2D array
    
    -- Now, let's identify the names that match
    local matchingList = {};
    local found = false;
    local innerFound = false;
    for i = 1 , #GRM_G.listOfGuildies do
        innerFound = false;
        if string.lower ( partName ) == string.lower ( string.sub ( GRM_G.listOfGuildies[i][1] , 1 , #partName ) ) then
            innerFound = true;
            found = true;
            table.insert ( matchingList , GRM_G.listOfGuildies[i] );
        end
        if #matchingList == GRM_G.MaxAltAutoCompleteList then
            break;
        end
        if innerFound ~= true and found then    -- resource saving
            break;
        end
    end

    -- If No alphabetical matches, try partial
    if #matchingList == 0 then
        for i = 1 , #GRM_G.listOfGuildies do
            if string.find ( string.lower ( GRM_G.listOfGuildies[i][1] ) , string.lower ( partName ) ) ~= nil then
                table.insert ( matchingList , GRM_G.listOfGuildies[i] );
            end
            if #matchingList == GRM_G.MaxAltAutoCompleteList then
                break;
            end
        end
    end
    
    -- Populate the buttons now...
    if partName ~= nil and partName ~= "" then
        local resultCount = #matchingList;
        if resultCount > 0 then
            GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrameHelpText:Hide();
            GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrameHelpText2:Hide();
            GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrameTextBottom:Show();
            GRM.BuildAutoCompleteAltSelectionScrollFrame ( matchingList );
            GRM_G.currentHighlightIndex = 1;
            GRM.ResetAutoCompleteHighlights();
            GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons[1][1]:LockHighlight();
        else
            GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrameHelpText:Show();
            GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrameHelpText2:Show();
            GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrameTextBottom:Hide();
            -- Hide the buttons
            if GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons then
                for i = 1 , #GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons do
                    GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons[i][1]:Hide();
                end
            end
            if string.lower ( GRM_G.currentName ) == string.lower ( partName ) then
                GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrameHelpText:SetText ( GRM.L ( "Player Cannot Add Themselves as an Alt" ) );
                GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrameHelpText2:Hide();
            else
                GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrameHelpText:SetText ( GRM.L ( "Player Not Found" ) );
            end
        end          
    else
        if GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons then
            for i = 1 , #GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons do
                GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons[i][1]:Hide();
            end
        end
        GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrameTextBottom:Hide();
        GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrameHelpText:SetText ( GRM.L ( "Please Type the Name of the alt" ) );
        GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrameHelpText:Show();
        GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrameHelpText2:Show();
    end
end

-- Method:              GRM.KickAllAlts ( string )
-- What it Does:        Bans all listed alts of the player as well and adds them to the ban list. Of note, addons cannot kick players anymore, so this only adds to ban list.
-- Purpose:             QoL. Option to ban players' alts as well if they are getting banned.
GRM.KickAllAlts = function ( playerName )
    GRM_G.KickAllAltsTable = {};
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][playerName];

    if player ~= nil then
    -- Ok, let's parse the player's data!
        local listOfAlts = player.alts;
        local tempAlt;
        local instructionNote = GRM.L ( "Reason Banned?" ) .. "\n" .. GRM.L ( "Click \"YES\" When Done" );
        local result = "";

        if #listOfAlts > 0 and ( GRM_G.isChecked2 or GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanAllAltsCheckbox:GetChecked() ) then
            local index;
            for s = 1 , #listOfAlts do
                tempAlt = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][listOfAlts[s][1]];

                if tempAlt ~= nil and tempAlt.name ~= GRM_G.addonUser then        -- Logic to avoid kicking oneself ( or at least to avoid getting error notification )
                    -- The banning...
                    if GRM_G.isChecked or GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanAllAltsCheckbox:GetChecked() then
                        tempAlt.bannedInfo[1] = true;
                        tempAlt.bannedInfo[2] = time();
                        tempAlt.bannedInfo[3] = false;
                        tempAlt.bannedInfo[4] = GRM_G.addonUser;

                        if GRM_UI.GRM_MemberDetailPopupEditBox:IsVisible() then
                            result = GRM_UI.GRM_MemberDetailPopupEditBox:GetText();
                        elseif GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanReasonEditBox:IsVisible() then
                            result = GRM.Trim ( GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanReasonEditBox:GetText() );
                        end 

                        if result ~= nil and result ~= instructionNote and result ~= GRM.L ( "Reason Banned?" ) and result ~= "" then
                            tempAlt.reasonBanned = result;
                        elseif result == nil or result == GRM.L ( "Reason Banned?" ) then
                            tempAlt.reasonBanned = "";
                        else
                            tempAlt.reasonBanned = result;
                        end
                    end

                    -- The kicking...
                    if GRM_G.isChecked2 then

                        table.insert ( GRM_G.KickAllAltsTable , {} );
                        index = #GRM_G.KickAllAltsTable;
                        GRM_G.KickAllAltsTable[index].name = tempAlt.name;
                        GRM_G.KickAllAltsTable[index].class = GRM.GetClassColorRGB ( tempAlt.class );
                        GRM_G.KickAllAltsTable[index].lastOnline = tempAlt.lastOnline;
                        GRM_G.KickAllAltsTable[index].action = GRM.L ( "Kick" );
                        GRM_G.KickAllAltsTable[index].macro = "/gremove";
                        GRM_G.KickAllAltsTable[index].isHighlighted = false;
                        
                    end
                end
            end
            if #GRM_G.KickAllAltsTable > 0 then
                -- Bring popup reminder to select it...
                GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "Kick macro created. Press \"CTRL-SHIFT-K\" to kick all of {name}'s alts" , GRM.SlimName ( playerName ) ) );
                GRM_G.KickAltControl = true;
                GRM_UI.GRM_ToolCoreFrame.TabPosition = 1;
                if not GRM_UI.GRM_ToolCoreFrame or ( GRM_UI.GRM_ToolCoreFrame and not GRM_UI.GRM_ToolCoreFrame:IsVisible() ) then
                    GRM_UI.GRM_ToolCoreFrame:Show();
                elseif GRM_UI.GRM_ToolCoreFrame:IsVisible() then
                    GRM_UI.RefreshManagementTool( GRM_G.KickAltControl );
                    GRM_G.KickAltControl = false;
                end
            end
        end
    end

    -- Refresh the frames!
    GRM.RefreshSelectHybridFrames ( true , true , true , false );
end

-- Method:          GRM.KickAllBanned()
-- What it Does:    Triggers the frames to load in the macro tool to ban ALL the players currently still in the guild but are banned
-- Purpose:         Easy cleanup of removal of a player by manual banning, and all their alts.
GRM.KickAllBanned = function()
    GRM_G.KickAllBannedTable = {};

    local names = select ( 2 , GRM.GetNumberOfPlayerSBannedCurrentlyInGuild() );
    local c = 1;

    if names ~= nil then
        for i = 1 , #names do
            if names[i][1] ~= GRM_G.addonUser then
                GRM_G.KickAllBannedTable[c] = {};
                GRM_G.KickAllBannedTable[c].name = names[i][1];
                GRM_G.KickAllBannedTable[c].class = names[i][2];
                GRM_G.KickAllBannedTable[c].lastOnline = names[i][3];
                GRM_G.KickAllBannedTable[c].action = GRM.L ( "Kick" );
                GRM_G.KickAllBannedTable[c].macro = "/gremove";
                GRM_G.KickAllBannedTable[c].isHighlighted = false;
                c = c + 1;
            end
        end
    end

    if #GRM_G.KickAllBannedTable > 0 then
        -- Bring popup reminder to select it...
        GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "Kick macro created. Press Hotkey to Remove Banned Players Still in Guild" ) );
        GRM_G.kickBannedControl = true;
        GRM_UI.GRM_ToolCoreFrame.TabPosition = 1; -- Since we are removing players it will now function by setting it to default kick tab
        if not GRM_UI.GRM_ToolCoreFrame or ( GRM_UI.GRM_ToolCoreFrame and not GRM_UI.GRM_ToolCoreFrame:IsVisible() ) then
            GRM_UI.GRM_ToolCoreFrame:Show();
        elseif GRM_UI.GRM_ToolCoreFrame:IsVisible() then
            GRM_UI.RefreshManagementTool( false , GRM_G.kickBannedControl );
            GRM_G.kickBannedControl = false;
        end
        
    end

    -- Refresh the frames!
    GRM.RefreshSelectHybridFrames ( true , true , true , false );
end

-- Method:          GRM.CreateMacro ( string , string , string , string )
-- What it Does:    Builds a general non-character specific macro based on given arguments
-- Purpose:         To be able to auto-generate a macro, due to limitations of executing things in the code, we just create an in-game one-button to click
GRM.CreateMacro = function ( macroText , name , icon , keyBind , isLogOff )

    if GRM_G.MacroDelay and not GRM_G.inCombat then
        GRM_G.MacroDelay = false;
    end

    if not GRM_G.inCombat and not GRM_G.MacroDelay then
        -- First, let's check if the macro exists
        local index = GetMacroIndexByName ( name );
        local canBuild = false;

        if index == 0 and GetNumMacros() < MAX_ACCOUNT_MACROS then
            -- no macro yet created
            CreateMacro ( name , icon , macroText );
            canBuild = true;
        elseif index ~= 0 then
            -- edit the macro that already exists...
            EditMacro ( index , name , icon , macroText );
            canBuild = true;
        else
            -- Not enough macro slots...
            GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "Unable to create GRM hotkey macro. You currently are at the cap of {num} macros." , nil , nil , GetNumMacros() ) )
        end

        -- Set Keybind if not nil
        if canBuild and keyBind and keyBind ~= "" and not isLogOff then
            SetBindingMacro ( keyBind , name );
        end
    elseif not isLogOff and GRM_G.inCombat then
        if not GRM_G.MacroDelay then
            GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "Unable to create hotkey macro. Player is currently in combat and action is restricted. It will auto-build once out of combat." ) );
            GRM_G.MacroDelay = true;
        end

        -- Retry after 5 seconds.
        C_Timer.After ( 5 , function()
            GRM.CreateMacro ( macroText , name , icon , keyBind , isLogOff );
        end);
    end
end

-- Method:          GRM.CleanupMacros ( int )
-- What it Does:    Clears out known existing macros and keybinds used by this addon for temp abilities, like invite all to guild, or gkick all alts.
-- Purpose:         Prevent macro mess.
GRM.CleanupMacros = function( delay )
    local macros = { "GRM_Inv" , "GRM_Kick" };

    if not delay then
        delay = 0;
    else
        GRM.Report ( GRM.L ( "Macro will auto-remove after {num} seconds." , nil , nil , delay ) );
    end
    C_Timer.After ( delay , function()
        for i = 1 , #macros do
            DeleteMacro ( macros[i] );
        end
    end);
end

-- Method:          GRM.BuildMacroInviteAll ( string , array )
-- What it Does:    Creates a string to be used to set a macro for inviting all players.
-- Purpose:         Added functionality for the player of the addon
GRM.BuildMacroInviteAll = function ( slashCommand , listOfPlayers )
    local newMacroString = "";
    for i = 1 , #listOfPlayers do
        if i == 1 then
            newMacroString = slashCommand .. " " .. listOfPlayers[i][1];
        else
            local finalString = ( newMacroString .. "\n" .. slashCommand .. " " .. listOfPlayers[i][1] );
            if #finalString < 256 then
                newMacroString = finalString;
                -- ELSE - announce and que multiple macros?
            end
        end
    end
    return newMacroString;
end

-- Method:          GRM.BanSpecificPlayer ( string , boolean , string )
-- What it Does:    Bans just a specific player, either in the guild database, or the left player database
-- purpose:         To maintain exact function of banning a player without doing other tasks.
GRM.BanSpecificPlayer = function ( playerName , isAlt , banReason , personWhoBanned )
    local player = GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ][playerName];
    local isFoundInGuild = false;

    if not personWhoBanned then
        personWhoBanned = "";
    end

    if player ~= nil then
        GRM_G.tempAddBanClass = player.class;
    else
        player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][playerName];
        if player ~= nil then
            isFoundInGuild = true;
            GRM_G.tempAddBanClass = player.class;
        end
    end

    if player ~= nil then
        if isFoundInGuild then
            if player.bannedInfo[1] then
                -- Player was previously banned! This is just an update!
                if not isAlt then
                    GRM.Report ( GRM.L ( "{name}'s Ban Info has Been Updated!" , GRM.GetClassifiedName ( playerName , false ) ) );
                end
            else
                player.bannedInfo[1] = true;
                player.bannedInfo[2] = time();
            end
        else
            if player.bannedInfo[1] then
                -- Player was previously banned! This is just an update!
                if not isAlt then
                    GRM.Report ( GRM.L ( "{name}'s Ban Info has Been Updated!" , GRM.GetStringClassColorByName ( playerName , true ) .. playerName .. "|r" ) );
                end
            else
                player.bannedInfo[1] = true;
                player.bannedInfo[2] = time();
            end
        end

        player.bannedInfo[3] = false;
        player.bannedInfo[4] = personWhoBanned;
        player.reasonBanned = banReason;
    end
end


------------------------------------
---- END OF ALT MANAGEMENT ---------
------------------------------------

------------------------------------
------ METADATA TRACKING LOGIC -----
--- Reporting, Live Tracking, Etc --
------------------------------------

-- Method:          GRM.AddMemberRecord()
-- What it Does:    Builds Member Record into Guild History with various metadata
-- Purpose:         For reliable guild data tracking.
GRM.AddMemberRecord = function ( memberInfo , isReturningMember , oldMemberInfo )
    
    -- -- First things first... ensure the player is not already added...
    if GRM_GuildMemberHistory_Save[GRM_G.F][GRM_G.guildName][memberInfo.name] ~= nil then
        return -- Already in the guild. Abort!!!
    end;
    
    local name = memberInfo.name;
    local timeSeconds = time();
    local joinDate = GRM.GetTimestamp();
    
    GRM_GuildMemberHistory_Save[GRM_G.F][GRM_G.guildName][name] = {};
    local member = GRM_GuildMemberHistory_Save[GRM_G.F][GRM_G.guildName][name];

    member["name"] = memberInfo.name;                       -- 1
    member["rankName"] = memberInfo.rankName;               -- 4
    member["rankIndex"] = memberInfo.rankIndex;             -- 5
    member["level"] = memberInfo.level;                     -- 6
    member["note"] = memberInfo.note;                       -- 7
    member["officerNote"] = memberInfo.officerNote;         -- 8
    member["class"] = memberInfo.class;                     -- 9
    member["isMain"] = false;                               -- 10
    member["alts"] = {};                                    -- 11
    member["leftGuildDate"] = {};                           -- 15
    member["leftGuildEpoch"] = {};                          -- 16
    member["bannedInfo"] = { false , 0 , false , "" };      -- 17
    member["reasonBanned"] = "";                            -- 18
    member["oldRank"] = "";                                 -- 19
    member["joinDate"] = {};                                -- 20
    member["joinDateEpoch"] = {};                           -- 21

    member["events"] = { { { 0 , 0 , 0 } , false , "" } , { { 0 , 0 , 0 } , false , "" , 0 } };                                 -- 22 Position 1 = anniversary , Position 2 = birthday = { { day , month , year } , reportedToCalendar , customDescription , timestamp }
    member["customNote"] = { true , 0 , "" , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankCustom , false , "" };    -- 23 { syncEnabled , epochStampOfEdit , "NameOfPlayerWhoEdited" , rankFilterIndex , rankModifiedAtleastOnce , "customNoteString" }
    
    member["lastOnline"] = 0;                               -- 24
    member["rankHistory"] = { { "" , "" , 0 } };            -- 25
    member["recommendToKick"] = false;                      -- 27
    member["recommendToDemote"] = false;
    member["recommendToPromote"] = false;
    member["zone"] = memberInfo.zone;                       -- 28
    member["achievementPoints"] = memberInfo.achievementPoints;  -- 29
    member["isMobile"] = memberInfo.isMobile;               -- 30
    member["guildRep"] = memberInfo.rep;                    -- 31
    member["timeEnteredZone"] = timeSeconds;                -- 32
    member["isOnline"] = memberInfo.isOnline;               -- 33
    member["status"] = memberInfo.status;                   -- 34 AFK, Active, Busy
    member["verifiedJoinDate"] = { "" , 0 };                -- 35
    member["verifiedPromoteDate"] = { "" , 0 };             -- 36
    member["removedAlts"] = {};                             -- 37
    member["mainStatusChangeTime"] = 0;                     -- 39
    member["joinDateUnknown"] = false;                      -- 40
    member["promoteDateUnknown"] = false                    -- 41
    member["GUID"] = memberInfo.GUID;                       -- 42
    member["isUnknown"] = false;                            -- 43
    member["birthdayUnknown"] = false;                      -- 44
    member["safeList"] = {}                                 -- Updated R1.92    - Kick , promote , demote
    member.safeList["kick"] = { false , false , 0 , 0 };    -- Macro Tool monitoring protection
    member.safeList["promote"] = { false , false , 0 , 0 };
    member.safeList["demote"] = { false , false , 0 , 0 };
    member["race"] = memberInfo.race;                       -- 46
    member["sex"] = memberInfo.sex;                         -- 47                                                                
    
    if isReturningMember then
        if oldMemberInfo.oldRank ~= ( "< " .. GRM.L ( "Unknown" ) .. " >" ) then
            member.leftGuildDate = oldMemberInfo.leftGuildDate;
            member.leftGuildEpoch = oldMemberInfo.leftGuildEpoch;
            member.oldRank = oldMemberInfo.oldRank;
            member.joinDate = oldMemberInfo.joinDate;
            table.insert ( member.joinDate , joinDate );                -- Add the new join date to history
            member.joinDateEpoch = oldMemberInfo.joinDateEpoch;
            table.insert ( member.joinDateEpoch , timeSeconds );        -- likewise, add the meta seconds.
            member.events = oldMemberInfo.events;
            member.customNote = oldMemberInfo.customNote;
            member.rankHistory = oldMemberInfo.rankHistory;
            member.race = oldMemberInfo.race;
            member.sex = oldMemberInfo.sex;
        end
        member.bannedInfo = oldMemberInfo.bannedInfo;
        member.reasonBanned = oldMemberInfo.reasonBanned;
    
    end
  
end

-- Method:          GRM.AddMemberToLeftPlayers ( array , int , int , string )
-- What it does:    First, it adds a new player to the saved list. This basically builds a metadata profile. Then, we add that player to players that left, then remove it from current guildies list.
-- Purpose:         If a player installs the addon AFTER people have left the guild, for example, you need to know their details to have them on the ban list. This builds a profile if another sync'd player has them banned
--                  as you cannot just add the name as banned, you literally have to build a full metadata file for them for it to work properly in the case that they return to the guild.
GRM.AddMemberToLeftPlayers = function ( memberInfo , leftGuildMeta , oldJoinDateMeta , personWhoBanned )
    -- First things first, add them!
    GRM.AddMemberRecord( memberInfo , false , nil );
    local leftGuildDate = GRM.EpochToDateFormat ( leftGuildMeta ) .. " 12:01am";
    local oldJoinDate = GRM.EpochToDateFormat ( oldJoinDateMeta ) .. " 12:01am";

    if not personWhoBanned then
        personWhoBanned = "";
    end
    
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][memberInfo.name];

    -- Ok, now that it is added, what we need to do now is REMOVE the player from the GRM_GuildMemberHistory_Save and then add it to the end of the left player history.
    -- Some updates must be had, however.
    if player ~= nil then
        table.insert ( player.leftGuildDate, leftGuildDate );                                                                   -- leftGuildDate
        table.insert ( player.leftGuildEpoch, leftGuildMeta );                                                                  -- leftGuildDateMeta
        player.oldRank = player.rankName;         -- old Rank on leaving.
        table.insert( player.joinDate , oldJoinDate );                                                                               -- oldJoinDate
        table.insert( player.joinDateEpoch , oldJoinDateMeta );                                                                           -- oldJoinDateMeta

        -- Update GUID
        player.GUID = memberInfo.GUID;

        -- If not banned, then let's ensure we reset his data.
        if not player.bannedInfo[1] then
            player.bannedInfo[1] = false;
            player.bannedInfo[2] = 0;
            player.bannedInfo[3] = false;
            player.bannedInfo[4] = personWhoBanned;
            player.reasonBanned = "";
        end
        -- Adding to LeftGuild Player history library
        GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][GRM_G.guildName][memberInfo.name] = {};
        GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][GRM_G.guildName][memberInfo.name] = GRM.DeepCopyArray ( player );
    end

    if player.GUID == "" or player.class == "UNKNOWN" then
        C_Timer.After ( 5 , GRM_Patch.BuildGUIDProfilesForAllNoLongerInGuild );
    end

    -- Now need to remove it
    GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][memberInfo.name] = nil;
end

----------------------------------
------ LOG FUNCTIONS -------------
----------------------------------


-- Method:          GRM.GetMessageRGB( int )
-- What it Does:    Returns the 3 RGB colors colors based on the given index on a 1.0 scale
-- Purpose:         Save on code when need color call. I also did this as a 3 argument return, rather than a single array, just as a proof of concept
--                  since this whole project was also a bit of a Lua learning moment.
GRM.GetMessageRGB = function ( index )
    local r = 0;
    local g = 0;
    local b = 0;

    if index == 1 then      -- Promotion 
        r = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[4][1];
        g = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[4][2];
        b = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[4][3];
    elseif index == 2 then  -- Demotion
        r = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[5][1];
        g = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[5][2];
        b = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[5][3];
    elseif index == 3 then  -- Leveled
        r = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[2][1];
        g = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[2][2];
        b = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[2][3];
    elseif index == 4 then  -- Note
        r = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[6][1];
        g = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[6][2];
        b = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[6][3];
    elseif index == 5 then  -- OfficerNote
        r = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[7][1];
        g = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[7][2];
        b = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[7][3];
    elseif index == 6 then  -- Rank Rename
        r = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[10][1];
        g = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[10][2];
        b = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[10][3];
    elseif index == 7 or index == 8 then  -- Join/Rejoin
        r = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[1][1];
        g = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[1][2];
        b = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[1][3];
    elseif index == 9 or index == 17 or index == 20 or index == 21 then -- Ban and Unbans
        r = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[14][1];
        g = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[14][2];
        b = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[14][3];
    elseif index == 10 then -- Left Guild
        r = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[12][1];
        g = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[12][2];
        b = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[12][3];
    elseif index == 11 then -- NameChange
        r = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[9][1];
        g = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[9][2];
        b = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[9][3];
    elseif index == 12 or index == 18 or index == 98 or index == 99 then -- WhiteText
        r = 1.0;
        g = 1.0;
        b = 1.0;
    elseif index == 13 then -- Rejoining Player Warning (RED)
        r = 0.4;
        g = 0.71;
        b = 0.9;
    elseif index == 14 then -- Return from inactivity
        r = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[3][1];
        g = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[3][2];
        b = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[3][3];
    elseif index == 15 then -- Event Announcement
        r = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[11][1];
        g = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[11][2];
        b = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[11][3];
    elseif index == 16 or index == 22 or index == 23 then -- Recommendations
        r = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[13][1];
        g = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[13][2];
        b = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[13][3];
    elseif index == 19 then  -- Custom Note
        r = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[8][1];
        g = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[8][2];
        b = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[8][3];
    end

    return r , g , b;
end

-- Method:          GRM.AddLog( array )
-- What it Does:    Adds a simple array to the Logreport that includes the indexcode for color, and the included changes as a string
-- Purpose:         For ease in adding to the core log.
GRM.AddLog = function ( logEntry )
    if logEntry ~= nil then
        table.insert ( GRM_LogReport_Save[GRM_G.F][GRM_G.guildName] , logEntry );
    end
end

--------------------------
------ CHAT FUNCTIONS ----
--------------------------

-- Method:          GRM.PrintLog ( array )
-- What it Does:    Sets the color of the string to be reported to the frame (typically chat frame, or to the Log Report frame)
-- Purpose:         Color coding log and chat frame reporting.
GRM.PrintLog = function ( logReport )

    if logReport[2] ~= "" and logReport[2] ~= nil then
        GRM.ReportLocationCheck();

        for i = 1 , #GRM_G.Chat do
            if ( logReport[1] == 1 ) then -- Promoted
                GRM_G.Chat[i]:AddMessage( logReport[2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[4][1] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[4][2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[4][3] );
            elseif ( logReport[1] == 2 ) then -- Demoted
                GRM_G.Chat[i]:AddMessage( logReport[2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[5][1] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[5][2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[5][3] );
            elseif ( logReport[1] == 3 ) then -- Leveled
                GRM_G.Chat[i]:AddMessage( logReport[2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[2][1] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[2][2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[2][3] );
            elseif ( logReport[1] == 4 ) then -- Note
                GRM_G.Chat[i]:AddMessage( logReport[2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[6][1] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[6][2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[6][3] );
            elseif ( logReport[1] == 5 ) then -- Officer Note
                GRM_G.Chat[i]:AddMessage( logReport[2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[7][1] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[7][2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[7][3] );
            elseif ( logReport[1] == 6 ) then -- Rank Renamed
                GRM_G.Chat[i]:AddMessage( logReport[2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[10][1] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[10][2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[10][3] );
            elseif ( logReport[1] == 7 ) or ( logReport[1] == 8 ) then -- Join and Rejoin!
                GRM_G.Chat[i]:AddMessage( logReport[2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[1][1] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[1][2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[1][3] );
            elseif ( logReport[1] == 9 ) or ( logReport[1] == 17 ) or ( logReport[1] == 20 ) or ( logReport[1] == 21 ) then -- WARNING BANNED PLAYER REJOIN!
                GRM_G.Chat[i]:AddMessage( logReport[2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[14][1] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[14][2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[14][3] );
            elseif ( logReport[1] == 10 ) then -- Left the guild
                GRM_G.Chat[i]:AddMessage( logReport[2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[12][1] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[12][2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[12][3] );
            elseif ( logReport[1] == 11 ) then -- Namechanged
                GRM_G.Chat[i]:AddMessage( logReport[2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[9][1] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[9][2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[9][3] );
            elseif ( logReport[1] == 12 ) or ( logReport[1] == 18 ) or ( logReport[1] == 98 ) or ( logReport[1] == 99 ) then -- WHITE TEXT IGNORE RGB COLORING
                GRM_G.Chat[i]:AddMessage( logReport[2] , 1.0 , 1.0 , 1.0 );
            elseif ( logReport[1] == 13 ) then -- Rejoining PLayer Custom Note Report - returning player warning
                GRM_G.Chat[i]:AddMessage( logReport[2] , 0.4 , 0.71 , 0.9 );
            elseif ( logReport[1] == 14 ) then -- Player has returned from inactivity
                GRM_G.Chat[i]:AddMessage( logReport[2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[3][1] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[3][2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[3][3] );
            elseif ( logReport[1] == 15 ) then -- For event notifications like upcoming anniversaries.
                GRM_G.Chat[i]:AddMessage( logReport[2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[11][1] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[11][2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[11][3] );
            elseif ( logReport[1] == 16 ) or ( logReport[1] == 22 ) or ( logReport[1] == 23 ) then -- For Recommendations
                GRM_G.Chat[i]:AddMessage( logReport[2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[13][1] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[13][2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[13][3] );
            elseif ( logReport[1] == 19 ) then -- For Custom Note
                GRM_G.Chat[i]:AddMessage ( logReport[2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[8][1] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[8][2] , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logColor[8][3] );
            end
        end
    end
end

-- Method:          GRM.Report ( string )
-- What it Does:    Sends to the main chat window messages on various events as deemed necessary to report on by addon creator.
-- Purpose:         To clean up the reporting and have a way to present the information blended into the default system UI
GRM.Report = function ( msg )
    if GRM_G.AddonIsFullyConfigured then
        GRM.ReportLocationCheck();
    end

    for i = 1 , #GRM_G.Chat do
        GRM_G.Chat[i]:AddMessage ( msg , 1.0 , 0.84 , 0 );
    end

end

-- Method:          GRM.ReportLocationCheck()
-- What it Does:    It checks if the window for the custom location to report is still open, and if it is not, it sets the addon to report back to default location if all the custom ones are closed.
-- Purpose:         Customization of note location and automation for good user experience to auto reset to defaults if player closes the window.
GRM.ReportLocationCheck = function()

    -- Validate the reporting windows
    -- Remove invalid - if you end up removing all, reset to the default.

    for i = #GRM_G.Chat , 1 , -1 do
        if not GetClickFrame ( GRM_G.Chat[i]:GetName() .. "Tab" ):IsVisible() then
            table.remove ( GRM_G.Chat , i );
        end
    end
    
    if #GRM_G.Chat == 0 then
        GRM_G.Chat = { DEFAULT_CHAT_FRAME };
    end

end

-----------------------------------
--------- SCROLL FRAME LOGIC ------
-----------------------------------

-- Method:          GRM.BuildEventCalendarManagerScrollFrame()
-- What it Does:    This populates properly the event ScrollFrame
-- Purpose:         Scroll Frame management for smoother User Experience
GRM.BuildEventCalendarManagerScrollFrame = function()
    -- SCRIPT LOGIC ON ADD EVENT SCROLLING FRAME
    local scrollHeight = 0;
    local scrollWidth = 561;
    local buffer = 15;

    GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons = GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons or {};  -- Create a table for the Buttons.
    -- populating the window correctly.
    local tempHeight = 0;
    for i = 1 , #GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName] do
        -- if font string is not created, do so.
        if not GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons[i] then
            local tempButton = CreateFrame ( "Button" , "PlayerToAdd" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame ); -- Names each Button 1 increment up
            GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons[i] = { tempButton , tempButton:CreateFontString ( "PlayerToAddText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) , tempButton:CreateFontString ( "PlayerToAddTitleText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) , tempButton:CreateFontString ( "PlayerToAddDescriptionText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) };
        end
        local EventButtons = GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons[i][1];
        local EventButtonsText = GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons[i][2];
        local EventButtonsText2 = GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons[i][3];
        local EventButtonsText3 = GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons[i][4];
        local classColorRGB = GRM.GetClassColorRGB ( GRM.GetPlayerClass ( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][1] ) , false );

        -- Set the values..
        EventButtons:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame , 7 , -99 );
        EventButtons:SetWidth ( 558 );
        EventButtons:SetHeight ( 19 );
        EventButtons:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        EventButtons:RegisterForDrag ( "LeftButton" );
        EventButtons:SetScript ( "OnDragStart" , function()
            GRM_UI.GRM_RosterChangeLogFrame:StartMoving();
        end);
        EventButtons:SetScript ( "OnDragStop" , function()
            GRM_UI.GRM_RosterChangeLogFrame:StopMovingOrSizing()
            GRM_UI.SaveCorePosition();
        end);

        EventButtonsText:SetText ( GRM.SlimName ( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][1] ) );
        EventButtonsText:SetTextColor ( classColorRGB[1] , classColorRGB[2] , classColorRGB[3] , 1 );
        EventButtonsText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 11 );
        EventButtonsText:SetPoint ( "LEFT" , EventButtons );
        EventButtonsText:SetJustifyH ( "LEFT" );
        local name = GRM.SlimName ( string.sub ( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][2] , 0 , ( string.find ( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][2] , " " ) - 1 ) - 2 ) );
        local eventName = string.sub ( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][2] , string.find ( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][2] , " " ) , #GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][2] );
        local result = "";
        -- For localization of final display fontstring
        if string.find ( eventName , "Anniversary!" ) ~= nil then
            result = GRM.L ( "{name}'s Anniversary!" , name );
        elseif string.find ( eventName , "Birthday!" ) ~= nil then
            result = GRM.L ( "{name}'s Birthday!" , name );
        else
            result = GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][2];
        end
        EventButtonsText2:SetText ( result );
        EventButtonsText2:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 11 );
        EventButtonsText2:SetJustifyH ( "LEFT" );
        EventButtonsText2:SetWordWrap ( false );
        EventButtonsText2:SetWidth ( 171 )
        EventButtonsText3:SetText ( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][6] );
        EventButtonsText3:SetWidth ( 275 );
        EventButtonsText3:SetWordWrap ( false );
        EventButtonsText3:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 11 );
        EventButtonsText3:SetPoint ( "LEFT" , EventButtons );
        EventButtonsText3:SetJustifyH ( "LEFT" );

        EventButtons:SetScript ( "OnEnter" , function()
            if GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i] ~= nil and GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][1] ~= nil then
                GRM_UI.SetTooltipScale();

                GameTooltip:SetOwner ( EventButtons  , "ANCHOR_CURSOR" );
                GameTooltip:AddLine ( GRM.GetClassifiedName ( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][1] , false ) );
                GameTooltip:AddLine ( GRM.L ( "|CFFE6CC7FClick|r to select player event" ) );
                GameTooltip:AddLine ( GRM.L ( "|CFFE6CC7FCtrl-Click|r to open Player Window" ) );
                GameTooltip:AddLine ( GRM.L ( "|CFFE6CC7FCtrl-Shift-Click|r to Search the Log for Player" ) );
                GameTooltip:Show();

            end
        end);
        EventButtons:SetScript ( "OnLeave" , function()
            GRM.RestoreTooltip();
        end);

        local timer = 0;
        EventButtons:SetScript ( "OnUpdate" , function( self , elapsed )
            timer = timer + elapsed;
            if timer > 0.1 then
                if self:IsMouseOver ( 1 , -1 , -1 , 1 ) then
                    if EventButtonsText3:IsMouseOver( 9 , -9 , -9 , 9 ) then            -- Since the button is large, the text needs to compensate.

                        if GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i] ~= nil then

                            GRM.RestoreTooltip();
                            GRM_UI.SetTooltipScale();
                            GameTooltip:SetOwner( self , "ANCHOR_CURSOR"  );
                            GameTooltip:AddLine( "|cFFFFFFFF" .. string.upper ( GRM.L ( "Full Description:" ) ) );
                            GameTooltip:AddLine( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][6] , 1.0 , 0.84 , 0 , true );
                            GameTooltip:Show();

                        end

                    elseif GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i] ~= nil then

                        GameTooltip:SetOwner ( EventButtons  , "ANCHOR_CURSOR" );
                        GameTooltip:AddLine ( GRM.GetClassifiedName ( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][1] , false ) );
                        GameTooltip:AddLine ( GRM.L ( "|CFFE6CC7FClick|r to select player event" ) );
                        GameTooltip:AddLine ( GRM.L ( "|CFFE6CC7FCtrl-Click|r to open Player Window" ) );
                        GameTooltip:AddLine ( GRM.L ( "|CFFE6CC7FCtrl-Shift-Click|r to Search the Log for Player" ) );
                        GameTooltip:Show();
                    end
                end
                timer = 0;
            end
        end);

        -- Logic
        EventButtons:SetScript ( "OnClick" , function ( self , button )
            if button == "LeftButton" then
                -- For highlighting purposes
                -- parse out the button number, which will correlate with addonque frame...
                local buttonName = self:GetName();
                local index = -1;
                if tonumber ( string.sub ( buttonName , #buttonName - 1 ) ) == nil then -- If nil then it is only 1 number
                    index = tonumber ( string.sub ( buttonName , #buttonName ) );
                else
                    index = tonumber ( string.sub ( buttonName , #buttonName - 1 ) );
                end

                if IsShiftKeyDown() and IsControlKeyDown() then
                    GRM.RestoreTooltip();
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogTab:Click();
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogEditBox:SetText( GRM.SlimName ( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][index][1] ) );
                elseif IsControlKeyDown() then
                    GRM.OpenPlayerWindow( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][index][1] );
                else

                    -- Deal with the highlights
                    for j = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons do
                        if EventButtons ~= GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons[j][1] then
                            GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons[j][1]:UnlockHighlight();
                        else
                            GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons[j][1]:LockHighlight();
                        end
                    end
                    
                    if ( GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameNameToAddText:GetText() == nil ) or ( GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameNameToAddText:GetText() ~= nil and ( GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameNameToAddText:GetText() ~= EventButtonsText2:GetText() or not GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameNameToAddText:IsVisible() ) ) then
                        GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameNameToAddText:SetText ( EventButtonsText2:GetText() );
                        GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameNameToAddTitleText:SetText ( EventButtonsText: GetText() );
                        GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameNameDateText:SetText(  GRM.FormatTimeStamp ( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][index][4] .. " " .. monthEnum2 [ tostring ( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][index][3] ) ] .. " '19" , false , true ) );

                        if GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameStatusMessageText:IsVisible() then
                            GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameStatusMessageText:Hide();
                            GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameNameToAddText:Show();
                            GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameNameDateText:Show();
                        end
                    end
                end
            end

        end);
        
        -- Now let's pin it!
        if i == 1 then
            EventButtons:SetPoint ( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame , "TOPLEFT" , 3 , -12 );
            EventButtonsText:SetPoint ( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame , "TOPLEFT" , 3 , -12 );
            EventButtonsText2:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame , -89 , -12 );
            EventButtonsText3:SetPoint ( "LEFT" , EventButtonsText2 , "RIGHT" , 3 , 0 );
            scrollHeight = scrollHeight + EventButtons:GetHeight();
        else
            EventButtons:SetPoint( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons[i - 1][1] , "BOTTOMLEFT" , 0 , - buffer );
            EventButtonsText:SetPoint ( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons[i - 1][2] , "BOTTOMLEFT" , 0 , - ( buffer + tempHeight ) );
            EventButtonsText2:SetPoint ( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons[i - 1][3] , "BOTTOMLEFT" , 0 , - ( buffer + tempHeight ) );
            EventButtonsText3:SetPoint ( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons[i - 1][4] , "BOTTOMLEFT" , 0 , - ( buffer + tempHeight ) );
            scrollHeight = scrollHeight + EventButtons:GetHeight() + buffer;
        end
        EventButtons:Show();
        tempHeight = ( EventButtons:GetHeight() - EventButtonsText2:GetHeight() );
    end
    -- Update the size -- it either grows or it shrinks!
    GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame:SetSize ( scrollWidth , scrollHeight );

    --Set Slider Parameters ( has to be done after the above details are placed )
    local scrollMax = ( scrollHeight - 348 ) + ( buffer * 1.5 );
    if scrollMax < 0 then
        scrollMax = 0;
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollFrameSlider:SetMinMaxValues ( 0 , scrollMax );
    -- Mousewheel Scrolling Logic
    GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollFrame:EnableMouseWheel( true );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollFrame:SetScript( "OnMouseWheel" , function( _ , delta )
        local current = GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollFrameSlider:GetValue();
        
        if IsShiftKeyDown() and delta > 0 then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollFrameSlider:SetValue ( 0 );
        elseif IsShiftKeyDown() and delta < 0 then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollFrameSlider:SetValue ( scrollMax );
        elseif delta < 0 and current < scrollMax then
            if IsControlKeyDown() then
                GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollFrameSlider:SetValue ( current + 60 );
            else
                GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollFrameSlider:SetValue ( current + 20 );
            end
        elseif delta > 0 and current > 1 then
            if IsControlKeyDown() then
                GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollFrameSlider:SetValue ( current - 60 );
            else
                GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollFrameSlider:SetValue ( current - 20 );
            end
        end
    end);
end


-- Method:          GRM.BuildAddonUserScrollFrame()
-- What it Does:    Builds the potential scroll frame to house the entire list of all guildies who have addon installed and enabled
-- Purpose:         Much better and cleaner UI to have a scroll window, imo.
GRM.BuildAddonUserScrollFrame = function()
    local scrollHeight = 0;
    local scrollWidth = 561;
    local buffer = 15;

    GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame.AllFrameFontstrings = GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame.AllFrameFontstrings or {};  -- Create a table for the Buttons.
    -- Building all the fontstrings.
    for i = 1 , #GRM_G.currentAddonUsers do
        -- We know there is at least one, so let's hide the warning string...
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame.GRM_AddonUsersCoreFrameTitleText2:Hide();
        -- if font string is not created, do so.
        if not GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame.AllFrameFontstrings[i] then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame.AllFrameFontstrings[i] = { GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame:CreateFontString ( "GRM_AddonUserNameText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) , GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame:CreateFontString ( "GRM_AddonUserSyncText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) , GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame:CreateFontString ( "GRM_AddonUserVersionText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) };
        end

        local AddonUserText1 = GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame.AllFrameFontstrings[i][1];
        local AddonUserText2 = GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame.AllFrameFontstrings[i][2];
        local AddonUserText3 = GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame.AllFrameFontstrings[i][3];
        local classColorRGB = GRM.GetClassColorRGB ( GRM.GetPlayerClass ( GRM_G.currentAddonUsers[i][1] ) );
        AddonUserText1:SetText ( GRM.SlimName ( GRM_G.currentAddonUsers[i][1] ) );
        if classColorRGB ~= nil then
            AddonUserText1:SetTextColor ( classColorRGB[1] , classColorRGB[2] , classColorRGB[3] );
        end
        AddonUserText1:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 13 );
        AddonUserText1:SetJustifyH ( "LEFT" );

        -- Get the right RGB coloring for the text.
        local r , g , b;
        if GRM_G.currentAddonUsers[i][2] == "Ok!" then
            r = 0;
            g = 0.77;
            b = 0.063;
        else
            r = 0.64;
            g = 0.102;
            b = 0.102;
        end
        AddonUserText2:SetTextColor ( r , g , b , 1.0 ); 
        AddonUserText2:SetText ( GRM.L ( GRM_G.currentAddonUsers[i][2] ) );
        AddonUserText2:SetWidth ( 240 );
        AddonUserText2:SetWordWrap ( false );
        AddonUserText2:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 13 );
        AddonUserText2:SetJustifyH ( "CENTER" );
        AddonUserText3:SetText ( string.sub ( GRM_G.currentAddonUsers[i][3] , string.find ( GRM_G.currentAddonUsers[i][3] , "R" , -8 ) , #GRM_G.currentAddonUsers[i][3] ) );
        AddonUserText3:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 13 );
        AddonUserText3:SetJustifyH ( "CENTER" );
        AddonUserText3:SetWidth ( 125 );

        local stringHeight = AddonUserText1:GetStringHeight();

        -- Now let's pin it!
        if i == 1 then
            AddonUserText1:SetPoint( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame , "TOPLEFT" , 5 , - 15 );
            AddonUserText2:SetPoint( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame , "TOP" , -6 , - 15 );
            AddonUserText3:SetPoint( "TOPRIGHT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame , "TOPRIGHT" , -2 , - 15 );
            scrollHeight = scrollHeight + stringHeight;
        else
            AddonUserText1:SetPoint( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame.AllFrameFontstrings[i - 1][1] , "BOTTOMLEFT" , 0 , - buffer );
            AddonUserText2:SetPoint( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame.AllFrameFontstrings[i - 1][2] , "BOTTOMLEFT" , 0 , - buffer );
            AddonUserText3:SetPoint( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame.AllFrameFontstrings[i - 1][3] , "BOTTOMLEFT" , 0 , - buffer );
            scrollHeight = scrollHeight + stringHeight + buffer;
        end
        AddonUserText1:Show();
        AddonUserText2:Show();
        AddonUserText3:Show();
    end
            
    -- Hides all the additional strings... if necessary ( necessary because some people may have logged off thus you need to hide those frames)
    for i = #GRM_G.currentAddonUsers + 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame.AllFrameFontstrings do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame.AllFrameFontstrings[i][1]:Hide();
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame.AllFrameFontstrings[i][2]:Hide();
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame.AllFrameFontstrings[i][3]:Hide();
    end 

    -- Update the size -- it either grows or it shrinks!
    GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame:SetSize ( scrollWidth , scrollHeight );

    --Set Slider Parameters ( has to be done after the above details are placed )
    local scrollMax = ( scrollHeight - 391 ) + ( buffer * .5 );  -- 18 comes from fontSize (11) + buffer (7);
    if scrollMax < 0 then
        scrollMax = 0;
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollFrameSlider:SetMinMaxValues ( 0 , scrollMax );
    -- Mousewheel Scrolling Logic
    GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollFrame:EnableMouseWheel( true );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollFrame:SetScript( "OnMouseWheel" , function( _ , delta )
        local current = GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollFrameSlider:GetValue();
        
        if IsShiftKeyDown() and delta > 0 then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollFrameSlider:SetValue ( 0 );
        elseif IsShiftKeyDown() and delta < 0 then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollFrameSlider:SetValue ( scrollMax );
        elseif delta < 0 and current < scrollMax then
            if IsControlKeyDown() then
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollFrameSlider:SetValue ( current + 60 );
            else
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollFrameSlider:SetValue ( current + 20 );
            end
        elseif delta > 0 and current > 1 then
            if IsControlKeyDown() then
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollFrameSlider:SetValue ( current - 60 );
            else
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollFrameSlider:SetValue ( current - 20 );
            end
        end
    end);

    -- Statement on who is using the addon!
    if #GRM_G.currentAddonUsers == 0 then
        local numGuildiesOnline = GRM.GetNumGuildiesOnline( false ) - 1; -- Don't include yourself!
        local result = GRM.L ( "No Guildie Online With Addon." );
        if numGuildiesOnline == 1 then
            result = result .. "\n" .. GRM.L ( "ONE Person is Online. Recommend It!" );
        elseif numGuildiesOnline > 1 then
            result = result .. "\n" .. GRM.L ( "{num} others are Online! Recommend It!" , nil , nil , numGuildiesOnline );
        end
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame.GRM_AddonUsersCoreFrameTitleText2:SetText ( result );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersScrollChildFrame.GRM_AddonUsersCoreFrameTitleText2:Show();
    end
end

-- Method:          GRM.GetSortedAltNamesWithDetails ( string )
-- What it Does:    Returns the alt grouping of the player, with their own name, the player details, as well as main sorted as index 1.
-- Purpose:         Mainly for use with the alt groupings window on mouseover of the player alts on core popup window.
GRM.GetSortedAltNamesWithDetails = function ( playerName )
    local finalList = {};
    local type = GRM_G.altDetailsControl[1];
    local descending = GRM_G.altDetailsControl[2];
    local isAdded = false;
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][playerName];

    if player ~= nil then
        local tempAlt;

        -- Build the list of alts.
        if #player.alts > 0 then
            for r = 1 , #player.alts do

                tempAlt = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][player.alts[r][1]];
                if tempAlt ~= nil then

                    local playerDetails = { tempAlt.name , tempAlt.level , tempAlt.class , tempAlt.rankIndex , tempAlt.isMain , tempAlt.lastOnline , tempAlt.joinDate[#tempAlt.joinDate] , tempAlt.rankHistory[#tempAlt.rankHistory][2] , tempAlt.isOnline };
                    
                    -- Alphabetical
                    if type == 1 then
                        -- Add the alt to the list
                        table.insert ( finalList , 1 , playerDetails );
                        if r == #player.alts then
                            if descending then
                                sort ( finalList , function ( a , b ) return a[1] < b[1] end );     -- Sorts complex table with first index alphabetically with score ranking
                            else
                                sort ( finalList , function ( a , b ) return b[1] < a[1] end );     -- Sorts complex table with first index alphabetically with score ranking
                            end
                        end
                    
                        -- By Time
                    elseif type == 2 then
                        if #finalList == 0 then
                            table.insert ( finalList , playerDetails );
                        else
                            -- Now, need to scan through the list to see what we have
                            isAdded = false;
                            for j = 1 , #finalList do
                                if descending then
                                    if playerDetails[6] < finalList[j][6] then
                                        table.insert ( finalList , j , playerDetails );
                                        isAdded = true;
                                        break;
                                    end
                                else
                                    if playerDetails[6] > finalList[j][6] then
                                        table.insert ( finalList , j , playerDetails );
                                        isAdded = true;
                                        break;
                                    end
                                end
                            end
                            if not isAdded then
                                table.insert ( finalList , playerDetails ); -- Just add it to the end
                            end
                        end

                        -- by Level
                    elseif type == 3 then

                        if #finalList == 0 then
                            table.insert ( finalList , playerDetails );
                        else
                            -- Now, need to scan through the list to see what we have
                            isAdded = false;
                            for j = 1 , #finalList do
                                if descending then
                                    if playerDetails[2] > finalList[j][2] then
                                        table.insert ( finalList , j , playerDetails );
                                        isAdded = true;
                                        break;
                                    end
                                else
                                    if playerDetails[2] < finalList[j][2] then
                                        table.insert ( finalList , j , playerDetails );
                                        isAdded = true;
                                        break;
                                    end
                                end
                            end
                            if not isAdded then
                                table.insert ( finalList , playerDetails ); -- Just add it to the end
                            end
                        end

                        -- by Rank
                    elseif type == 4 then
                        if #finalList == 0 then
                            table.insert ( finalList , playerDetails );
                        else
                            -- Now, need to scan through the list to see what we have
                            isAdded = false;
                            for j = 1 , #finalList do
                                if descending then
                                    if playerDetails[4] < finalList[j][4] then
                                        table.insert ( finalList , j , playerDetails );
                                        isAdded = true;
                                        break;
                                    end
                                else
                                    if playerDetails[4] > finalList[j][4] then
                                        table.insert ( finalList , j , playerDetails );
                                        isAdded = true;
                                        break;
                                    end
                                end
                            end
                            if not isAdded then
                                table.insert ( finalList , playerDetails ); -- Just add it to the end
                            end
                        end
                    end
                end
            end
        end
    end
    return finalList;
end

-- Method:          GRM.BuildAltGroupingScrollFrame( string )
-- What it Does:    It builds the alt groupings info on mouseover with shift pressed
-- Purpose:         For quick look at the alt info...
GRM.BuildAltGroupingScrollFrame = function( currentName )
    local scrollHeight = 0;
    local scrollWidth = 315;
    local buffer = 9;
    GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.GRM_AltGroupingScrollFrameSlider:SetValue(0);

    GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.AllFrameFontstrings = GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.AllFrameFontstrings or {};  -- Create a table for the Buttons.
    -- Building all the fontstrings.
    GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrameTitle:SetText ( GRM_G.CurrentCalendarHexCode .. GRM.L ( "{name}'s Alts" , GRM.SlimName ( currentName ) ) );

    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][ currentName ];
    if player ~= nil then

        -- Collect all the alt names and sort them.
        local listOfAlts = GRM.GetSortedAltNamesWithDetails ( currentName );
        local mainFound = false;
        for i = 1 , #listOfAlts do  -- The +1 is for the player so they can count themselves too...
            -- if font string is not created, do so.
            if not GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.AllFrameFontstrings[i] then
                GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.AllFrameFontstrings[i] = { GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame:CreateFontString ( "GRM_AltName" .. i , "OVERLAY" , "GameFontWhiteTiny" ) , GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame:CreateFontString ( "GRM_AltLevel" .. i , "OVERLAY" , "GameFontWhiteTiny" ) , GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame:CreateFontString ( "GRM_AltRank" .. i , "OVERLAY" , "GameFontWhiteTiny" ) , GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame:CreateFontString ( "GRM_AltLastOnline" .. i , "OVERLAY" , "GameFontWhiteTiny" ) };
            end
    
            local AltName = GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.AllFrameFontstrings[i][1];
            local AltLvl = GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.AllFrameFontstrings[i][2];
            local AltRank = GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.AllFrameFontstrings[i][3];
            local AltLastOnline = GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.AllFrameFontstrings[i][4];
            
            local altClassRGB = GRM.GetClassColorRGB ( listOfAlts[i][3] , false );

            AltName:SetText ( GRM.SlimName ( listOfAlts[i][1] ) );
            AltName:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 9 );
            AltName:SetJustifyH ( "LEFT" );
            AltName:SetTextColor ( altClassRGB[1] , altClassRGB[2] , altClassRGB[3] , 1 );
    
            AltLvl:SetText ( listOfAlts[i][2] );
            AltLvl:SetWidth ( 75 );
            AltLvl:SetWordWrap ( false );
            AltLvl:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 9 );
            AltLvl:SetJustifyH ( "CENTER" );

            AltRank:SetText ( GuildControlGetRankName( listOfAlts[i][4] + 1 ) );
            AltRank:SetWidth ( 75 );
            AltRank:SetWordWrap ( false );
            AltRank:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 9 );
            AltRank:SetJustifyH ( "CENTER" );

            AltLastOnline:SetWidth ( 100 );
            AltLastOnline:SetWordWrap ( false );
            AltLastOnline:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 9 );

            -- Last Online
            if listOfAlts[i][9] then
                AltLastOnline:SetText ( GRM.L ( "Online" ) );
                AltLastOnline:SetTextColor ( 0.12 , 1.0 , 0.0 , 1.0 );
            else
                AltLastOnline:SetText ( GRM.HoursReport ( listOfAlts[i][6] ) );
                AltLastOnline:SetTextColor ( 1 , 1 , 1 , 1.0 );
            end
    
            local stringHeight = AltLastOnline:GetStringHeight();
    
            -- Now let's pin it!
            if i == 1 then
                AltName:SetPoint( "TOPLEFT" , GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame , "TOPLEFT" , 17 , -5 );
                AltLvl:SetPoint( "TOP" , GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame , "TOP" , -40 , -5 );
                AltRank:SetPoint( "TOP" , GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame , "TOP" , 30 , -5 );
                AltLastOnline:SetPoint( "TOP" , GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame , "TOP" , 108 , -5 );

                scrollHeight = scrollHeight + stringHeight;
            else
                AltName:SetPoint( "TOPLEFT" , GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.AllFrameFontstrings[i - 1][1] , "BOTTOMLEFT" , 0 , - buffer );
                AltLvl:SetPoint( "TOP" , GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.AllFrameFontstrings[i - 1][2] , "BOTTOM" , 0 , - buffer );
                AltRank:SetPoint( "TOP" , GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.AllFrameFontstrings[i - 1][3] , "BOTTOM" , 0 , - buffer );
                AltLastOnline:SetPoint( "TOP" , GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.AllFrameFontstrings[i - 1][4] , "BOTTOM" , 0 , - buffer );
                scrollHeight = scrollHeight + stringHeight + buffer;
            end
            -- Main tag
            if listOfAlts[i][5] then
                GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.GRM_MainTag:SetPoint ( "TOP" , AltName , "BOTTOM" );
                GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.GRM_MainTag:Show();
                mainFound = true;
            end
            
            AltName:Show();
            AltLvl:Show();
            AltRank:Show();
            AltLastOnline:Show();
        end

        if not mainFound then
            GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.GRM_MainTag:Hide();
        end
                
        -- Hides all the additional strings... if necessary
        for i = #listOfAlts + 1 , #GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.AllFrameFontstrings do
            GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.AllFrameFontstrings[i][1]:Hide();
            GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.AllFrameFontstrings[i][2]:Hide();
            GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.AllFrameFontstrings[i][3]:Hide();
            GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.AllFrameFontstrings[i][4]:Hide();
        end 
    
        -- Update the size -- it either grows or it shrinks!
        GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame:SetSize ( scrollWidth , scrollHeight );
    
        --Set Slider Parameters ( has to be done after the above details are placed )
        local scrollMax = ( scrollHeight - 105 ) + ( buffer ); 
        if scrollMax < 0 then
            scrollMax = 0;
            GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.GRM_AltGroupingScrollFrameSlider:Hide();
            GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollFrame:EnableMouseWheel( false );
        else
            GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.GRM_AltGroupingScrollFrameSlider:SetMinMaxValues ( 0 , scrollMax );
            -- Mousewheel Scrolling Logic
            GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollFrame:EnableMouseWheel( true );
            GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollFrame:SetScript( "OnMouseWheel" , function( _ , delta )
                local current = GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.GRM_AltGroupingScrollFrameSlider:GetValue();
                
                if delta < 0 and current < scrollMax then
                    if IsControlKeyDown() then
                        GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.GRM_AltGroupingScrollFrameSlider:SetValue ( current + 60 );
                    else
                        GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.GRM_AltGroupingScrollFrameSlider:SetValue ( current + 20 );
                    end
                elseif delta > 0 and current > 1 then
                    if IsControlKeyDown() then
                        GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.GRM_AltGroupingScrollFrameSlider:SetValue ( current - 60 );
                    else
                        GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.GRM_AltGroupingScrollFrameSlider:SetValue ( current - 20 );
                    end
                end
            end);
            GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame.GRM_AltGroupingScrollChildFrame.GRM_AltGroupingScrollFrameSlider:Show();
        end
    end 
end

-- Method:          GRM.BuildAutoCompleteAltSelectionScrollFrame ( table )
-- What it Does:    The add alt list is now a scroll frame rather than 6 static choices...
-- Purpose:         Quality of life improvement on selecting players to add to alt. Just makes it easier to find matches.
GRM.BuildAutoCompleteAltSelectionScrollFrame = function ( listOfAlts )
    local buttonCap = GRM_G.MaxAltAutoCompleteList;
    local numButtons = #listOfAlts;
    if numButtons > buttonCap then
        numButtons = buttonCap;
    end
    GRM_G.NumberAltInSelection = numButtons;

    local scrollHeight = 0;
    local scrollWidth = GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame:GetWidth() - 10;

    GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons = GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons or {};  -- Create a table for the Buttons.
    for i = 1 , numButtons do  -- The +1 is for the player so they can count themselves too...
        -- if font string is not created, do so.
        if not GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons[i] then
            local tempButton = CreateFrame ( "Button" , "AltSelection" .. i , GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame ); -- Names each Button 1 increment up
            GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons[i] = { tempButton , tempButton:CreateFontString ( "AltButtonText" .. i , "OVERLAY" , "GameFontWhiteTiny" )  };
        end

        local button = GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons[i][1];
        local buttonText = GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons[i][2];

        button:SetSize ( 112 , 14 );
        button:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );

        local classColor = GRM.GetClassColorRGB ( listOfAlts[i][2] , false );
        buttonText:SetTextColor ( classColor[1] , classColor[2] , classColor[3] );
        buttonText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 8 );
        buttonText:SetJustifyH ( "LEFT" );
        buttonText:SetText ( listOfAlts[i][1] .. GRM.GetAltTag ( listOfAlts[i][3] ) );
        buttonText:SetPoint ( "LEFT" , button );

        if i == 1 then
            button:SetPoint ( "TOP" , GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame , "TOP" , 0 , 0 );
        else 
            button:SetPoint ( "TOPLEFT" , GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons[i-1][1] , "BOTTOMLEFT" , 0 , 0 );
        end
        -- Build height
        scrollHeight = scrollHeight + button:GetHeight();
        -- Set button logic...
        button:SetScript ( "OnClick" , function ( self , key )
            if key == "LeftButton" then

                local buttonName = self:GetName();
                local index = -1;
                if tonumber ( string.sub ( buttonName , #buttonName - 1 ) ) == nil then -- If nil then it is only 1 number
                    index = tonumber ( string.sub ( buttonName , #buttonName ) );
                else
                    index = tonumber ( string.sub ( buttonName , #buttonName - 1 ) );
                end
                local result = GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons[index][2]:GetText();
                GRM_G.currentHighlightIndex = index;
                result = GRM.RemoveMainAltTags ( result );
                GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltEditBox:SetText ( result );
                GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltEditBox:SetFocus();
                GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltEditBox:SetCursorPosition ( #GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltEditBox:GetText() );
                GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltEditBox:HighlightText ( #GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltEditBox:GetText() + 1 );
                GRM.AddAltAutoComplete();
            end
        end);
        button:Show();
    end
    -- Hide unused buttons...
    for i = numButtons + 1 , #GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons do
        GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons[i][1]:Hide();
    end
    -- Reset the highlight...

    -- Configure Slider
    GRM.ConfigureSlider ( GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollFrame , GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame , GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollFrameSlider , scrollWidth , scrollHeight , 84 , true )
end

-- Method:          GRM.BuildAutoCompleteBanNames ( table )
-- What it Does:    When cycling ban names, this provides an auto-complete list
-- Purpose:         Quality of life improvement on selecting players to add as a ban. Just makes it easier to find matches.
GRM.BuildAutoCompleteBanNames = function ( listOfAlts )
    local buttonCap = GRM_G.MaxAltAutoCompleteList;
    local numButtons = #listOfAlts;
    if numButtons > buttonCap then
        numButtons = buttonCap;
    end

    local scrollHeight = 0;
    local scrollWidth = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollFrame:GetWidth() - 5;

    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollChildFrame.AllButtons = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollChildFrame.AllButtons or {};  -- Create a table for the Buttons.
    for i = 1 , numButtons do  -- The +1 is for the player so they can count themselves too...
        -- if font string is not created, do so.
        if not GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollChildFrame.AllButtons[i] then
            local tempButton = CreateFrame ( "Button" , "BanButton" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollChildFrame ); -- Names each Button 1 increment up
            GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollChildFrame.AllButtons[i] = { tempButton , tempButton:CreateFontString ( "BanButtonText" .. i , "OVERLAY" , "GameFontWhiteTiny" )  };
        end

        local button = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollChildFrame.AllButtons[i][1];
        local buttonText = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollChildFrame.AllButtons[i][2];

        button:SetSize ( 112 , 14 );
        button:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );

        local classColor = GRM.GetClassColorRGB ( listOfAlts[i][2] , false );
        buttonText:SetTextColor ( classColor[1] , classColor[2] , classColor[3] );
        buttonText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 8 );
        buttonText:SetJustifyH ( "LEFT" );
        buttonText:SetText ( listOfAlts[i][1] );
        buttonText:SetPoint ( "LEFT" , button );

        if i == 1 then
            button:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollChildFrame , "TOP" , 0 , 0 );
            GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollFrame:Show();
        else 
            button:SetPoint ( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollChildFrame.AllButtons[i-1][1] , "BOTTOMLEFT" , 0 , 0 );
        end
        -- Build height
        scrollHeight = scrollHeight + button:GetHeight();
        -- Set button logic...
        button:SetScript ( "OnClick" , function ( _ , key )
            if key == "LeftButton" then
                GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanNameSelectionEditBox.clickControl = true;
                local indexOfClass = 1
                local result = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollChildFrame.AllButtons[i][2]:GetText();
                local class = "";

                for i = 1 , #GRM_G.autoCompleteBanList do
                    if GRM_G.autoCompleteBanList[i][1] == result then
                        class = GRM_G.autoCompleteBanList[i][2];
                    end
                end

                -- Set the index                
                for j = 1 , #AllClasses do
                    if string.lower ( AllClasses[j] ) == string.lower ( class ) then
                        indexOfClass = j;
                        break;
                    end
                end

                GRM_G.tempAddBanClass = string.upper ( AllClasses[indexOfClass] );
                local colors = GRM.GetClassColorRGB ( GRM_G.tempAddBanClass );

                -- Now, update the targets
                local tempName = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanNameSelectionEditBox:GetText() .. "-" .. GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerSelected.GRM_BanServerSelectedText:GetText();
                local tempClass = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownClassSelectedText:GetText();

                GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanNameSelectionEditBox:SetText ( GRM.SlimName ( result ) );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerSelected.GRM_BanServerSelectedText:SetText ( string.sub ( result , string.find ( result , "-" ) + 1 ) );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownClassSelectedText:SetText ( GRM.L ( GRM.FormatInputName ( GRM_G.tempAddBanClass ) ) );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownClassSelectedText:SetTextColor ( colors[1] , colors[2] , colors[3] );

                if tempName == result and tempClass == GRM.L ( GRM.FormatInputName ( GRM_G.tempAddBanClass ) ) then
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanNameSelectionEditBox:ClearFocus();
                end
            end
        end);
        button:Show();
    end
    -- Hide unused buttons...
    for i = numButtons + 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollChildFrame.AllButtons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollChildFrame.AllButtons[i][1]:Hide();
    end
    if numButtons == 0 then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollFrame.GRM_BanFrameHelperText:SetText ( GRM.L ( "No Matches Found. Add a Custom New Player or Match" ) );
    else
        GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollFrame.GRM_BanFrameHelperText:SetText ( GRM.L ( "Former and Current Members" ) );
    end
    -- Reset the highlight...

    -- Configure Slider
    GRM.ConfigureSlider ( GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollFrame , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollChildFrame , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanScrollFrame.GRM_AddBanScrollFrameSlider , scrollWidth , scrollHeight , 98 , true )
end

-- Method:          GRM.ResetAutoCompleteHighlights()
-- What it Does:    Resets all highlights and disables except for the one selected
-- Purpose:         Quality of life visual controls...
GRM.ResetAutoCompleteHighlights = function ()
    for i = 1 , #GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons do
        if i ~= GRM_G.currentHighlightIndex then
            GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame.GRM_AddAltScrollChildFrame.AllButtons[i][1]:UnlockHighlight();
        end
    end
end

-- Method:          GRM.ConfigureSlider ( scrollFrameObject , frameObject , sliderFrameObject , int , int , int )
-- What it Does:    Configures any scrollframes variables to a consistent setting for the addon given the inputs.
-- Purpose:         Keep down on code bloat for each scrollframe.
GRM.ConfigureSlider = function ( scrollFrame , childFrame , slider , scrollWidth , scrollHeight , staticFrameSize , hideSliderIfNotNeeded )
    --Set Slider Parameters ( has to be done after the above details are placed )
    childFrame:SetSize ( scrollWidth , scrollHeight );
    local scrollMax = ( scrollHeight - staticFrameSize );
    if scrollMax < 0 then
        scrollMax = 0;
    end

    -- Mousewheel Scrolling Logic
    slider:SetMinMaxValues ( 0 , scrollMax );
    scrollFrame:EnableMouseWheel( true );
    scrollFrame:SetScript( "OnMouseWheel" , function( _ , delta )
        local current = slider:GetValue();

        if IsShiftKeyDown() and delta > 0 then
            slider:SetValue ( 0 );
        elseif IsShiftKeyDown() and delta < 0 then
            slider:SetValue ( scrollMax );
        elseif delta < 0 and current < scrollMax then
            if IsControlKeyDown() then
                slider:SetValue ( current + 60 );
            else
                slider:SetValue ( current + 20 );
            end
        elseif delta > 0 and current > 1 then
            if IsControlKeyDown() then
                slider:SetValue ( current - 60 );
            else
                slider:SetValue ( current - 20 );
            end
        end
    end);

    if hideSliderIfNotNeeded and scrollMax == 0 then
        slider:Hide();
    else
        slider:Show();
    end
end


-- Method:          GRM.GetBackupEntries ( int )
-- What it Does:    Gets all of the guild information for all guilds' saved data.
-- Purpose:         Hybrid scrollframe load of the backup features.
GRM.GetBackupEntries = function ( faction )
    local result = {};
    local tempGuildCreationDate = "";
    local numGuildies = 0;
    local autoDate = "";
    local manualDate = "";

    for guildName in pairs ( GRM_GuildDataBackup_Save[faction] ) do
        tempGuildCreationDate = GRM_GuildDataBackup_Save[faction][guildName][1];
        autoDate = "";
        manualDate = "";
        numGuildies = GRM.GetNumGuildiesInGuild ( guildName , faction );

        if #GRM_GuildDataBackup_Save[faction][guildName].Auto > 0 then
            autoDate = GRM.FormatTimeStamp ( string.gsub ( GRM_GuildDataBackup_Save[faction][guildName]["Auto"].date , "AUTO_" , "" ) , true );
        end

        if #GRM_GuildDataBackup_Save[faction][guildName].Manual > 0 then
            manualDate = GRM.FormatTimeStamp ( GRM_GuildDataBackup_Save[faction][guildName]["Manual"].date  , true );
        end


        -- Set the current guild you are in as first entries.
        if faction == GRM_G.F and guildName == GRM_G.guildName then
            table.insert ( result , 1 , { 1 , guildName , tempGuildCreationDate , numGuildies } );
            table.insert ( result , 2 , { 2 , GRM.L ( "Backup {num}:" , nil , nil , 1 ) , manualDate } );
            table.insert ( result , 3 , { 3 , GRM.L ( "Auto {num}:" , nil , nil , 1 ) , autoDate } );
        else
            table.insert ( result , { 1 , guildName , tempGuildCreationDate , numGuildies } );
            table.insert ( result , { 2 , GRM.L ( "Backup {num}:" , nil , nil , 1 ) , manualDate } );
            table.insert ( result , { 3 , GRM.L ( "Auto {num}:" , nil , nil , 1 ) , autoDate } );
        end

    end

    return result;
end

-- Method:          GRM.BackupHybridShiftDown()
-- What it Does:    Shifts the values down by 1 when using the mousewheel
-- Purpose:         Hybrid Scroll frame capabilities on the backup window.
GRM.BackupHybridShiftDown = function()
    local buttons = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons;
    local MouseOverButton = 0;

    -- Shift them down...
    for i = 1 , #buttons - 1 do

        -- Header Line
        if buttons[i+1][2]:IsVisible() then
            -- Values shift
            buttons[i][2]:SetText( buttons[i+1][2]:GetText() );
            buttons[i][2]:SetTextColor ( buttons[i+1][2]:GetTextColor ( buttons[i+1][2]:GetText() ) );
            buttons[i][5]:SetText( buttons[i+1][5]:GetText() );
            buttons[i][6]:SetText( buttons[i+1][6]:GetText() );

            -- Appearance
            buttons[i][2]:Show();
            buttons[i][3]:Hide();
            buttons[i][4]:Hide();
            buttons[i][5]:Show();
            buttons[i][6]:Show();
            buttons[i][7]:Hide();
            buttons[i][8]:Hide();

        else
            -- Values Shift
            buttons[i][3]:SetText( buttons[i+1][3]:GetText() );
            buttons[i][4]:SetText( buttons[i+1][4]:GetText() );
            
            -- Appearance
            buttons[i][2]:Hide();
            buttons[i][3]:Show();
            buttons[i][4]:Show();
            buttons[i][5]:Hide();
            buttons[i][6]:Hide();
            if buttons[i+1][7]:IsVisible() then
                buttons[i][7]:SetText( buttons[i+1][7]:GetText() );
                buttons[i][7]:Show();
            else
                buttons[i][7]:Hide();
            end
            if buttons[i+1][8]:IsVisible() then
                buttons[i][8]:Show();
            else
                buttons[i][8]:Hide();
            end
        end

        if MouseOverButton == 0 and buttons[i][1]:IsMouseOver() then
            MouseOverButton = i;
        end
    end
    GRM.BackupSetLastValue();
    if MouseOverButton > 0 then
        GRM.UpdateBackupTooltip( MouseOverButton );
    end
end

-- Method:          GRM.BackupHybridShiftUp()
-- What it Does:    Shifts the values up by 1 when using the mousewheel
-- Purpose:         Hybrid Scroll frame capabilities on the backup window.
GRM.BackupHybridShiftUp = function()
    local buttons = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons;
    local MouseOverButton = 0;

    -- Shift them down...
    for i = #buttons , 2 , -1 do

        -- Header Line
        if buttons[i-1][2]:IsVisible() then
            -- Values shift
            buttons[i][2]:SetText( buttons[i-1][2]:GetText() );
            buttons[i][2]:SetTextColor ( buttons[i-1][2]:GetTextColor ( buttons[i-1][2]:GetText() ) );
            buttons[i][5]:SetText( buttons[i-1][5]:GetText() );
            buttons[i][6]:SetText( buttons[i-1][6]:GetText() );

            -- Appearance
            buttons[i][2]:Show();
            buttons[i][3]:Hide();
            buttons[i][4]:Hide();
            buttons[i][5]:Show();
            buttons[i][6]:Show();
            buttons[i][7]:Hide();
            buttons[i][8]:Hide();

        else
            -- Values Shift
            buttons[i][3]:SetText( buttons[i-1][3]:GetText() );
            buttons[i][4]:SetText( buttons[i-1][4]:GetText() );
            
            -- Appearance
            buttons[i][2]:Hide();
            buttons[i][3]:Show();
            buttons[i][4]:Show();
            buttons[i][5]:Hide();
            buttons[i][6]:Hide();
            if buttons[i-1][7]:IsVisible() then
                buttons[i][7]:SetText( buttons[i-1][7]:GetText() );
                buttons[i][7]:Show();
            else
                buttons[i][7]:Hide();
            end
            if buttons[i-1][8]:IsVisible() then
                buttons[i][8]:Show();
            else
                buttons[i][8]:Hide();
            end
        end

        if MouseOverButton == 0 and buttons[i][1]:IsMouseOver() then
            MouseOverButton = i;
        end
    end
    GRM.BackupSetFirstValue();
    if MouseOverButton > 0 then
        GRM.UpdateBackupTooltip( MouseOverButton );
    end
end

-- Method:          GRM.BackupSetLastValue()
-- What it Does:    Sets the last value of the hybridscrollframe backups at position 12
-- Purpose:         Clean scrolling
GRM.BackupSetLastValue = function()
    GRM.SetBackupValues ( #GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.Offset );
end

-- Method:          GRM.BackupSetFirstValue()
-- What it Does:    Sets the first value of the hybridscrollframe backups at position 1
-- Purpose:         Clean scrolling
GRM.BackupSetFirstValue = function()
    GRM.SetBackupValues ( 1 , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.Offset - #GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons + 1 );
end

-- Method:          GRM.UpdateBackupTooltip ( int )
-- What it Does:    Builds the tooltip for the backup window
-- Purpose:         Quality of life feature
GRM.UpdateBackupTooltip = function( ind )
    if not GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_BackupPurgeGuildOption:IsVisible() and GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][2]:IsVisible() and GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][1]:IsMouseOver() then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_GuildNameTooltip:SetOwner( GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][1] , "ANCHOR_CURSOR" );
        if GRM_G.selectedFID == "H" then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_GuildNameTooltip:AddLine ( GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][2]:GetText() , 0.61 , 0.14 , 0.137 );
        elseif GRM_G.selectedFID == "A" then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_GuildNameTooltip:AddLine ( GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][2]:GetText() , 0.078 , 0.34 , 0.73 );
        end
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_GuildNameTooltip:AddLine ( GRM.L ( "Right-Click for options to remove this guild from the addon database completely" ) , 1 , 0.84 , 0 , true );

        if GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][5]:GetText() == GRM.L ( "Unknown" ) and GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][6]:GetText() == GRM.L ( "Unknown" ) then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_GuildNameTooltip:AddLine ( GRM.L ( "No player data found, recommend full removal." ) , 1 , 0 , 0 , true );
        end

        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_GuildNameTooltip:Show();
    elseif not GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][2]:IsVisible() or not GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][1]:IsMouseOver() then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_GuildNameTooltip:Hide();
    end
end

-- Method:          GRM.SetBackupValues ( int , int )
-- What it Does:    Builds the values of the hybrid scroll frame given button
-- Purpose:         Establish the values for the backup window
GRM.SetBackupValues = function ( ind , ind2 )
    local line = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind];

    -- Main top Line
    if GRM_G.BackupEntries[ind2][1] == 1 then
        line[2]:SetText ( GRM_G.BackupEntries[ind2][2] );
        if GRM_G.selectedFID == "H" then
            line[2]:SetTextColor ( 0.61 , 0.14 , 0.137 );
        else
            line[2]:SetTextColor ( 0.078 , 0.34 , 0.73 );
        end

        if GRM_G.BackupEntries[ind2][3] == "" then
            line[5]:SetText ( GRM.L ( "Unknown" ) );
        else
            line[5]:SetText ( GRM_G.BackupEntries[ind2][3] );
        end
        if GRM_G.BackupEntries[ind2][4] == 0 then
            line[6]:SetText ( GRM.L ( "Unknown" ) );
        else
            line[6]:SetText ( GRM_G.BackupEntries[ind2][4] );
        end

        line[2]:Show();
        line[3]:Hide();
        line[4]:Hide();
        line[5]:Show();
        line[6]:Show();
        line[7]:Hide();
        line[8]:Hide();

    -- Manual Backup Line
    elseif GRM_G.BackupEntries[ind2][1] > 1 then

        line[3]:SetText ( GRM_G.BackupEntries[ind2][2] )

        if GRM_G.BackupEntries[ind2][3] == "" then
            line[4]:SetText ( " < " .. GRM.L ( "None" ) .. " > " );
            if GRM_G.BackupEntries[ind2][1] == 2 then
                line[7]:SetText ( GRM.L ( "Set Backup" ) );
                line[7]:Show();
            else
                line[7]:Hide();
            end
            line[8]:Hide();
        else
            line[4]:SetText ( GRM_G.BackupEntries[ind2][3] );
            line[7]:SetText ( GRM.L ( "Restore" ) );
            line[7]:Show();
            line[8]:Show();
        end

        line[2]:Hide();
        line[3]:Show();
        line[4]:Show();
        line[5]:Hide();
        line[6]:Hide();
        
    end

    -- Update the tooltip if underlying data changes
    if GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_GuildNameTooltip:IsVisible() and GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][1]:IsMouseOver() then 
        GRM.UpdateBackupTooltip ( ind );
    end

    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_BackupPurgeGuildOption:Hide();
end

-- Method:          GRM.BuildBackupScrollFrame ( bool , bool )
-- What it Does:    Builds the backup hybdrid scrollframe and initializes the buttons properly
-- Purpose:         Backup optional functionality
GRM.BuildBackupScrollFrame = function ( showAll , fullRefresh )
    local hybridScrollFrameButtonCount = 12;
    local buttonHeight = 27.417;
    local scrollHeight = 0;
    local buttonWidth = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollFrame:GetWidth() - 5;

    if showAll and fullRefresh then
        GRM_G.BackupEntries = GRM.GetBackupEntries ( GRM_G.selectedFID );
        -- Establish the memory use...
        UpdateAddOnMemoryUsage();

        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_MemoryUsageText:SetText ( 
            GRM.L ( "Memory Usage: {num} MB" , nil , nil , GRM.Round ( GetAddOnMemoryUsage ( GRM_G.addonName ) / 1000 , 2 ) )
        );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_AutoBackupTimeOverlayNoteText:SetText ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].autoIntervalDays );
        -- AutoBackupSettings
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].autoIntervalDays > 1 then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.DaysOnAutoBackupText2:SetText ( GRM.L ( "Days" ) );
        else
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.DaysOnAutoBackupText2:SetText ( GRM.L ( "Day" ) );
        end

        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].allowAutoBackups then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_AutoBackupCheckBox:SetChecked( true );
        else
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_AutoBackupCheckBox:SetChecked( false );
        end
        -- Hide any popups
    end

    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_BackupPurgeGuildOption:Hide();

    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons or {};
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.Offset = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.Offset or ( hybridScrollFrameButtonCount );

    if GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.Offset < hybridScrollFrameButtonCount then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.Offset = hybridScrollFrameButtonCount;
    elseif GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.Offset > hybridScrollFrameButtonCount and GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.Offset > #GRM_G.BackupEntries then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.Offset = #GRM_G.BackupEntries;
    end

    for i = 1 , #GRM_G.BackupEntries do
        -- Build HybridScrollFrame Buttons
        if i <= hybridScrollFrameButtonCount then
            if not GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[i] then

                local button = CreateFrame ( "Button" , "GRMBackupCoreButton_" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[i] = {
                    button ,
                    button:CreateFontString ( "GuildBString1_" .. i , "OVERLAY" , "GameFontWhiteTiny" ) ,
                    button:CreateFontString ( "GuildBString2_" .. i , "OVERLAY" , "GameFontWhiteTiny" ) ,
                    button:CreateFontString ( "GuildBString3_" .. i , "OVERLAY" , "GameFontWhiteTiny" ) ,
                    button:CreateFontString ( "GuildBString4_" .. i , "OVERLAY" , "GameFontWhiteTiny" ) ,
                    button:CreateFontString ( "GuildBString5_" .. i , "OVERLAY" , "GameFontWhiteTiny" ) ,
                    CreateFrame ( "Button" , "GuildBackup1_" .. i , button , "UIPanelButtonTemplate" ) ,
                    CreateFrame ( "Button" , "GuildBackup2_" .. i , button , "UIPanelButtonTemplate" ) ,
                };

                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[i][8]:SetText ( GRM.L ( "Remove" ) );   -- all of these will be the same

                button = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[i][1];
                if i == 1 then
                    button:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame , "TOP" , 12 , 0 );
                else 
                    button:SetPoint ( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[i-1][1] , "BOTTOMLEFT" , 0 , 0 );
                end

                -- button:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
                button:SetSize ( buttonWidth , buttonHeight );
                GRM.BuildBackupHybridButtons ( i , false , hybridScrollFrameButtonCount );
                
            end
        end

        if i >= ( GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.Offset - hybridScrollFrameButtonCount + 1 ) and i <= GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.Offset then
            GRM.SetBackupValues ( i - ( GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.Offset - hybridScrollFrameButtonCount ) , i );
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[i - ( GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.Offset - hybridScrollFrameButtonCount )][1]:Show();
        end
        
        -- Slider Height is controlled by tallying how many of these are necessary
        scrollHeight = scrollHeight + buttonHeight;
    end
    -- Hide unused buttons...
    for i = #GRM_G.BackupEntries + 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[i][1]:Hide();
    end

    GRM.SetHybridScrollFrameSliderParameters ( 
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollFrame , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollFrameSlider , 
        buttonWidth , buttonHeight , scrollHeight , #GRM_G.BackupEntries , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons , 
        GRM.BackupHybridShiftDown , GRM.BackupHybridShiftUp , hybridScrollFrameButtonCount
    );
end
 
-- Method:          GRM.BuildBackupHybridButtons ( int , boolean , int )
-- What it Does:    Initiates the buttons and logic for the Backup buttons
-- Purpose:         Compartmentalize the code for easier call back to.
GRM.BuildBackupHybridButtons = function ( ind , isResizeAction , numButtons )

    local coreButton = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][1];
    local buttonText1 = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][2];
    local buttonText2 = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][3];
    local buttonText3 = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][4];
    local buttonText4 = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][5];
    local buttonText5 = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][6];
    local button1 = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][7];
    local button2 = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[ind][8];

    -- Guild Name
    buttonText1:SetPoint ( "LEFT" , coreButton , "LEFT" , 5 , 0 );
    buttonText1:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 16 );
    buttonText1:SetJustifyH ( "LEFT" );
    buttonText1:SetWidth ( 278 )
    buttonText1:SetWordWrap ( false );

    -- Backup Header manual or auto
    buttonText2:SetPoint ( "LEFT" , coreButton , "LEFT" , 5 , 0 );
    buttonText2:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 14 );
    buttonText2:SetWidth ( 100 );
    buttonText2:SetJustifyH ( "LEFT" );
    buttonText2:SetWordWrap ( false );

    -- Backup date of save
    buttonText3:SetPoint ( "LEFT" , buttonText2 , "RIGHT" , 10 , 0 );
    buttonText3:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 14 );
    buttonText3:SetWidth ( 160 );
    buttonText3:SetJustifyH ( "CENTER" );
    buttonText3:SetWordWrap ( false );

    -- Date
    buttonText4:SetPoint ( "LEFT" , buttonText1 , "RIGHT" , 0 , 0 );
    buttonText4:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 16 );
    buttonText4:SetWidth ( 110 );
    buttonText4:SetJustifyH ( "CENTER" );
    buttonText4:SetWordWrap ( false );

    -- Num Guildies
    buttonText5:SetPoint ( "LEFT" , buttonText4 , "RIGHT" , 50 , 0 );
    buttonText5:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 16 );
    buttonText5:SetWidth ( 110 );
    buttonText5:SetJustifyH ( "CENTER" );
    buttonText5:SetWordWrap ( false );
    
    if not isResizeAction then

        button1:SetSize ( 110 , 21 );
        button1:SetPoint ( "CENTER" , buttonText4 );

        button2:SetSize ( 110 , 21 );
        button2:SetPoint ( "CENTER" , buttonText5 );
        button2:SetText ( GRM.L ( "Remove" ) );         -- This will always be remove.

        coreButton:EnableMouse ( true );
        coreButton:RegisterForDrag ( "LeftButton" );
        button1:EnableMouse ( true );
        button1:RegisterForDrag ( "LeftButton" );
        button2:EnableMouse ( true );
        button2:RegisterForDrag ( "LeftButton" );

        -- Setup draggable conditions
        coreButton:SetScript ( "OnDragStart" , function()
            GRM_UI.GRM_RosterChangeLogFrame:StartMoving();
        end);
        coreButton:SetScript ( "OnDragStop" , function()
            GRM_UI.GRM_RosterChangeLogFrame:StopMovingOrSizing();
            GRM_UI.SaveCorePosition();
        end);

        -- Restore or Set Backup
        button1:SetScript ( "OnClick" , function ( self , buttonClicked )
            if buttonClicked == "LeftButton" then
                local index = tonumber ( string.sub ( self:GetName() , string.find ( self:GetName() , "_" ) + 1 ) );    -- Get index of the AllBackupButtons 
                local backupEntryFinal = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.Offset - numButtons + index;

                if backupEntryFinal > 3 then
                    if backupEntryFinal % 3 == 0 then
                        backupEntryFinal = backupEntryFinal - 2;
                    elseif backupEntryFinal % 3 == 2 then
                        backupEntryFinal = backupEntryFinal - 1;
                    end
                else
                    backupEntryFinal = 1;
                end
                
                local guildName = GRM_G.BackupEntries[ backupEntryFinal ][2];

                -- For retore
                if self:GetText() == GRM.L ( "Restore" ) then
                    GRM_UI.GRM_RosterConfirmFrameText:SetText( GRM.L ( "Really restore {name} Backup Point?" , guildName ) );
                    GRM_UI.GRM_RosterConfirmYesButtonText:SetText ( GRM.L ( "Yes!" ) );

                    -- Popup window logic control
                    GRM_UI.GRM_RosterConfirmYesButton:SetScript ( "OnClick" , function( _ , button )
                        if button == "LeftButton" then
                            GRM.LoadGuildBackup ( string.gsub ( guildName , "\"" , "" ) , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[index][5]:GetText() , GRM_G.selectedFID , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[index][4]:GetText() );
                            GRM.BuildBackupScrollFrame ( true , true );
                            GRM_UI.GRM_RosterConfirmFrame:Hide();
                        end
                    end);
                    GRM_UI.GRM_RosterConfirmFrame:Show();

                -- For manually setting the backup
                elseif self:GetText() == GRM.L ( "Set Backup" ) then

                    GRM.AddGuildBackup ( string.gsub ( guildName , "\"" , "" ) , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[index - 1][5]:GetText() , GRM_G.selectedFID );
                    GRM.BuildBackupScrollFrame ( true , true );
                end
            end
        end);

        -- Remove a Backup
        button2:SetScript ( "OnClick" , function ( self , buttonClicked )
            if buttonClicked == "LeftButton" then
                local index = tonumber ( string.sub ( self:GetName() , string.find ( self:GetName() , "_" ) + 1 ) );    -- Get index of the AllBackupButtons
                local backupEntryFinal = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.Offset - numButtons + index;

                if backupEntryFinal > 3 then
                    if backupEntryFinal % 3 == 0 then
                        backupEntryFinal = backupEntryFinal - 2;
                    elseif backupEntryFinal % 3 == 2 then
                        backupEntryFinal = backupEntryFinal - 1;
                    end
                else
                    backupEntryFinal = 1;
                end

                local guildName = GRM_G.BackupEntries[ backupEntryFinal ][2];

                GRM_UI.GRM_RosterConfirmFrameText:SetText( GRM.L ( "Really remove {name} Backup Point?" , guildName ) );
                GRM_UI.GRM_RosterConfirmYesButtonText:SetText ( GRM.L ( "Yes!" ) );
                GRM_UI.GRM_RosterConfirmYesButton:SetScript ( "OnClick" , function( _ , button )
                    if button == "LeftButton" then
                        GRM.RemoveGuildBackup ( string.gsub ( guildName , "\"" , "" ) , GRM_G.selectedFID , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[index][4]:GetText() , true );
                        GRM.BuildBackupScrollFrame ( true , true );
                        GRM_UI.GRM_RosterConfirmFrame:Hide();
                    end
                end);
                GRM_UI.GRM_RosterConfirmFrame:Show();
            end
        end);

        coreButton:SetScript ( "OnMouseDown" , function ( self , button )
            if button == "RightButton" then
                local index = tonumber ( string.sub ( self:GetName() , string.find ( self:GetName() , "_" ) + 1 ) );

                if GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[index][2]:IsVisible() and self:IsMouseOver() then
                    GRM_G.BackupFrameSelectDetails = { string.gsub ( GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[index][2]:GetText() , "\"" , "" ) , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_CoreBackupScrollChildFrame.AllBackupButtons[index][5]:GetText() };
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_BackupPurgeGuildOption:ClearAllPoints();
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_BackupPurgeGuildOption:SetPoint( "TOPRIGHT" , self , "TOPLEFT" , -12 , -5 );
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_BackupPurgeGuildOption:Show();
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_GuildNameTooltip:Hide();
                end
            end
        end);

        coreButton:SetScript ( "OnEnter" , function ( self )
            GRM.UpdateBackupTooltip ( tonumber ( string.sub ( self:GetName() , string.find ( self:GetName() , "_" ) + 1 ) ) );
        end);
        coreButton:SetScript ( "OnLeave" , function()
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame.GRM_GuildNameTooltip:Hide();
        end);
    end
end

-- Method:          GRM.GetAuditEntries ()
-- What it Does:    Determines which grouping to import for sorted Audit list
-- Purpose:         Audit UX feature controls for sorting.
GRM.GetAuditEntries = function ()
    local typeOfSort = GRM_G.AuditSortType;
    local result = {};
    if typeOfSort == nil or typeOfSort == 1 then
        result =  GRM.GetAllGuildiesInOrder ( true , true );
    elseif typeOfSort == 2 then
        result =  GRM.GetAllGuildiesInOrder ( true , false );
    elseif typeOfSort == 3 then
        result = GRM.GetAllGuildiesInJoinDateOrder ( true , true );
    elseif typeOfSort == 4 then
        result = GRM.GetAllGuildiesInJoinDateOrder ( true , false );
    elseif typeOfSort == 5 then
        result = GRM.GetAllGuildiesInPromoDateOrder ( true , true );
    elseif typeOfSort == 6 then
        result = GRM.GetAllGuildiesInPromoDateOrder ( true , false );
    elseif typeOfSort == 7 then
        result = GRM.GetAllMainsAndAltsInOrder ( true );
    elseif typeOfSort == 8 then
        result = GRM.GetAllMainsAndAltsInOrder ( false );
    elseif typeOfSort == 9 then
        result = GRM.GetAllGuildiesByBirthdayDateOrder ( true , true );
    elseif typeOfSort == 10 then
        result = GRM.GetAllGuildiesByBirthdayDateOrder ( true , false );
    end
    return result;
end

-- Method:          GRM.SetAuditValues ( int , int )
-- What it Does:    Builds the values of the given line in the audit window
-- Purpose:         Quality of life feature.
GRM.SetAuditValues = function ( ind , ind2 )
    local line = GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[ind];
    local ok = { 0 , 0.77 , 0.063 };
    local notOk = { 0.64 , 0.102 , 0.102 };
    local unknown = { 1.0 , 0.647 , 0 };
    local color = {};

    local getColor = function ( text )
        local result = {};
        if text == GRM.L ( "No Date Set" ) or text == GRM.L ( "Main or Alt?" ) then
            result = notOk;
        elseif text == GRM.L ( "Unknown" ) then
            result = unknown;
        else
            result = ok;
        end
        return result;
    end

    -- Player Name
    line[2]:SetText ( GRM_G.AuditEntries[ind2][1] );
    line[2]:SetTextColor ( GRM_G.AuditEntries[ind2][5][1] , GRM_G.AuditEntries[ind2][5][2] , GRM_G.AuditEntries[ind2][5][3] , 1 );

    -- Join Date
    line[3]:SetText ( GRM_G.AuditEntries[ind2][2] );
    color = getColor ( GRM_G.AuditEntries[ind2][2] );
    line[3]:SetTextColor ( color[1] , color[2] , color[3] );

    -- Promotion Date
    line[4]:SetText ( GRM_G.AuditEntries[ind2][3] );
    color = getColor ( GRM_G.AuditEntries[ind2][3] );
    line[4]:SetTextColor ( color[1] , color[2] , color[3] );

    -- Birthdate
    line[5]:SetText ( GRM_G.AuditEntries[ind2][6] );
    color = getColor ( GRM_G.AuditEntries[ind2][6] );
    line[5]:SetTextColor ( color[1] , color[2] , color[3] );

    -- Main/Alt Status
    line[6]:SetText ( GRM_G.AuditEntries[ind2][4] );
    color = getColor ( GRM_G.AuditEntries[ind2][4] );
    line[6]:SetTextColor ( color[1] , color[2] , color[3] );

    -- Update the tooltip if underlying data changes
    if GameTooltip:IsVisible() and GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[ind][1]:IsMouseOver() then 
        GRM.UpdateAuditTooltip ( ind );
    end
end

-- Method:          GRM.AuditHybridShiftDown()
-- What it Does:    Shifts all the values DOWN one line to give the illusion of scrolling when in reality this is just a hybrid scrollframe
-- Purpose:         Clean scrolling
GRM.AuditHybridShiftDown = function()
    if #GRM_G.AuditEntries > 16 then
        local buttons = GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons;
        local MouseOverButton = 0;

        -- Shift them down...
        for i = 1 , #buttons - 1 do

            -- Header Line
            buttons[i][2]:SetText( buttons[i+1][2]:GetText() );
            buttons[i][2]:SetTextColor ( buttons[i+1][2]:GetTextColor ( buttons[i+1][2]:GetText() ) );
            buttons[i][3]:SetText( buttons[i+1][3]:GetText() );
            buttons[i][3]:SetTextColor ( buttons[i+1][3]:GetTextColor ( buttons[i+1][3]:GetText() ) );
            buttons[i][4]:SetText( buttons[i+1][4]:GetText() );
            buttons[i][4]:SetTextColor ( buttons[i+1][4]:GetTextColor ( buttons[i+1][4]:GetText() ) );
            buttons[i][5]:SetText( buttons[i+1][5]:GetText() );
            buttons[i][5]:SetTextColor ( buttons[i+1][5]:GetTextColor ( buttons[i+1][5]:GetText() ) );
            buttons[i][6]:SetText( buttons[i+1][6]:GetText() );
            buttons[i][6]:SetTextColor ( buttons[i+1][6]:GetTextColor ( buttons[i+1][6]:GetText() ) );

            if MouseOverButton == 0 and buttons[i][1]:IsMouseOver() then
                MouseOverButton = i;
            end
        end

        GRM.AuditSetLastValue();
        if MouseOverButton > 0 then
            GRM.UpdateAuditTooltip ( MouseOverButton );
        end
    end
end

-- Method:          GRM.AuditHybridShiftUp()
-- What it Does:    Shifts all the values up one line to give the illusion of scrolling when in reality this is just a hybrid scrollframe
-- Purpose:         Clean scrolling
GRM.AuditHybridShiftUp = function()
    if #GRM_G.AuditEntries > 16 then
        local buttons = GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons;
        local MouseOverButton = 0;

        for i = #buttons , 2 , -1 do

            buttons[i][2]:SetText( buttons[i-1][2]:GetText() );
            buttons[i][2]:SetTextColor ( buttons[i-1][2]:GetTextColor ( buttons[i-1][2]:GetText() ) );
            buttons[i][3]:SetText( buttons[i-1][3]:GetText() );
            buttons[i][3]:SetTextColor ( buttons[i-1][3]:GetTextColor ( buttons[i-1][3]:GetText() ) );
            buttons[i][4]:SetText( buttons[i-1][4]:GetText() );
            buttons[i][4]:SetTextColor ( buttons[i-1][4]:GetTextColor ( buttons[i-1][4]:GetText() ) );
            buttons[i][5]:SetText( buttons[i-1][5]:GetText() );
            buttons[i][5]:SetTextColor ( buttons[i-1][5]:GetTextColor ( buttons[i-1][5]:GetText() ) );
            buttons[i][6]:SetText( buttons[i-1][6]:GetText() );
            buttons[i][6]:SetTextColor ( buttons[i-1][6]:GetTextColor ( buttons[i-1][6]:GetText() ) );

            if MouseOverButton == 0 and buttons[i][1]:IsMouseOver() then
                MouseOverButton = i;
            end
        end

        GRM.AuditSetFirstValue();
        if MouseOverButton > 0 then
            GRM.UpdateAuditTooltip ( MouseOverButton );
        end
    end
end

-- Method:          GRM.AuditSetLastValue()
-- What it Does:    Sets the last value of the hybridscrollframe backups at position 16
-- Purpose:         Clean scrolling
GRM.AuditSetLastValue = function()
    GRM.SetAuditValues ( #GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons , GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.Offset );
end

-- Method:          GRM.AuditSetFirstValue()
-- What it Does:    Sets the first value of the hybridscrollframe backups at position 1
-- Purpose:         Clean scrolling
GRM.AuditSetFirstValue = function()
    GRM.SetAuditValues ( 1 , GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.Offset - #GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons + 1 );
end

-- Method:          GRM.UpdateAuditTooltip()
-- What it Does:    Sets the tooltip for the audit window
-- Purpose:         Make it clear the QoL controls.
GRM.UpdateAuditTooltip = function ( ind )
    GRM_UI.SetTooltipScale();
    GameTooltip:SetOwner ( GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[ind][1] , "ANCHOR_CURSOR" );
    GameTooltip:AddLine ( GRM.GetClassifiedName ( GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[ind][2]:GetText() , false ) );
    GameTooltip:AddLine ( GRM.L ( "|CFFE6CC7FCtrl-Click|r to open Player Window" ) );
    GameTooltip:AddLine( GRM.L ( "|CFFE6CC7FCtrl-Shift-Click|r to Search the Log for Player" ) );
    GameTooltip:AddLine ( "|CFFE6CC7FRight-Click|r for Additional Options" );

    -- Check for the tag
    if string.find ( GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[ind][3]:GetText() , "!!" , 1 , true ) ~= nil or
        string.find ( GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[ind][4]:GetText() , "!!" , 1 , true ) ~= nil then
        GameTooltip:AddLine ( " " );
        GameTooltip:AddLine ( GRM.L ( "The {name} tag indicates a date must be verified to sync" , "|CFFFF0000!!|r" ) );
        GameTooltip:AddLine ( GRM.L ( "To confirm or edit the date, open the player window, right click the date, edit, and submit" ) );
    end
    GameTooltip:Show();
end

-- Method:          GRM.TriggerRefreshAuditReset()
-- What it Does:    Refreshes the audit frames after hiding the tooltip
-- Purpose:         Prevent code bloat for something with repeated use.
GRM.TriggerRefreshAuditReset = function()
    GRM.RestoreTooltip()
    GRM.RefreshAuditFrames ( true , true );
end

-- Method:          GRM.RefreshAuditFrames( bool , bool )
-- What it Does:    Updates the audit frames when called
-- Purpose:         Audit frames are useful so the leader or player can do an easy visual check of the entire guild on what is needed.
GRM.RefreshAuditFrames = function ( showAll , fullRefresh )
    local hybridScrollFrameButtonCount = 14;
    local buttonHeight = 22.1875;
    local scrollHeight = 0;
    local buttonWidth = GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollFrame:GetWidth() - 5;

    if showAll and fullRefresh then
        GRM_G.AuditEntries = GRM.GetAuditEntries();
        GRM_G.AuditEntryTotals = GRM.GetIncompleteGuildDataCounts();
        -- numBdayUnknown , numBdayNoDate

        -- Only show the "complete" guildies in the list.
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].onlyShowIncomplete then
            local isFound;
            for i = #GRM_G.AuditEntries , 1 , -1 do
                isFound = false;
                for j = 1 , #GRM_G.AuditEntryTotals[6] do
                    if GRM_G.AuditEntryTotals[6][j] == GRM_G.AuditEntries[i][1] then
                        isFound = true;
                        break;
                    end
                end
                if not isFound then
                    table.remove ( GRM_G.AuditEntries , i );
                end
            end
        end
    end


    GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons = GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons or {};
    GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.Offset = GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.Offset or ( hybridScrollFrameButtonCount );

    if GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.Offset < hybridScrollFrameButtonCount then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.Offset = hybridScrollFrameButtonCount;
    elseif GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.Offset > hybridScrollFrameButtonCount and GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.Offset > #GRM_G.AuditEntries then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.Offset = #GRM_G.AuditEntries;
    end

    for i = 1 , #GRM_G.AuditEntries do
        -- Build HybridScrollFrame Buttons
        if i <= hybridScrollFrameButtonCount then
            if not GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[i] then

                local button = CreateFrame ( "Button" , "GRMAuditCoreButton_" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[i] = {
                    button ,
                    button:CreateFontString ( "AudtString1_" .. i , "OVERLAY" , "GameFontWhiteTiny" ) ,
                    button:CreateFontString ( "AudtString2_" .. i , "OVERLAY" , "GameFontWhiteTiny" ) ,
                    button:CreateFontString ( "AudtString3_" .. i , "OVERLAY" , "GameFontWhiteTiny" ) ,
                    button:CreateFontString ( "AudtString4_" .. i , "OVERLAY" , "GameFontWhiteTiny" ) ,
                    button:CreateFontString ( "AudtString5_" .. i , "OVERLAY" , "GameFontWhiteTiny" ) ,
                };

                button = GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[i][1];
                if i == 1 then
                    button:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame , "TOP" , 0 , 0 );
                else 
                    button:SetPoint ( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[i-1][1] , "BOTTOMLEFT" , 0 , 0 );
                end

                button:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
                button:SetSize ( buttonWidth , buttonHeight );
                GRM.BuildAuditScrollButtons ( i );
                
            end
        end

        if i >= ( GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.Offset - hybridScrollFrameButtonCount + 1 ) and i <= GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.Offset then
            GRM.SetAuditValues ( i - ( GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.Offset - hybridScrollFrameButtonCount ) , i );
            GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[i - ( GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.Offset - hybridScrollFrameButtonCount )][1]:Show();
        end
        
        -- Slider Height is controlled by tallying how many of these are necessary
        scrollHeight = scrollHeight + buttonHeight;
    end

    -- Hide unused buttons...
    for i = #GRM_G.AuditEntries + 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[i][1]:Hide();
    end

    GRM.SetHybridScrollFrameSliderParameters ( 
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame , GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollFrame , GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollFrameSlider, 
        buttonWidth , buttonHeight , scrollHeight , #GRM_G.AuditEntries , GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons , 
        GRM.AuditHybridShiftDown , GRM.AuditHybridShiftUp , hybridScrollFrameButtonCount
    );

    GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditFrameText5:SetText ( GRM.L ( "Total Incomplete: {num} / {custom1}" , nil , nil , GRM_G.AuditEntryTotals[5] , GRM.GetNumGuildies() ) );
    GRM_UI.ScaleFontStringToObjectSize ( true , 190 , GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditFrameText5 , 2 );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditFrameText5:Show();
    GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditFrameText8:SetText ( GRM.L ( "Mains:\n{num}" , nil , nil , GRM.GetNumMains() ) );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditFrameText8:Show();
    GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditFrameText7:SetText ( GRM.L ( "Unique Accounts:\n{num}" , nil , nil , GRM_G.numAccounts ) );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditFrameText7:Show();
    if ( GRM_G.AuditEntryTotals[1] + GRM_G.AuditEntryTotals[2] ) == 0 then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetJoinUnkownButton.GRM_SetJoinUnkownButtonText:SetText ( GRM.L ( "All Complete" ) );
    elseif GRM_G.AuditEntryTotals[2] > 0 then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetJoinUnkownButton.GRM_SetJoinUnkownButtonText:SetText ( GRM.L ( "Set Incomplete to Unknown" ) );
    else
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetJoinUnkownButton.GRM_SetJoinUnkownButtonText:SetText ( GRM.L ( "Clear All Unknown" ) );
    end

    if ( GRM_G.AuditEntryTotals[3] + GRM_G.AuditEntryTotals[4] ) == 0 then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetPromoUnkownButton.GRM_SetPromoUnkownButtonText:SetText ( GRM.L ( "All Complete" ) );
    elseif GRM_G.AuditEntryTotals[4] > 0 then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetPromoUnkownButton.GRM_SetPromoUnkownButtonText:SetText ( GRM.L ( "Set Incomplete to Unknown" ) );
    else
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetPromoUnkownButton.GRM_SetPromoUnkownButtonText:SetText ( GRM.L ( "Clear All Unknown" ) );
    end

    if ( GRM_G.AuditEntryTotals[7] + GRM_G.AuditEntryTotals[8] ) == 0 then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetBdayUnkownButton.GRM_SetBdayUnkownButtonText:SetText ( GRM.L ( "All Complete" ) );
    elseif GRM_G.AuditEntryTotals[8] > 0 then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetBdayUnkownButton.GRM_SetBdayUnkownButtonText:SetText ( GRM.L ( "Set Incomplete to Unknown" ) );
    else
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetBdayUnkownButton.GRM_SetBdayUnkownButtonText:SetText ( GRM.L ( "Clear All Unknown" ) );
    end

    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeBirthdaysInAudit then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditBirthdayToggleButton:SetChecked ( true );
    end

    GRM_UI.RefreshAuditTab ( GRM_UI.GRM_RosterChangeLogFrame.GRM_GuildAuditTab );
end

-- Method:          GRM.UnlockAuditButtonHighlights ( object )
-- What it Does:    Unlocks all button highlights not clicked
-- Purpose:         UX
GRM.UnlockAuditButtonHighlights = function()
    for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[i][1]:UnlockHighlight();
    end
end

-- Method:          GRM.BuildAuditScrollButtons ( int )
-- What it Does:    Initiates the buttons and their values for each line of the audit hybridscrollframe
-- Purpose:         Create a smooth scrolling experience in the audit window
GRM.BuildAuditScrollButtons = function ( ind , isResizeAction )
    local coreButton = GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[ind][1];
    local buttonText1 = GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[ind][2];
    local buttonText2 = GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[ind][3];
    local buttonText3 = GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[ind][4];
    local buttonText4 = GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[ind][5];
    local buttonText5 = GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[ind][6];

    -- 566 width

    -- Name
    buttonText1:SetPoint ( "LEFT" , coreButton , "LEFT" , 5 , 0 );
    buttonText1:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 11 );
    buttonText1:SetJustifyH ( "LEFT" );
    buttonText1:SetWidth ( 142 )
    buttonText1:SetWordWrap ( false );

     -- Join Date
     buttonText2:SetPoint ( "LEFT" , buttonText1 , "RIGHT" , 0 , 0 );
     buttonText2:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 11 );
     buttonText2:SetWidth ( 105 );
     buttonText2:SetJustifyH ( "CENTER" );
     buttonText2:SetWordWrap ( false );
     
      -- Promo Date
    buttonText3:SetPoint ( "LEFT" , buttonText2 , "RIGHT" , 0 , 0 );
    buttonText3:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 11 );
    buttonText3:SetWidth ( 105 );
    buttonText3:SetJustifyH ( "CENTER" );
    buttonText3:SetWordWrap ( false );

    -- Birthday
    buttonText4:SetPoint ( "LEFT" , buttonText3 , "RIGHT" , 0 , 0 );
    buttonText4:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 11 );
    buttonText4:SetWidth ( 105 );
    buttonText4:SetJustifyH ( "CENTER" );
    buttonText4:SetWordWrap ( false );

    -- Main/Alt
    buttonText5:SetPoint ( "LEFT" , buttonText4 , "RIGHT" , -10 , 0 );
    buttonText5:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 11 );
    buttonText5:SetWidth ( 105 );
    buttonText5:SetJustifyH ( "CENTER" );
    buttonText5:SetWordWrap ( false );

    if not isResizeAction then

        coreButton:EnableMouse ( true );
        coreButton:RegisterForDrag ( "LeftButton" );

        -- Setup draggable conditions
        coreButton:SetScript ( "OnDragStart" , function()
            GRM_UI.GRM_RosterChangeLogFrame:StartMoving();
        end);
        coreButton:SetScript ( "OnDragStop" , function()
            GRM_UI.GRM_RosterChangeLogFrame:StopMovingOrSizing();
            GRM_UI.SaveCorePosition();
        end);

        coreButton:SetScript ( "OnMouseDown" , function ( self , button )
            local playerName = GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditScrollChildFrame.AllAuditButtons[ind][2]:GetText();

            if button == "LeftButton" then
                if IsShiftKeyDown() and IsControlKeyDown() then
                    GRM.RestoreTooltip();
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogTab:Click();
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogEditBox:SetText( GRM.SlimName ( playerName ) );
                elseif IsControlKeyDown() then
                    GRM.OpenPlayerWindow( playerName );        
                end

            elseif button == "RightButton" then
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditWindowDropDownFrame.name = playerName;
                GRM.RestoreTooltip();

                local buttonMsg = "";

                if GRM_GuildMemberHistory_Save[ GRM_G.F ][GRM_G.guildName][playerName] ~= nil then
                    if not GRM_GuildMemberHistory_Save[ GRM_G.F ][GRM_G.guildName][playerName].isMain then
                        buttonMsg = GRM.L ( "Set as Main" );
                    else
                        buttonMsg = GRM.L ( "Set as Alt" );
                    end
                end

                GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditWindowDropDownFrame.GRM_AuditWindowDropDownFrameText:SetText ( GRM.GetClassifiedName ( playerName , false ) );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditWindowDropDownFrame.GRM_AuditWindowDropDownAltMainButton.GRM_AuditWindowDropDownAltMainButtonText:SetText ( buttonMsg );
                local size = GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditWindowDropDownFrame.GRM_AuditWindowDropDownFrameText:GetWidth() + 25;

                if size < 110 then
                    size = 110;
                end

                GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditWindowDropDownFrame:SetWidth ( size );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditWindowDropDownFrame:ClearAllPoints();
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditWindowDropDownFrame:SetPoint ( "LEFT" , self , "RIGHT" , -15 , 0 );

                size = GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditWindowDropDownFrame:GetWidth();
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditWindowDropDownFrame.GRM_AuditWindowDropDownAltMainButton:SetWidth( GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditWindowDropDownFrame:GetWidth() - 15 );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditWindowDropDownFrame.GRM_AuditWindowDropDownCancelButton:SetWidth ( size - 5 );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditWindowDropDownFrame:Show();

                GRM.UnlockAuditButtonHighlights();
                self:LockHighlight();
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_AuditWindowDropDownFrame:SetScript ( "OnHide" , function()
                    self:UnlockHighlight();
                end);
                
            end
        end);            

        coreButton:SetScript ( "OnEnter" , function ()
            GRM.UpdateAuditTooltip( ind );
        end);

        coreButton:SetScript ( "OnLeave" , function()
            GRM.RestoreTooltip();
        end);
    end
end

-- Method:          GRM.GetIncompleteGuildDataCounts()
-- What it Does:    Scans over the guild for audit and retrieves the data on what is and is not missing. This is to be used for audit reporting
-- Purpose:         For player info and better control over their guild's data completeness...
GRM.GetIncompleteGuildDataCounts = function()
    local numJoinUnknown = 0;
    local numJoinNoDate = 0;
    local numPromoUnknown = 0;
    local numPromoNoDate = 0;
    local numBdayNoDate , numBdayUnknown = 0 , 0;
    local count2 = 0;                   -- This is the overall count of how many "incomplete"
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    local isComplete = true;
    local listOfIncompleteNames = {};

    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            isComplete = true;

            -- Join Date
            if player.verifiedJoinDate[2] == 0 then
                if player.joinDateUnknown then
                    numJoinUnknown = numJoinUnknown + 1;
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].unknownIsComplete then
                        isComplete = false;
                    end
                else
                    numJoinNoDate = numJoinNoDate + 1;
                    isComplete = false;
                end
            end

            -- Promo Date
            if player.verifiedPromoteDate[2] == 0 then
                if player.promoteDateUnknown then
                    numPromoUnknown = numPromoUnknown + 1;
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].unknownIsComplete then
                        isComplete = false;
                    end
                else
                    numPromoNoDate = numPromoNoDate + 1;
                    isComplete = false;
                end
            end

            -- Birthdays
            if player.events[2][1][1] == 0 then
                if player.birthdayUnknown then
                    numBdayUnknown = numBdayUnknown + 1;
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].unknownIsComplete and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeBirthdaysInAudit then
                        isComplete = false;
                    end
                else
                    numBdayNoDate = numBdayNoDate + 1;
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeBirthdaysInAudit then
                        isComplete = false;
                    end
                end
            end

            -- Main or Alt
            if not player.isMain then
                -- Ok, they are not the main... do they have alts? If they have alts, we should see if one of them is listed as main.
                if #player.alts > 0 then
                    local mainIsFound = false;
                    for m = 1 , #player.alts do
                        if player.alts[m][5] then
                            mainIsFound = true;
                            break;
                        end
                    end
                    -- No one is listed as "main" in alt grouping.
                    if not mainIsFound then
                        isComplete = false;
                    end
                else
                    isComplete = false;
                end
            end

            if not isComplete or not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].onlyShowIncomplete then
                if not isComplete then
                    count2 = count2 + 1;
                    table.insert ( listOfIncompleteNames , player.name );
                end
            end
        end
    end
    return { numJoinUnknown , numJoinNoDate , numPromoUnknown , numPromoNoDate , count2 , listOfIncompleteNames , numBdayUnknown , numBdayNoDate };
end

-- Method:          RefreshAddonUserFrames()
-- What it Does:    It Initializes and rebuilds the frames to see who you are syncing with in the guild and if not, why not.
-- Purpose:         Purely quality of life information.
GRM.RefreshAddonUserFrames = function()
    -- To prevent double spam...
    GRM_G.timer5 = 0;

    -- Notification that player has sync disabled themselves.
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersSyncEnabledText:Hide();
    else
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame.GRM_AddonUsersSyncEnabledText:Show();
    end

    -- Now, let's load and refresh the data!
    GRM.RegisterGuildAddonUsersRefresh ();
    GRM.BuildAddonUserScrollFrame();
end

-- Method:          GRM.RefreshAddEventFrame();
-- What it Does:    Refreshes the details, in case an event happes WHILE the window is open
-- Purpose:         QOL - Clean user experience. User it not forced to close window and reopen it to trigger updates. This will be used on the fly.
GRM.RefreshAddEventFrame = function()
    -- Clear the buttons first
    if GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons ~= nil then
        for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons do
            GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons[i][1]:Hide();
            GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_AddEventScrollChildFrame.allFrameButtons[i][1]:UnlockHighlight();
        end
    end
    -- Status Notification logic
    -- remember, position 1 is the guild name, so players start at index 2
    if #GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName] > 0 then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameStatusMessageText:SetText ( GRM.L ( "Please Select Event to Add to Calendar" ) );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameStatusMessageText:Show();
        GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameNameToAddText:Hide();
        GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameNameDateText:Hide();
    else
        GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameStatusMessageText:SetText ( GRM.L ( "No Calendar Events to Add" ) );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameStatusMessageText:Show();
        GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameNameToAddText:Hide();
        GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameNameDateText:Hide();
    end

    if GRM_G.BuildVersion >= 30000 and CanEditGuildEvent() then
        if not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].allowEventsToCalendar then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameStatusMessageText2:SetText ( GRM.L ( "You Currently Have Disabled Adding Events to Calendar" ) );
            GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameStatusMessageText2:Show();
        else
            GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameStatusMessageText2:Hide();
        end
    else
        GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameStatusMessageText2:SetText ( GRM.L ( "You Do Not Have Permission to Add Events to Calendar" ) );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame.GRM_EventsFrameStatusMessageText2:Show();
    end
    -- Ok Building Frame!
    GRM.BuildEventCalendarManagerScrollFrame();
end

-- Method:          GRM.FinalReport()
-- What it Does:    Organizes flow of final report and send it to chat frame and to the logReport.
-- Purpose:         Clean organization for presentation.
GRM.FinalReport = function()
    if GRM_G.changeHappenedExitScan or not IsInGuild() then
        GRM.ResetTempLogs();
        GRM_G.changeHappenedExitScan = false;
        GRM_G.CurrentlyScanning = false;
        return;
    end
    local needToReport = false;

    -- For extra tracking info to display if the left player is on the server anymore...
    if #GRM_G.TempLeftGuild > 0 then
        -- need to build the names of those leaving for insert...
        
        local names = {};
        for i = 1 , #GRM_G.leavingPlayers do
            table.insert ( names , { GRM_G.leavingPlayers[i].name , GRM_G.leavingPlayers[i].GUID } );  -- Name and GUID
        end
        -- Establishing the players that left but are still on the server
        GRM.SetplayersStillOnServer ( names );
    end

    -- Cleanup the notes for reporting
    -- Join Dates Cleaned up First
    if #GRM_G.TempNewMember > 0 then
        local tempTable = {};
        for i = 1 , #GRM_G.TempNewMember do
            if string.find ( GRM_G.TempNewMember[i][2] , GRM.L ( "Invited By:" ) ) ~= nil then
                table.insert ( tempTable , 1 , GRM_G.TempNewMember[i] );
            else
                table.insert ( tempTable , GRM_G.TempNewMember[i] );
            end
        end
        GRM_G.TempNewMember = tempTable;
    end

    -- No need to spam the chat window when logging in.
    if not GRM_G.OnFirstLoad then

        if #GRM_G.TempBannedRejoin > 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].joined then
            
            for i = 1 , #GRM_G.TempBannedRejoin do
                GRM.PrintLog ( GRM_G.TempBannedRejoin[i] );
            end
        end
    
        if #GRM_G.TempRejoin > 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].joined then
            
            for i = 1 , #GRM_G.TempRejoin do
                GRM.PrintLog ( GRM_G.TempRejoin[i] );            -- Same Comments on down
            end
        end

        if #GRM_G.TempNewMember > 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].joined then
            
            for i = 1 , #GRM_G.TempNewMember do
                GRM.PrintLog ( GRM_G.TempNewMember[i] );   -- Send to print to chat window
            end
        end

        if #GRM_G.TempNameChanged > 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].nameChange then
            
            for i = 1 , #GRM_G.TempNameChanged do
                GRM.PrintLog ( GRM_G.TempNameChanged[i] );
            end
        end

        if #GRM_G.TempLogPromotion > 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].promotion then
            
            for i = 1 , #GRM_G.TempLogPromotion do
                GRM.PrintLog ( GRM_G.TempLogPromotion[i] );
            end
        end

        if #GRM_G.TempLogDemotion > 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].demotion then
            
            for i = 1 , #GRM_G.TempLogDemotion do
                GRM.PrintLog ( GRM_G.TempLogDemotion[i] );                          
            end
        end

        if #GRM_G.TempInactiveReturnedLog > 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].inactiveReturn then
            
            for i = 1 , #GRM_G.TempInactiveReturnedLog do
                GRM.PrintLog ( GRM_G.TempInactiveReturnedLog[i] );
            end
        end

        if #GRM_G.TempRankRename > 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].rankRename then
            
            for i = 1 , #GRM_G.TempRankRename do
                GRM.PrintLog ( GRM_G.TempRankRename[i] );
            end
        end
        if #GRM_G.TempLogLeveled > 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].leveled then
            for i = 1 , #GRM_G.TempLogLeveled do
                GRM.PrintLog ( GRM_G.TempLogLeveled[i] );                  
            end
        end

        if #GRM_G.TempLogNote > 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].note then
            
            for i = 1 , #GRM_G.TempLogNote do
                GRM.PrintLog ( GRM_G.TempLogNote[i] );         
            end
        end

        if #GRM_G.TempLogONote > 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].officerNote then
            
            for i = 1 , #GRM_G.TempLogONote do
                GRM.PrintLog ( GRM_G.TempLogONote[i] );  
            end
        end

        if #GRM_G.TempEventReport > 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].eventAnnounce then
            
            for i = 1 , #GRM_G.TempEventReport do
                GRM.PrintLog ( GRM_G.TempEventReport[i] );
            end
        end

        if #GRM_G.TempEventRecommendKickReport > 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].recommend then
            
            for i = 1 , #GRM_G.TempEventRecommendKickReport do
                GRM.PrintLog ( GRM_G.TempEventRecommendKickReport[i] ); 
            end
        end

        if #GRM_G.TempEventRecommendPromotionReport > 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].recommend then
            
            for i = 1 , #GRM_G.TempEventRecommendPromotionReport do
                GRM.PrintLog ( GRM_G.TempEventRecommendPromotionReport[i] ); 
            end
        end

        if #GRM_G.TempEventRecommendDemotionReport > 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].recommend then
            
            for i = 1 , #GRM_G.TempEventRecommendDemotionReport do
                GRM.PrintLog ( GRM_G.TempEventRecommendDemotionReport[i] ); 
            end
        end


    end

    -- OK, NOW LET'S REPORT TO LOG FRAME IN REVERSE ORDER!!!

    if #GRM_G.TempEventRecommendKickReport > 0 then
        needToReport = true;
        if GRM_G.OnFirstLoad then
            GRM_G.ChangesFoundOnLoad = true;
        end
        for i = 1 , #GRM_G.TempEventRecommendKickReport do
            GRM.AddLog ( GRM_G.TempEventRecommendKickReport[i] );                    
        end
    end

    if #GRM_G.TempEventRecommendPromotionReport > 0 then
        needToReport = true;
        if GRM_G.OnFirstLoad then
            GRM_G.ChangesFoundOnLoad = true;
        end
        for i = 1 , #GRM_G.TempEventRecommendPromotionReport do
            GRM.AddLog ( GRM_G.TempEventRecommendPromotionReport[i] );                    
        end
    end

    if #GRM_G.TempEventRecommendDemotionReport > 0 then
        needToReport = true;
        if GRM_G.OnFirstLoad then
            GRM_G.ChangesFoundOnLoad = true;
        end
        for i = 1 , #GRM_G.TempEventRecommendDemotionReport do
            GRM.AddLog ( GRM_G.TempEventRecommendDemotionReport[i] );                    
        end
    end

    if #GRM_G.TempEventReport > 0 then
        needToReport = true;
        if GRM_G.OnFirstLoad then
            GRM_G.ChangesFoundOnLoad = true;
        end
        for i = 1 , #GRM_G.TempEventReport do
            GRM.AddLog( GRM_G.TempEventReport[i] );
        end
    end

    if #GRM_G.TempLogONote > 0 then
        needToReport = true;
        if GRM_G.OnFirstLoad then
            GRM_G.ChangesFoundOnLoad = true;
        end
        for i = 1 , #GRM_G.TempLogONote do
            GRM.AddLog ( GRM_G.TempLogONote[i] );                    
        end
    end
 
    if #GRM_G.TempLogNote > 0 then
        needToReport = true;
        if GRM_G.OnFirstLoad then
            GRM_G.ChangesFoundOnLoad = true;
        end
        for i = 1 , #GRM_G.TempLogNote do
            GRM.AddLog ( GRM_G.TempLogNote[i] );                    
        end
    end

    if #GRM_G.TempLogLeveled > 0 then
        needToReport = true;
        if GRM_G.OnFirstLoad then
            GRM_G.ChangesFoundOnLoad = true;
        end
        for i = 1 , #GRM_G.TempLogLeveled do
            GRM.AddLog ( GRM_G.TempLogLeveled[i] );                    
        end
    end

    if #GRM_G.TempRankRename > 0 then
        needToReport = true;
        if GRM_G.OnFirstLoad then
            GRM_G.ChangesFoundOnLoad = true;
        end
        for i = 1 , #GRM_G.TempRankRename do
            GRM.AddLog ( GRM_G.TempRankRename[i] );
        end
    end

    if #GRM_G.TempLogDemotion > 0 then
        needToReport = true;
        if GRM_G.OnFirstLoad then
            GRM_G.ChangesFoundOnLoad = true;
        end
        for i = 1 , #GRM_G.TempLogDemotion do
            GRM.AddLog ( GRM_G.TempLogDemotion[i] );                           
        end
    end

    if #GRM_G.TempLogPromotion > 0 then
        needToReport = true;
        if GRM_G.OnFirstLoad then
            GRM_G.ChangesFoundOnLoad = true;
        end
        for i = 1 , #GRM_G.TempLogPromotion do
            GRM.AddLog ( GRM_G.TempLogPromotion[i] );
        end
    end

    if #GRM_G.TempNameChanged > 0 then
        needToReport = true;
        if GRM_G.OnFirstLoad then
            GRM_G.ChangesFoundOnLoad = true;
        end
        for i = 1 , #GRM_G.TempNameChanged do
            GRM.AddLog ( GRM_G.TempNameChanged[i] );
        end
    end

    if #GRM_G.TempInactiveReturnedLog > 0 then
        needToReport = true;
        if GRM_G.OnFirstLoad then
            GRM_G.ChangesFoundOnLoad = true;
        end
        for i = 1 , #GRM_G.TempInactiveReturnedLog do
            GRM.AddLog ( GRM_G.TempInactiveReturnedLog[i] );
        end
    end

    if #GRM_G.TempBannedRejoin > 0 then
        needToReport = true;
        if GRM_G.OnFirstLoad then
            GRM_G.ChangesFoundOnLoad = true;
        end
        for i = 1 , #GRM_G.TempBannedRejoin do
            GRM.AddLog ( GRM_G.TempBannedRejoin[i] );
        end
    end

    if #GRM_G.TempRejoin > 0 then
        needToReport = true;
        if GRM_G.OnFirstLoad then
            GRM_G.ChangesFoundOnLoad = true;
        end
        for i = 1 , #GRM_G.TempRejoin do
            GRM.AddLog ( GRM_G.TempRejoin[i] );
        end
    end

    if #GRM_G.TempNewMember > 0 then
        needToReport = true;
        if GRM_G.OnFirstLoad then
            GRM_G.ChangesFoundOnLoad = true;
        end
        for i = 1 , #GRM_G.TempNewMember do
            GRM.AddLog ( GRM_G.TempNewMember[i] );                                           -- Adding to the Log of Events
        end
    end

    -- FINAL PROCESSING INFORMATION!!!
    if GRM_G.changeHappenedExitScan or not IsInGuild() then
        GRM.ResetTempLogs();
        GRM_G.changeHappenedExitScan = false;
        GRM_G.CurrentlyScanning = false;
        return;
    end

    local timer = 0;
    if #GRM_G.TempLeftGuild > 0 then
        timer = 1.5;
    end

    C_Timer.After ( timer , function()
        -- Let's go through the Left Players.
        if #GRM_G.TempLeftGuild > 0 then
            needToReport = true;
            if GRM_G.OnFirstLoad then
                GRM_G.ChangesFoundOnLoad = true;
            end
            -- Let's compare our left players now...
            local isMatched = false;
            for i = 1 , #GRM_G.leavingPlayers do
                isMatched = false;
                for j = 1 , #GRM_G.playersStillOnServer do
                    if GRM_G.leavingPlayers[i].name == GRM_G.playersStillOnServer[j] then
                        isMatched = true;
                        -- now let's match it to propper tempLeft table
                        break;
                    end
                end

                local timePassed = GRM.GetTimePlayerHasBeenMember ( GRM_G.leavingPlayers[i].name );
                if timePassed ~= "" then
                    timePassed = ( "|cFFFFFFFF" .. GRM.L ( "Time as Member:" ) .. " " .. timePassed .. "|r" );
                end
                -- if not isMatched then (player not on friends list... this means that the player has left the server or namechanged)
                if not isMatched then
                    for m = 1 , #GRM_G.TempLeftGuild do
                        if string.find ( GRM_G.TempLeftGuild[m][2] , GRM.L ( "Player no longer on Server" ) ) == nil and ( string.find ( GRM_G.TempLeftGuild[m][2] , GRM.L ( "has Left the guild" ) ) ~= nil or string.find ( GRM_G.TempLeftGuild[m][2] , GRM.L ( "is no longer in the Guild!" ) ) ~= nil ) and string.find ( GRM_G.TempLeftGuild[m][2] , GRM.SlimName ( GRM_G.leavingPlayers[i].name ) ) ~= nil then
                            if string.find ( GRM_G.TempLeftGuild[m][2] , GRM.L ( "ALTS IN GUILD:" ) ) ~= nil then
                                local index2 = select ( 2 , string.find ( GRM_G.TempLeftGuild[m][2] , "\n" ) );
                                if timePassed ~= "" then
                                    GRM_G.TempLeftGuild[m][2] = string.sub ( GRM_G.TempLeftGuild[m][2] , 1 , index2 - 1 ) .. " |CFFFF0000(" .. GRM.L ( "Player no longer on Server" ) .. ")|CFF808080" .. string.sub ( GRM_G.TempLeftGuild[m][2] , index2 ) .. "\n" .. timePassed;
                                else
                                    GRM_G.TempLeftGuild[m][2] = string.sub ( GRM_G.TempLeftGuild[m][2] , 1 , index2 - 1 ) .. " |CFFFF0000(" .. GRM.L ( "Player no longer on Server" ) .. ")|CFF808080" .. string.sub ( GRM_G.TempLeftGuild[m][2] , index2 );
                                end
                            else
                                if timePassed ~= "" then
                                    GRM_G.TempLeftGuild[m][2] = GRM_G.TempLeftGuild[m][2] .. " |CFFFF0000(" .. GRM.L ( "Player no longer on Server" ) .. ")\n" .. timePassed;
                                else
                                    GRM_G.TempLeftGuild[m][2] = GRM_G.TempLeftGuild[m][2] .. " |CFFFF0000(" .. GRM.L ( "Player no longer on Server" ) .. ")";
                                end
                            end
                            GRM_G.TempLeftGuild[m][12] = true;
                            break;
                        end
                    end
                else
                    -- Player is still on the server, just no longer in the guild
                    for m = 1 , #GRM_G.TempLeftGuild do
                        if string.find ( GRM_G.TempLeftGuild[m][2] , GRM.L ( "has Left the guild" ) ) ~= nil and string.find ( GRM_G.TempLeftGuild[m][2] , GRM.SlimName ( GRM_G.leavingPlayers[i].name ) ) ~= nil then
                            if timePassed ~= "" then
                                GRM_G.TempLeftGuild[m][2] = GRM_G.TempLeftGuild[m][2] .. " (" .. timePassed .. ")";
                            else
                                GRM_G.TempLeftGuild[m][2] = GRM_G.TempLeftGuild[m][2];
                            end
                            break;
                        end
                    end

                    for m = 1 , #GRM_G.TempLeftGuild do
                        if string.find ( GRM_G.TempLeftGuild[m][2] , GRM.L ( "kicked" ) ) ~= nil and string.find ( GRM_G.TempLeftGuild[m][2] , GRM.SlimName ( GRM_G.leavingPlayers[i].name ) ) ~= nil then
                            if timePassed ~= "" then
                                GRM_G.TempLeftGuild[m][2] = GRM_G.TempLeftGuild[m][2] .. " (" .. timePassed .. ")";
                            else
                                GRM_G.TempLeftGuild[m][2] = GRM_G.TempLeftGuild[m][2];
                            end
                            break;
                        end
                    end
                end

            end
            -- Ok, sending to chat
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].left then
                for i = 1 , #GRM_G.TempLeftGuild do
                    GRM.PrintLog ( GRM_G.TempLeftGuild[i] );
                end
            end
            -- sending to log
            for i = 1 , #GRM_G.TempLeftGuild do
                if GRM_G.OnFirstLoad then
                    GRM_G.ChangesFoundOnLoad = true;
                end
                GRM.AddLog ( GRM_G.TempLeftGuild[i] );
            end
        end

        -- Update the Add Event Window
        if #GRM_G.TempEventReport > 0 and GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame:IsVisible() then
            GRM.RefreshAddEventFrame();
        end
        -- Clear the changes.
        GRM.ResetTempLogs();

        if GRM_G.OnFirstLoad then
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].viewOnLoad then
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].onlyViewIfChanges and GRM_G.ChangesFoundOnLoad then
                    GRM_UI.GRM_RosterChangeLogFrame:Show();
                end
            end

        end
        -- Let's update the frames!
        if needToReport and GRM_UI.GRM_RosterChangeLogFrame ~= nil and GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then
            GRM.BuildLogComplete( true , true );
        end

        if GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
            GRM.PopulateMemberDetails ( GRM_G.currentName );
        end

        GRM_G.TimeAtCompletion = time();
        GRM_G.OnFirstLoad = false;
        GRM_G.numRanksHasChanged = false;
        GRM_G.changeHappenedExitScan = false;
        GRM_G.CurrentlyScanning = false;
    end);
end

-- Method:          GRM.GetLogFormattedTimestamp ( array , string )
-- What it Does:    Attaches the timestamp properly to the log string at start and in converted format
-- Purpose:         Reusable function for cleaner code for repeat reporting.
GRM.GetLogFormattedTimestamp = function ( date , result )
    if date[4] ~= nil and date[5] ~= nil then
        result = GRM.FormatTimeStamp ( date , true ) .. " : " .. result;
    end

    return result;
end

-------------------------------
----- BUILD LOG STRINGS -------
-------------------------------

-- Method:          GRM.NoteFormatParsingProtection ( string )
-- What it Does:    Changes any "%" to be "%%" before passing through strings in custom notes as the % is a regex indicator of an escape capture index. So, if a player has % in their note it will disappear, If it has %% it will show. It also will convert any URL code to the char, like the %20 = a space.
-- Purpose:         Prevent any conflicts on player's own note, officer note, custom note.
GRM.NoteFormatParsingProtection = function ( note )

    local function urlCodeToChar ( x )
        return string.char ( tonumber ( x , 16 ) );
    end
    return string.gsub ( string.gsub ( note , "%%(%x%x)" , urlCodeToChar ) , "%%" , "%%%%" );

end

-- Method:          GRM.GetPromotionLogString ( bool , string , string , string , string , array )
-- What it Does:    Builds the string on the fly of when someone is promoted in the guild for the log
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetPromotionLogString = function ( foundInLog , player1 , player2 , initRank , finRank , date )
    local result = "";

    if foundInLog and ( initRank ~= finRank ) then
        result = GRM.L ( "{name} PROMOTED {name2} from {custom1} to {custom2}" , player1 , player2 , nil , initRank , finRank );
    else
        if initRank == finRank then

            for i = 1 ,  GuildControlGetNumRanks() do
                if GuildControlGetRankName ( i ) == initRank then
                    finRank = finRank .. " (" .. ( tostring ( i ) ) .. ")"   -- new rank index
                    initRank = initRank .. " (" .. ( tostring ( i + 1 ) ) .. ")"; -- Old rank index
                    break;
                end
            end
        end

        result = GRM.L ( "{name} has been PROMOTED from {custom1} to {custom2}" , player2 , nil , nil , initRank , finRank );
    end

    return GRM.GetLogFormattedTimestamp ( date , result ) , result;
end

-- Method:          GRM.GetDemotionLogString ( bool , string , string , string , string , array )
-- What it Does:    Builds the string on the fly of when someone is demoted in the guild for the log
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetDemotionLogString = function ( foundInLog , player1 , player2 , initRank , finRank , date )
    local result = "";

    if foundInLog then
        result = GRM.L ( "{name} DEMOTED {name2} from {custom1} to {custom2}" , player1 , player2 , nil , initRank , finRank );
    else
        result = GRM.L ( "{name} has been DEMOTED from {custom1} to {custom2}" , player2 , nil , nil , initRank , finRank );
    end

    return GRM.GetLogFormattedTimestamp ( date , result ) , result;
end

-- Method:          GRM.GetLeveledString ( string , int , int , int , array )
-- What it Does:    Builds the string on the fly of when someone has leveled and reached configured milestones.
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetLeveledString = function ( simpleName , milestoneLevel , level , numGained , date , atLevelCap , fullName )
    local result = "";

    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].useMainTag and fullName ~= nil then
        local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][ fullName ];

        if player ~= nil then

            local mainDisplay = GRM.GetMainTags ( false , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].mainTagIndex );
            if player.isMain then
                simpleName = simpleName .. " " .. GRM_G.MainTagHexCode .. mainDisplay .. "|r";
            else
                if #player.alts > 0 then
                    for j = 1 , #player.alts do
                        if player.alts[j][5] then
                            simpleName = simpleName .. " " .. GRM_G.MainTagHexCode .. mainDisplay .. "|r (" .. GRM.GetStringClassColorByName ( player.alts[j][1] , false ) .. GRM.SlimName ( player.alts[j][1] ) .. "|r)";

                            break;
                        end
                    end
                end
            end
        end
    end

    if milestoneLevel == 0 or atLevelCap then
        if atLevelCap then
            result = GRM.L ( "{name} has Reached the {num} Level Cap! {custom1}" , simpleName , nil , "|cffafffdc"  .. tostring ( level ).. "|r" , "|cffffd600***|r |cffafffdc" .. GRM.L ( "Hurray!" ) .. "|r|cffffd600***|r" );
        else
            result = GRM.L ( "{name} has Leveled to {num}" , simpleName , nil , level );
        end
    else
        if milestoneLevel ~= level then
            result = GRM.L ( "{name} has Surpassed their Level {num} Milestone and is Now {custom1}" , simpleName , nil , milestoneLevel , level );
        else
            result = GRM.L ( "{name} has Reached their Level {num} Milestone" , simpleName , nil , level );
        end
    end
    if numGained > 1 then
        result = result .. " " .. GRM.L ( "(+{num} levels)" , nil , nil , numGained );
    else
        result = result .. " " .. GRM.L ( "(+{num} level)" , nil , nil , numGained );
    end
    result = GRM.GetLogFormattedTimestamp ( date , result );

    return result;
end

-- Method:          GRM.GetNoteChangeString ( string , string , string , array )
-- What it Does:    Builds the string on the fly of when someone's public note has changed
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetNoteChangeString = function ( simpleName , oldNote , newNote , date )
    local result = "";

    oldNote = GRM.NoteFormatParsingProtection ( oldNote );
    newNote = GRM.NoteFormatParsingProtection ( newNote );

    if oldNote == "" then
        result = GRM.L ( "{name}'s PUBLIC Note: \"{custom1}\" was Added" , simpleName , nil , nil , newNote );
    elseif newNote == "" then
        result = GRM.L ( "{name}'s PUBLIC Note: \"{custom1}\" was Removed" , simpleName , nil , nil , oldNote );
    else
        result = GRM.L ( "{name}'s PUBLIC Note: \"{custom1}\" to \"{custom2}\"" , simpleName , nil , nil , oldNote , newNote );
    end
    
    return GRM.GetLogFormattedTimestamp ( date , result ) , result;
end

-- Method:          GRM.GetOfficerNoteChangeString ( string , string , string , array )
-- What it Does:    Builds the string on the fly of when someone's public note has changed
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetOfficerNoteChangeString = function ( simpleName , oldNote , newNote , date )
    local result = "";

    oldNote = GRM.NoteFormatParsingProtection ( oldNote );
    newNote = GRM.NoteFormatParsingProtection ( newNote );

    if oldNote == "" then
        result = GRM.L ( "{name}'s OFFICER Note: \"{custom1}\" was Added" , simpleName , nil , nil , newNote );
    elseif newNote == "" then
        result = GRM.L ( "{name}'s OFFICER Note: \"{custom1}\" was Removed" , simpleName , nil , nil , oldNote );
    else
        result = GRM.L ( "{name}'s OFFICER Note: \"{custom1}\" to \"{custom2}\"" , simpleName , nil , nil , oldNote , newNote );
    end
    return  GRM.GetLogFormattedTimestamp ( date , result ) , result;
end

-- Method:          GRM.GetRankRenamedString ( int , string , string , array )
-- What it Does:    Builds the string on the fly of when rank is changed or added or removed
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetRankRenamedString = function ( rankNum , oldRank , newRank , date )
    local result = "";

    if rankNum == nil then
        result = GRM.L ( "Guild Rank Renamed from {custom1} to {custom2}" , nil , nil , nil , oldRank , newRank );
    else
        if rankNum > 0 then
            if rankNum == 1 then
                result = GRM.L ( "A new rank has been added to the guild!" );
            else
                result = GRM.L ( "{num} new ranks have been added to the guild!" , nil , nil , rankNum );
            end
        else
            if rankNum == -1 then
                result = GRM.L ( "The guild has removed a rank!" );
            else
                result = GRM.L ( "{num} guild ranks have been removed!" , nil , nil , ( rankNum * -1 ) );
            end
        end
    end

    result = GRM.GetLogFormattedTimestamp ( date , result );

    return result;
end

-- Method:          GRM.GetInactiveReturnString ( string , int , array )
-- What it Does:    Builds the string for report on inactive player coming back online after absence.
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetInactiveReturnString = function ( simpleName , hoursReport , date )
    local result = "";

    result = GRM.L ( "{name} has Come ONLINE after being INACTIVE for {num}" , simpleName , nil , hoursReport );
    result = GRM.GetLogFormattedTimestamp ( date , result );

    return result;
end

-- Method:          GRM.GetRecommendKickString ( string , int , array , array )
-- What it Does:    Builds the string for report recommending a kick
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetRecommendKickString = function ( name , numRules , date , ruleNames )
    local result = "";

    -- Original formatting
    if type ( numRules ) == "string" then
        result = GRM.L ( "{name} has been OFFLINE for {num}. Kick Recommended!" , name , nil , numRules );
    else

        -- TOP line
        if numRules > 1 then
            result = GRM.L ( "{name} matches {num} macro tool rules. Kick Recommended!" , GRM.SlimName ( name ) , nil , numRules );
        else
            result = GRM.L ( "{name} matches {num} macro tool rule. Kick Recommended!" , GRM.SlimName ( name ) , nil , numRules );
        end

        if ruleNames then
            for i = 1 , #ruleNames do
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].kickRules[ruleNames[i]] and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].kickRules[ruleNames[i]].customLog and #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].kickRules[ruleNames[i]].customLogMsg > 0 then
                    result = result .. "\n|CFFFFFFFF\"" .. ruleNames[i] .. "\"|r - " .. GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].kickRules[ruleNames[i]].customLogMsg;
                end
            end
        end
    end
    
    result = GRM.GetLogFormattedTimestamp ( date , result );

    return result;
end

-- Method:          GRM.GetPromotionRecommendString ( string , int , array , array )
-- What it Does:    Builds the string for report recommending a promotion
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetPromotionRecommendString = function ( name , numRules , date , ruleNames )
    local result = "";

     -- TOP line
     if numRules > 1 then
        result = GRM.L ( "{name} matches {num} macro tool rules. Promotion Recommended!" , GRM.SlimName ( name ) , nil , numRules );
    else
        result = GRM.L ( "{name} matches {num} macro tool rule. Promotion Recommended!" , GRM.SlimName ( name ) , nil , numRules );
    end

    if ruleNames then
        for i = 1 , #ruleNames do
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].promoteRules[ruleNames[i]] and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].promoteRules[ruleNames[i]].customLog and #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].promoteRules[ruleNames[i]].customLogMsg > 0 then
                result = result .. "\n|CFFFFFFFF\"" .. ruleNames[i] .. "\"|r - " .. GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].promoteRules[ruleNames[i]].customLogMsg;
            end
        end
    end
    
    result = GRM.GetLogFormattedTimestamp ( date , result );

    return result;
end

-- Method:          GRM.GetDemotionRecommendString ( string , int , array , array )
-- What it Does:    Builds the string for report recommending a Demotion
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetDemotionRecommendString = function ( name , numRules , date , ruleNames )
    local result = "";

     -- TOP line
     if numRules > 1 then
        result = GRM.L ( "{name} matches {num} macro tool rules. Demotion Recommended!" , GRM.SlimName ( name ) , nil , numRules );
    else
        result = GRM.L ( "{name} matches {num} macro tool rule. Demotion Recommended!" , GRM.SlimName ( name ) , nil , numRules );
    end

    if ruleNames then
        for i = 1 , #ruleNames do
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].demoteRules[ruleNames[i]] and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].demoteRules[ruleNames[i]].customLog and #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].demoteRules[ruleNames[i]].customLogMsg > 0 then
                result = result .. "\n|CFFFFFFFF\"" .. ruleNames[i] .. "\"|r - " .. GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].demoteRules[ruleNames[i]].customLogMsg;
            end
        end
    end
    
    result = GRM.GetLogFormattedTimestamp ( date , result );

    return result;
end

-- Method:          GRM.GetNameChangeString ( string , int , array )
-- What it Does:    Builds the string for report recommending a kick
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetNameChangeString = function ( simpleNewName , oldName , date )
    local result = "";

    result = GRM.L ( "{name} has Name-Changed to {name2}" , oldName , simpleNewName );
    result = GRM.GetLogFormattedTimestamp ( date , result );
    return result;
end

-- Method:          GRM.GetLeftOrKickString ( string , bool , string , array , array , string , string , string , array , bool , bool , string )
-- What it Does:    Builds the string for players left or kicked from guild
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetLeftOrKickString = function ( unitName , playerWasKicked , timePassed , logEntryMetaData , listOfAlts , mainName , publicNote , officerNote , date , isFoundInEventLog , isNoLongerOnServer , isLiveDetection , playerLevel , customNote )
    local result = "";
    local level = "";

    if playerLevel ~= nil and playerLevel > 0 then
        level = ( " " .. GRM.L ( "(LVL: {num})" , nil , nil , playerLevel ) );
    end

    if timePassed ~= "" then
        timePassed = ( "\n|cFFFFFFFF" .. GRM.L ( "Time as Member:" ) .. " " .. timePassed .. "|r" );
    end

    if not playerWasKicked then

        -- Found in log OR Live
        if logEntryMetaData[1] and logEntryMetaData[2] ~= nil and logEntryMetaData[3] == nil then
            result = GRM.L ( "{name} has Left the guild" , GRM.SlimName ( logEntryMetaData[2] ) .. "|r" ) .. level;

        elseif isFoundInEventLog and not isLiveDetection and GRM.SlimName ( unitName ) ~= "" then
            result = GRM.L ( "{name} has Left the guild" , GRM.SlimName ( unitName ) .. "|r" ) .. level;
            
        else
            result = GRM.L ( "{name} is no longer in the Guild!" , GRM.SlimName ( unitName ) .. "|r" ) .. level;
        end

    else
        -- Log Entry found OR live kick/leave detected and successfully parsed the message
        if logEntryMetaData[1] and logEntryMetaData[2] ~= nil and logEntryMetaData[3] ~= nil then
            result = GRM.L ( "{name} KICKED {name2} from the Guild!" , GRM.SlimName ( logEntryMetaData[2] ) , GRM.SlimName ( logEntryMetaData[3] ) .. "|r" ) .. level;
            
        else
            result = GRM.L ( "{name} is no longer in the Guild!" , GRM.SlimName ( unitName ) .. "|r" ) .. level;
        end
    end

    if isNoLongerOnServer then
        result = result .. " |CFFFF0000(" .. GRM.L ( "Player no longer on Server" ) .. ")|r";
    end

    for i = 1 , #listOfAlts do
        if i == 1 then
            result = result .. "\n|CFFFFFFFF" .. GRM.L ( "ALTS IN GUILD:" ) .. " " .. listOfAlts[i];
        else
            result = result .. GRM.L ( "," ) .. " " .. listOfAlts[i];
        end

        if i == 5 and i < #listOfAlts then
            result = result .. " |r|CFF7F7F7F" .. GRM.L ( "(+ {num} More)" , nil , nil , ( #listOfAlts - i ) .. "|r" );
            break;
        end
    end

    if mainName ~= "" and mainName ~= unitName then
        -- Ok, if they have no mainName, let's check the left player's list for matches..
        result = result .. "\n|CFFFFFFFF" .. GRM.L ( "Player's Main: {name}" , GRM.GetClassifiedName ( mainName , true ) .. "|r" );
    elseif mainName == unitName then

        local name = "";
        

        -- Found in log OR Live
        if logEntryMetaData[1] and logEntryMetaData[2] ~= nil and logEntryMetaData[3] == nil then
            name = GRM.SlimName ( logEntryMetaData[2] ) .. "|r";

        elseif isFoundInEventLog and not isLiveDetection and GRM.SlimName ( unitName ) ~= "" then
            name = GRM.SlimName ( unitName ) .. "|r";
            
        else
            name = GRM.SlimName ( unitName );
        end

        result = result .. "\n|CFFFFFFFF" .. GRM.L ( "{name} is listed as the Main" , name .. " " .. GRM_G.MainTagHexCode .. GRM.GetMainTags ( false , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].mainTagIndex ) .. "|r" );
    end

    if timePassed ~= "" then
        result = result .. timePassed;
    end

    if publicNote ~= "" then
        result = result .. "\n|CFFFFFFFF" .. GRM.L ( "Public Note:" ) .. " " .. publicNote .. "|r";
    end
    if officerNote ~= "" then
        result = result .. "\n|CFFFFFFFF" .. GRM.L ( "Officer Note:" ) .. " " .. officerNote .. "|r";
    end
    if customNote ~= nil and customNote ~= "" then
        result = result .. "\n|CFFFFFFFF" .. GRM.L ( "Custom Notes:" ) .. "\n" .. customNote .. "|r";
    end

    return GRM.GetLogFormattedTimestamp ( date , result ) , result;
end

-- Method:          GRM.GetCustomNoteChangeString ( string , string , string , array )
-- What it Does:    Builds the string on the fly of when someone's public note has changed
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetCustomNoteChangeString = function ( newNote , oldNote , editorName , editedName , date )
    local result = "";

    oldNote = GRM.NoteFormatParsingProtection ( oldNote );
    newNote = GRM.NoteFormatParsingProtection ( newNote );

    if oldNote == "" then
        result = GRM.L ( "{name} modified {name2}'s CUSTOM Note: \"{custom1}\" was Added" , GRM.GetClassifiedName ( editorName , true ) , GRM.GetClassifiedName ( editedName , true ) , nil , newNote );
    elseif newNote == "" then
        result = GRM.L ( "{name} modified {name2}'s CUSTOM Note: \"{custom1}\" was Removed" , GRM.GetClassifiedName ( editorName , true ) , GRM.GetClassifiedName ( editedName , true ) , nil , oldNote );
    else
        result = GRM.L ( "{name} modified {name2}'s CUSTOM Note: \"{custom1}\" to \"{custom2}\"" , GRM.GetClassifiedName ( editorName , true ) , GRM.GetClassifiedName ( editedName , true ) , nil , oldNote , newNote );
    end

    return GRM.GetLogFormattedTimestamp ( date , result ) , result;
end

-- Method:          GRM.GetGuildNameChangeString ( string , string , array )
-- What it Does:    Builds the string on the fly of when someone's public note has changed
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetGuildNameChangeString = function ( playerName , slimGuildName , date )
    local result = "";

    result = GRM.L ( "{name}'s Guild has Name-Changed to \"{name2}\"" , playerName , slimGuildName );

    return GRM.GetLogFormattedTimestamp ( date , result ) , result;
end

-- Method:          GRM.GetBanLogString ( bool , string , string , string ,  array )
-- What it Does:    Builds the string on the fly of when someone is promoted in the guild for the log
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetBanLogString = function ( banAlts , playerName , playerBannedName , reason , date )
    local result = "";

    if banAlts then
        result = GRM.L ( "{name} has BANNED {name2} and all linked alts from the guild!" , playerName , playerBannedName );
    else
        result = GRM.L ( "{name} has BANNED {name2} from the guild!" , playerName , playerBannedName );
    end

    if reason ~= "" then
        result = result .. ( "\n|CFFFFFFFF" .. GRM.L ( "Reason Banned:" ) .. " " .. reason );
    else
        result = result .. ( "\n|CFFFFFFFF" .. GRM.L ( "Reason Banned:" ) .. " " .. GRM.L ( "None Given" ) );
    end


    return GRM.GetLogFormattedTimestamp ( date , result ) , result;
end

-- (Log Index 20)
-- Method:          GRM.GetBanLogUpdateAndEditString ( bool , bool , string , string , string , array )
-- What it Does:    Builds the string on the fly of when someone is promoted in the guild for the log
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetBanLogUpdateAndEditString = function ( banAlts , isAnEdit , playerName , playerBannedName , reason , date )
    local result = "";

    if banAlts then
        if not isAnEdit then
            result = GRM.L ( "{name} has BANNED {name2} and all linked alts from the guild!" , playerName , playerBannedName );
        else
            result = GRM.L ( "{name} has Updated {name2}'s BAN and also BANNED all linked alts from the guild!" , playerName , playerBannedName );
        end
    else
        if not isAnEdit then
            result = GRM.L ( "{name} has BANNED {name2} from the guild!" , playerName , playerBannedName );
        else
            result = GRM.L ( "{name}'s Ban has been Updated by {name2}!" , playerBannedName , playerName );
        end
    end

    if reason ~= "" then
        result = result .. ( "\n|CFFFFFFFF" .. GRM.L ( "Reason Banned:" ) .. " " .. reason );
    else
        result = result .. ( "\n|CFFFFFFFF" .. GRM.L ( "Reason Banned:" ) .. " " .. GRM.L ( "None Given" ) );
    end

    return GRM.GetLogFormattedTimestamp ( date , result ) , result;
end

-- (Log Index 21)
-- Method:          GRM.GetUnBanString ( string , string , array )
-- What it Does:    Builds the string on the fly of when someone's public note has changed
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetUnBanString = function ( playerNameBanned , playerNameBanning , date )
    local result = "";
    result = GRM.L ( "{name} has Removed {name2} from the Ban List." , playerNameBanning , playerNameBanned );

    return GRM.GetLogFormattedTimestamp ( date , result ) , result;
end

-- (Log Index 22)
-- Method:          GRM.GetBanStatusSyncString ( string , bool , string , string , string , array )
-- What it Does:    Builds the string on the fly of when someone's public note has changed
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetBanStatusSyncString = function ( banStatus , isAnEdit , playerNameBanned , playerNameBanning , reason , date )
    local result = "";

    if banStatus == "ban" then
        if isAnEdit then
            result = GRM.L ( "{name}'s Ban has been Updated by {name2}!" , playerNameBanned , playerNameBanning );                           
        else
            result = GRM.L ( "{name} has been BANNED from the guild!" , playerNameBanned );
        end
        if reason ~= "" then
            result = result .. ( "\n|CFFFFFFFF" .. GRM.L ( "Reason Banned:" ) .. " " .. reason );
        else
            result = result .. ( "\n|CFFFFFFFF" .. GRM.L ( "Reason Banned:" ) .. " " .. GRM.L ( "None Given" ) );
        end
    else
        if playerNameBanning ~= "" then
            result = GRM.L ( "{name} has Removed {name2} from the Ban List." , playerNameBanning , playerNameBanned );
        else
            result = GRM.L ( "{name} has been UN-BANNED from the guild!" , playerNameBanned );
        end
    end

    return GRM.GetLogFormattedTimestamp ( date , result ) , result;
end



-- Method:          GRM.GetJoinOrRejoinString ()
-- What it Does:    Builds the string for players joined or rejoined the guild
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetJoinOrRejoinString = function ( foundInLog , player1 , player2 , date , isRejoin , playerLevel , previousName , numTimesInGuild , isBanned , reasonBanned , dateLeft , howLongAgo , dateOriginallyJoined , origRank , customNote )
    local logReport = "";
    local numTimesString = "";
    local nameChangeString = "";
    local dateLeftG = "";
    local dateOrigJoin = "";
    local howLongReport = "";
    local banReason = "";
    local customNoteString = "";
    local header = "";
    if origRank == nil then
        origRank = GRM.L ( "Unknown" );
    end
    if customNote == nil then
        customNote = "";
    end
    
    -- Rejoin String details!!!
    if isRejoin then

        -- Configure Reason Banned and warning!
        if isBanned then

            if reasonBanned == "" then
                banReason = GRM.L ( "None Given" );
            else
                banReason = reasonBanned;
            end

            header = ( GRM.L ( "WARNING!" ) .. "\n" .. GRM.L ( "{name} REJOINED the guild but was previously BANNED!" , player2 ) );
            if foundInLog then
                if player1 == nil then
                    player1 = "|CFFFFFFFF" .. GRM.L ( "Unknown" ) .. "|r";
                end
                header = header  .. " " .. GRM.L ( "(Invited by: {name})" , player1 ) .. "|CFFFFFFFF";
            else
                header = header .. "|CFFFFFFFF";  -- Return the string to WHITE for the information to clearly read it.
            end

        else
            
            if foundInLog and player1 ~= nil then
                header = GRM.L ( "{name} has REINVITED {name2} to the guild" , player1 , player2 ) .. " " .. GRM.L ( "(LVL: {num})" , nil , nil , playerLevel );
            else
                header = GRM.L ( "{name} has REJOINED the guild" , player2 ) .. " " .. GRM.L ( "(LVL: {num})" , nil , nil , playerLevel );

                if foundInLog and player1 == nil then
                    player1 = "|CFFFFFFFF" .. GRM.L ( "Unknown" ) .. "|r";
                    header = header  .. " " .. GRM.L ( "(Invited by: {name})" , player1 ) .. "|CFFFFFFFF";
                end
                
            end
        end
        
        -- Number of times returning to the guild
        if numTimesInGuild > 1 then
            numTimesString = GRM.L ( "{name} has Been in the Guild {num} Times Before" , player2 , nil , numTimesInGuild );
        else
            numTimesString = GRM.L ( "{name} is Returning for the First Time." , player2 );
        end

        -- Player Namechanged when they were not in the guild.
        if previousName ~= "" then
            nameChangeString = "\n" .. GRM.L ( "{name} has Name-Changed to {name2}" , previousName , player2 );
        end

        -- Date left
        if dateLeft ~= "" then
            dateLeftG = GRM.FormatTimeStamp ( dateLeft );
        else
            dateLeftG = GRM.L ( "Unknown" );
        end

        -- How long ago
        if howLongAgo ~= -1 then
            howLongReport = GRM.L ( "({num} ago)" , nil , nil , GRM.GetTimePassedUsingEpochTime ( howLongAgo ).timestamp );
        end

        -- Original join date
        if dateOriginallyJoined == "" then
            dateOrigJoin = GRM.L ( "Unknown" );
        else

            dateOrigJoin = dateOriginallyJoined;

            -- Let's remove the hour at the end.
            local tempDate = string.find ( dateOrigJoin , "%s%d%d:" );
            if tempDate ~= nil then
                dateOrigJoin = string.sub ( dateOrigJoin , 1 , tempDate - 1 );
            end
        end

        if customNote ~= "" then
            customNoteString = "\n|r|CFF66B5E6" .. GRM.L ( "Additional Notes:" ) .. "|r\n" .. customNote .. "\n";
        end

        local information = {};

        if isBanned then
            information = {
                { "|r|CFF66B5E6" .. GRM.L ( "Date of Ban:" ) .. "|r|CFFFFFFFF" , dateLeftG .. " " .. howLongReport } , 
                { "|r|CFF66B5E6" .. GRM.L ( "Date Originally Joined:" ) .. "|r|CFFFFFFFF" , dateOrigJoin } ,
                { "|r|CFF66B5E6" .. GRM.L ( "Old Guild Rank:" ) .. "|r|CFFFFFFFF" , origRank } ,
                { "|r|CFF66B5E6" .. GRM.L ( "Reason:" ) .. "|r|CFFFFFFFF" , banReason }
            };
        else
            information = {
                { "|r|CFF66B5E6" .. GRM.L ( "Date Left:" ) .. "|r" , dateLeftG .. " " .. howLongReport } ,
                { "|r|CFF66B5E6" .. GRM.L ( "Date Originally Joined:" ) .. "|r" , dateOrigJoin } ,
                { "|r|CFF66B5E6" .. GRM.L ( "Old Guild Rank:" ) .. "|r" , origRank }
            };
        end

        -- Straight column for reporting (since unable to find a good tabbing solution in Lua for column alignment)
        information = GRM.AllignTwoColumns ( information , 20 );

        -- At this the string is mostly built. It still does not have the timstamp attached to the front
        logReport = ( header .. "\n|r|CFFFFFFFF" .. numTimesString .. "|r" .. information .. nameChangeString .. customNoteString );

    else
        ---- IS NOT REJOIN
        logReport = GRM.L ( "{name} has JOINED the guild!" , player2 ) .. " " .. GRM.L ( "(LVL: {num})" , nil , nil , playerLevel );
        if foundInLog then
            if player1 == nil then
                player1 = "|CFFFFFFFF" .. GRM.L ( "Unknown" ) .. "|r";
            end
            logReport = logReport .. " - " .. GRM.L ( "Invited By: {name}" , player1 );
        end
    end

    return GRM.GetLogFormattedTimestamp ( date , logReport ) , logReport;
end

-- Method:          GRM.GetEventString ( int , string , string , int , bool , string , array , int )
-- What it Does:    Builds the string for report on events upcoming in the guild.
-- Purpose:         Ease of reusing this code and ease of reading it.
GRM.GetEventString = function ( eventIndex , fullName , class , eventDay , eventMonthIndex , isLeapYear , date , numYears )
    local result = "";
    local eventDate = "";

    -- This needs to be reprocessed for language issues.
    if ( eventDay == 29 and eventMonthIndex == 2 ) and not isLeapYear then    -- If anniversary happened on leap year date, and the current year is NOT a leap year, then put it on 1 Mar.
        eventDate = GRM.L ( "1 Mar" );
    else
        eventDate = eventDay .. " " .. GRM.L ( monthEnum2 [ tostring ( eventMonthIndex ) ] );
    end

    if eventIndex == 1 then
        result = select ( 2 , GRM.GetAnniversaryLogReport ( fullName , class , numYears ) );
    elseif eventIndex == 2 then
        result = select ( 2 , GRM.GetBirthdayLogReport ( fullName , class ) );
    end
    result = result .. " ( " .. eventDate .. " )";

    result = GRM.GetLogFormattedTimestamp ( date , result );

    return result;
end

-- Method:          GRM.ReProcessLogString ( table )
-- What it Does:    Reprocesses all log entries, using their metadata, if available, and re-saves the string
-- Purpose:         Allow the player the change languages and have the strings get reprocessed.
GRM.ReProcessLogString = function( logEntry )
    local entryIndex = logEntry[1];
    local result = "";

    if #logEntry > 2 then
        
        if entryIndex == 1 then
            result = GRM.GetPromotionLogString ( logEntry[3] , logEntry[4] , logEntry[5] , logEntry[6] , logEntry[7] , logEntry[8] );
            
        elseif entryIndex == 2 then
            result = GRM.GetDemotionLogString ( logEntry[3] , logEntry[4] , logEntry[5] , logEntry[6] , logEntry[7] , logEntry[8] );

        elseif entryIndex == 3 then
            result = GRM.GetLeveledString ( logEntry[3] , logEntry[4] , logEntry[5] , logEntry[6] , logEntry[7] , logEntry[8] , logEntry[9] );

        elseif entryIndex == 4 then
            result = GRM.GetNoteChangeString ( logEntry[3] , logEntry[4] , logEntry[5] , logEntry[6] );

        elseif entryIndex == 5 then
            result = GRM.GetOfficerNoteChangeString ( logEntry[3] , logEntry[4] , logEntry[5] , logEntry[6] );

        elseif entryIndex == 6 then
            result = GRM.GetRankRenamedString ( logEntry[3] , logEntry[4] , logEntry[5] , logEntry[6] );

        elseif entryIndex == 7 then
            result = GRM.GetJoinOrRejoinString ( logEntry[3] , logEntry[4] , logEntry[5] , logEntry[6] , logEntry[7] , logEntry[8] , logEntry[9] , logEntry[10] , logEntry[11] , logEntry[12] , logEntry[13] , logEntry[14] , logEntry[15] , logEntry[16] , logEntry[17] );

        elseif entryIndex == 8 then
            result = GRM.GetJoinOrRejoinString ( logEntry[3] , logEntry[4] , logEntry[5] , logEntry[6] , logEntry[7] , logEntry[8] );

        elseif entryIndex == 9 then
            result = GRM.GetJoinOrRejoinString ( logEntry[3] , logEntry[4] , logEntry[5] , logEntry[6] , logEntry[7] , logEntry[8] , logEntry[9] , logEntry[10] , logEntry[11] , logEntry[12] , logEntry[13] , logEntry[14] , logEntry[15] , logEntry[16] , logEntry[17] );

        elseif entryIndex == 10 then
            local isFoundInEventLog , playerLevel , isLiveDetection , customNote;
            
            if #logEntry < 12 then
                isFoundInEventLog = nil;
            else
                isFoundInEventLog = logEntry[12];
            end

            if #logEntry < 13 then
                playerLevel = nil;
            else
                playerLevel = logEntry[13];
            end

            if #logEntry < 14 then
                isLiveDetection = false;
            else
                isLiveDetection = logEntry[14];
            end

            if #logEntry < 15 then
                customNote = "";
            else
                customNote = logEntry[15];
            end
            
            result = GRM.GetLeftOrKickString ( logEntry[3] , logEntry[4] , logEntry[5] , logEntry[6] , logEntry[7] , logEntry[8] , logEntry[9] , logEntry[10] , logEntry[11] , isFoundInEventLog , nil , isLiveDetection , playerLevel , customNote )

        elseif entryIndex == 11 then
            result = GRM.GetNameChangeString ( logEntry[3] , logEntry[4] , logEntry[5] );

        elseif entryIndex == 14 then
            result = GRM.GetInactiveReturnString ( logEntry[3] , logEntry[4] , logEntry[5] );

        elseif entryIndex == 15 then
            local numYears;
            if #logEntry == 10 then
                numYears = logEntry[10];
            end
            result = GRM.GetEventString ( logEntry[3] , logEntry[4] , logEntry[5] , logEntry[6] , logEntry[7] , logEntry[8] , logEntry[9] , numYears );

        elseif entryIndex == 16 then
            if #logEntry == 5 then
                result = GRM.GetRecommendKickString ( logEntry[3] , logEntry[4] , logEntry[5] );
            elseif #logEntry == 6 then
                result = GRM.GetRecommendKickString ( logEntry[3] , logEntry[4] , logEntry[5] , logEntry[6] );
            end
        elseif entryIndex == 22 then
            result = GRM.GetPromotionRecommendString ( logEntry[3] , logEntry[4] , logEntry[5] , logEntry[6] );

        elseif entryIndex == 23 then
            result = GRM.GetDemotionRecommendString ( logEntry[3] , logEntry[4] , logEntry[5] , logEntry[6] );

        elseif entryIndex == 19 then
            result = GRM.GetCustomNoteChangeString ( logEntry[3] , logEntry[4] , logEntry[5] , logEntry[6] , logEntry[7] );

        elseif entryIndex == 20 then
            result = GRM.GetBanLogUpdateAndEditString ( logEntry[3] , logEntry[4] , logEntry[5] , logEntry[6] , logEntry[7] , logEntry[8] );

        elseif entryIndex == 21 then
            -- Error protection from an old bug...
            if type ( logEntry[4] ) == "boolean" then
                logEntry[1] = 20;
                result = GRM.GetBanLogUpdateAndEditString ( logEntry[3] , logEntry[4] , logEntry[5] , logEntry[6] , logEntry[7] , logEntry[8] );
            else
                result = GRM.GetUnBanString ( logEntry[3] , logEntry[4] , logEntry[5] );
            end
        
        end
    else
        result = logEntry[2];         -- Cannot reprocess old stirng. No metadata.
    end

    return result;
end

-- Method:          GRM.ReprocessAllLogEntriesToCurrentLanguage()
-- What it Does:    When Changing the language, this lets you reprocess the entire log in the new language.
-- Purpose:         Ability to re-translate your log as new translation strings open
GRM.ReprocessAllLogEntriesToCurrentLanguage = function()
    for i = 1 , #GRM_LogReport_Save[GRM_G.F][GRM_G.guildName] do
        GRM_LogReport_Save[GRM_G.F][GRM_G.guildName][i][2] = GRM.ReProcessLogString ( GRM_LogReport_Save[GRM_G.F][GRM_G.guildName][i] );
    end

    GRM.BuildLogComplete( true , true );
end

---------------------------------------
--------- END LOG BUILD STRINGS -------
---------------------------------------


-------------------------------------------
------- BUILD LOG ENTRY STORAGE FORMATS ---
-------------------------------------------

-- Method:          GRM.AddPromotionDateTempLogEntry ( bool , string , string , string , string , array )
-- What it Does     Stores a temp log entry that will later be added in the final report with the pre-processed string
-- Purpose:         By adding all the metadata the string can be re-processed if the player changes their preferred date format or language.
GRM.AddPromotionDateTempLogEntry = function ( foundInLog , player1 , player2 , initRank , finRank , date )
    table.insert ( GRM_G.TempLogPromotion , { 1 , GRM.GetPromotionLogString ( foundInLog , player1 , player2 , initRank , finRank , date ) , foundInLog , player1 , player2 , initRank , finRank , date } );
end

-- Method:          GRM.AddDemotionDateTempLogEntry ( bool , string , string , string , string , array )
-- What it Does     Stores a temp log entry that will later be added in the final report with the pre-processed string
-- Purpose:         By adding all the metadata the string can be re-processed if the player changes their preferred date format or language.
GRM.AddDemotionDateTempLogEntry = function ( foundInLog , player1 , player2 , initRank , finRank , date )
    table.insert ( GRM_G.TempLogDemotion , { 2 , GRM.GetDemotionLogString ( foundInLog , player1 , player2 , initRank , finRank , date ) , foundInLog , player1 , player2 , initRank , finRank , date } );
end

-- Method:          GRM.AddRankRenameEntry ( int , string , string , array )
-- What it Does     Stores a temp log entry that will later be added in the final report with the pre-processed string
-- Purpose:         By adding all the metadata the string can be re-processed if the player changes their preferred date format or language.
GRM.AddRankRenameEntry = function ( rankNum , oldRank , newRank , date )
    table.insert ( GRM_G.TempRankRename , { 6 , GRM.GetRankRenamedString ( rankNum , oldRank , newRank , date ) , rankNum , oldRank , newRank , date } );
end

-- Method:          GRM.AddLeveledTempLogEntry ( string , int , int , int , array , bool )
-- What it Does     Stores a temp log entry that will later be added in the final report with the pre-processed string
-- Purpose:         By adding all the metadata the string can be re-processed if the player changes their preferred date format or language.
GRM.AddLeveledTempLogEntry = function ( simpleName , milestoneLevel , level , numGained , date , levelCap , fullName )
    table.insert ( GRM_G.TempLogLeveled , { 3 , GRM.GetLeveledString ( simpleName , milestoneLevel , level , numGained , date , levelCap , fullName ) , simpleName , milestoneLevel , level , numGained , date , levelCap , fullName } );
end

-- Method:          GRM.AddNoteTempLogEntry ( string , string , string , array )
-- What it Does     Stores a temp log entry that will later be added in the final report with the pre-processed string
-- Purpose:         By adding all the metadata the string can be re-processed if the player changes their preferred date format or language.
GRM.AddNoteTempLogEntry = function ( simpleName , oldNote , newNote , date )
    table.insert ( GRM_G.TempLogNote , { 4 , GRM.GetNoteChangeString ( simpleName , oldNote , newNote , date ) , simpleName , oldNote , newNote , date } );
end

-- Method:          GRM.AddOfficerNoteTempLogEntry ( string , string , string , array )
-- What it Does     Stores a temp log entry that will later be added in the final report with the pre-processed string
-- Purpose:         By adding all the metadata the string can be re-processed if the player changes their preferred date format or language.
GRM.AddOfficerNoteTempLogEntry = function ( simpleName , oldNote , newNote , date )
    table.insert ( GRM_G.TempLogONote , { 5 , GRM.GetOfficerNoteChangeString ( simpleName , oldNote , newNote , date ) , simpleName , oldNote , newNote , date } );
end

-- Method:          GRM.AddInactiveReturnTempLogEntry ( string , string , string , array )
-- What it Does     Stores a temp log entry that will later be added in the final report with the pre-processed string
-- Purpose:         By adding all the metadata the string can be re-processed if the player changes their preferred date format or language.
GRM.AddInactiveReturnTempLogEntry = function ( simpleName , hoursReport , date )
    table.insert ( GRM_G.TempInactiveReturnedLog , { 14 , GRM.GetInactiveReturnString ( simpleName , hoursReport , date ) , simpleName , hoursReport , date } );
end

-- Method:          GRM.AddEventRecommendKickTempLogEntry ( string , int , array , array )
-- What it Does     Stores a temp log entry that will later be added in the final report with the pre-processed string
-- Purpose:         By adding all the metadata the string can be re-processed if the player changes their preferred date format or language.
GRM.AddEventRecommendKickTempLogEntry = function ( name , numRules , date , ruleNames )
    table.insert ( GRM_G.TempEventRecommendKickReport , { 16 , GRM.GetRecommendKickString ( name , numRules , date , ruleNames ) , name , numRules , date , ruleNames } );
end

-- Method:          GRM.AddEventRecommendPromotionLogEntry ( string , int , array , array )
-- What it Does     Stores a temp log entry that will later be added in the final report with the pre-processed string
-- Purpose:         By adding all the metadata the string can be re-processed if the player changes their preferred date format or language.
GRM.AddEventRecommendPromotionLogEntry = function ( name , numRules , date , ruleNames )
    table.insert ( GRM_G.TempEventRecommendPromotionReport , { 22 , GRM.GetPromotionRecommendString ( name , numRules , date , ruleNames ) , name , numRules , date , ruleNames } );
end

-- Method:          GRM.AddEventRecommendDemotionLogEntry ( string , int , array , array )
-- What it Does     Stores a temp log entry that will later be added in the final report with the pre-processed string
-- Purpose:         By adding all the metadata the string can be re-processed if the player changes their preferred date format or language.
GRM.AddEventRecommendDemotionLogEntry = function ( name , numRules , date , ruleNames )
    table.insert ( GRM_G.TempEventRecommendDemotionReport , { 23 , GRM.GetDemotionRecommendString ( name , numRules , date , ruleNames ) , name , numRules , date , ruleNames } );
end

-- Method:          GRM.AddNameChangeTempLogEntry ( string , string , string , array )
-- What it Does     Stores a temp log entry that will later be added in the final report with the pre-processed string
-- Purpose:         By adding all the metadata the string can be re-processed if the player changes their preferred date format or language.
GRM.AddNameChangeTempLogEntry = function ( simpleName , oldName , date )
    table.insert ( GRM_G.TempNameChanged , { 11 , GRM.GetNameChangeString ( simpleName , oldName , date ) , simpleName , oldName , date } );
end

-- Method:          GRM.AddLeftOrKickEntry ( string , bool , string , array , array , string , string , string , array , bool )
-- What it Does     Stores a temp log entry that will later be added in the final report with the pre-processed string
-- Purpose:         By adding all the metadata the string can be re-processed if the player changes their preferred date format or language.
GRM.AddLeftOrKickEntry = function ( unitName , playerKicked , timePassed , logEntryMetaData , listOfAlts , mainName , publicNote , officerNote , date , isFoundInEventLog , _ , playerLevel , customNote )
    table.insert ( GRM_G.TempLeftGuild , { 10 , GRM.GetLeftOrKickString ( unitName , playerKicked , timePassed , logEntryMetaData , listOfAlts , mainName , publicNote , officerNote , date , isFoundInEventLog , nil , nil , playerLevel , customNote ) , unitName , playerKicked , timePassed , logEntryMetaData , listOfAlts , mainName , publicNote , officerNote , date , isFoundInEventLog , playerLevel , false , customNote } );
end

-- Method:          GRM.AddBanRejoinEntry ( ... )
-- What it Does     Stores a temp log entry that will later be added in the final report with the pre-processed string
-- Purpose:         By adding all the metadata the string can be re-processed if the player changes their preferred date format or language.
GRM.AddBanRejoinEntry = function ( foundInLog , player1 , player2 , date , isRejoin , nameChangeReport , previousName , numTimesInGuild , isBanned , reasonBanned , dateLeft , howLongAgo , dateOriginallyJoined , origRank , customNote , playerLevel )
    table.insert (
        GRM_G.TempBannedRejoin , 
        { 9 , GRM.GetJoinOrRejoinString ( foundInLog , player1 , player2 , date , isRejoin , nameChangeReport , previousName , numTimesInGuild , isBanned , reasonBanned , dateLeft , howLongAgo , dateOriginallyJoined , origRank , customNote , playerLevel ) ,
        foundInLog , player1 , player2 , date , isRejoin , nameChangeReport , previousName , numTimesInGuild , isBanned , reasonBanned , dateLeft , howLongAgo , dateOriginallyJoined , origRank , customNote , playerLevel }
    );
end

-- Method:          GRM.AddNormalRejoinEntry ( ... )
-- What it Does     Stores a temp log entry that will later be added in the final report with the pre-processed string
-- Purpose:         By adding all the metadata the string can be re-processed if the player changes their preferred date format or language.
GRM.AddNormalRejoinEntry = function ( foundInLog , player1 , player2 , date , isRejoin , nameChangeReport , previousName , numTimesInGuild , isBanned , reasonBanned , dateLeft , howLongAgo , dateOriginallyJoined , origRank , customNote , playerLevel )
    table.insert (
        GRM_G.TempRejoin , 
        { 7 , GRM.GetJoinOrRejoinString ( foundInLog , player1 , player2 , date , isRejoin , nameChangeReport , previousName , numTimesInGuild , isBanned , reasonBanned , dateLeft , howLongAgo , dateOriginallyJoined , origRank , customNote , playerLevel ) ,
        foundInLog , player1 , player2 , date , isRejoin , nameChangeReport , previousName , numTimesInGuild , isBanned , reasonBanned , dateLeft , howLongAgo , dateOriginallyJoined , origRank , customNote , playerLevel }
    );
end

-- Method:          GRM.AddJoinEntry ( bool , string , string , array , bool , int )
-- What it Does     Stores a temp log entry that will later be added in the final report with the pre-processed string
-- Purpose:         By adding all the metadata the string can be re-processed if the player changes their preferred date format or language.
GRM.AddJoinEntry = function ( foundInLog , player1 , player2 , date , isRejoin , playerLevel )
    table.insert ( GRM_G.TempNewMember , { 8 , GRM.GetJoinOrRejoinString ( foundInLog , player1 , player2 , date , isRejoin , playerLevel ) , foundInLog , player1 , player2 , date , isRejoin , playerLevel } );
end

-- Method:          GRM.AddEventEntry ( int , string , string , int , bool , string , array , int )
-- What it Does     Stores a temp log entry that will later be added in the final report with the pre-processed string
-- Purpose:         By adding all the metadata the string can be re-processed if the player changes their preferred date format or language.
GRM.AddEventEntry = function ( eventIndex , fullName , class , eventDay , eventMonthIndex , isLeapYear , date , numYears )
    table.insert ( GRM_G.TempEventReport , { 15 , GRM.GetEventString ( eventIndex , fullName , class , eventDay , eventMonthIndex , isLeapYear , date , numYears ) , eventIndex , fullName , class , eventDay , eventMonthIndex , isLeapYear , date , numYears } );
end

-- Method:          GRM.GetRuleNames ( table )
-- What it Does:    Returns an array of strings of the given player's matching ruels
-- Purpose:         Log reporting
GRM.GetRuleNames = function( player )
    local result = {};

    for i = #player , 1 , -1 do
        table.insert ( result , player[i][1] );
    end

    return result;
end

-- Method           GRM.ScanRecommendationsList()
-- What it Does:    Builds recommend log messages and also reports them as needed
-- Purpose:         So player knows that it is time to kick a player.
GRM.ScanRecommendationsList = function()
    local playerRecommendation = {};
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    local tempListOfNames = {};
    local ruleNames = "";

    -- Kick Recommendations
    if CanGuildRemove() then        -- No need to do the work and report if you cannot remove players
        playerRecommendation = GRM.GetKickNamesByFilterRules();
        tempListOfNames = {};
        ruleNames = "";
    
        for i = 1 , #playerRecommendation do
            tempListOfNames[playerRecommendation[i].name] = {};     -- Going to be used for validating in list or not.
            ruleNames = GRM.GetRuleNames ( playerRecommendation[i] );
    
            if not guildData[playerRecommendation[i].name].recommendToKick then
                guildData[playerRecommendation[i].name].recommendToKick = true;     -- This acts to prevent the repeat announcement to the log.
                GRM.AddEventRecommendKickTempLogEntry ( GRM.GetClassifiedName ( playerRecommendation[i].name , true ) , #playerRecommendation[i] , select ( 2 , GRM.GetTimestamp() ) , ruleNames );
            end
    
        end
    
        -- Clear all names NOT on this list.
        for _ , player in pairs ( guildData ) do
            if type ( player ) == "table" and player.recommendToKick and tempListOfNames[player.name] == nil then
                player.recommendToKick = false;        
            end
        end
    end
    
    -- Promotion Recommendations
    if CanGuildPromote() then        -- No need to do the work and report if you cannot remove players
        playerRecommendation = GRM.GetNamesByFilterRules ( 2 );
        tempListOfNames = {};
        ruleNames = "";
    
        for i = 1 , #playerRecommendation do
            tempListOfNames[playerRecommendation[i].name] = {};     -- Going to be used for validating in list or not.
            ruleNames = GRM.GetRuleNames ( playerRecommendation[i] );
    
            if not guildData[playerRecommendation[i].name].recommendToPromote then
                guildData[playerRecommendation[i].name].recommendToPromote = true;     -- This acts to prevent the repeat announcement to the log.
                GRM.AddEventRecommendPromotionLogEntry ( GRM.GetClassifiedName ( playerRecommendation[i].name , true ) , #playerRecommendation[i] , select ( 2 , GRM.GetTimestamp() ) , ruleNames );
            end
    
        end

        -- Clear all names NOT on this list.
        for _ , player in pairs ( guildData ) do
            if type ( player ) == "table" and player.recommendToPromote and tempListOfNames[player.name] == nil then
                player.recommendToPromote = false;        
            end
        end
    end

    -- Demotion Recommendations
    if CanGuildDemote() then        -- No need to do the work and report if you cannot remove players
        playerRecommendation = GRM.GetNamesByFilterRules ( 3 );
        tempListOfNames = {};
        ruleNames = "";
    
        for i = 1 , #playerRecommendation do
            tempListOfNames[playerRecommendation[i].name] = {};     -- Going to be used for validating in list or not.
            ruleNames = GRM.GetRuleNames ( playerRecommendation[i] );
    
            if not guildData[playerRecommendation[i].name].recommendToDemote then
                guildData[playerRecommendation[i].name].recommendToDemote = true;     -- This acts to prevent the repeat announcement to the log.
                GRM.AddEventRecommendDemotionLogEntry ( GRM.GetClassifiedName ( playerRecommendation[i].name , true ) , #playerRecommendation[i] , select ( 2 , GRM.GetTimestamp() ) , ruleNames );
            end
    
        end

        -- Clear all names NOT on this list.
        for _ , player in pairs ( guildData ) do
            if type ( player ) == "table" and player.recommendToDemote and tempListOfNames[player.name] == nil then
                player.recommendToDemote = false;        
            end
        end
    end


end


-----------------------------------------
-------- END OF LOG ENTRY FORMATTING ----
-----------------------------------------

-- Method:          GRM.GetGuildEventString ( int , string , string )
-- What it Does:    Gets more exact info from the actual Guild Event Log ( can only be queried once per 10 seconds) as a string
-- Purpose:         This parses more exact info, like "who" did the kicking, or "who" invited who, and so on.
GRM.GetGuildEventString = function ( index , playerName , initRank , finRank , class )
    -- index 1 = demote , 2 = promote , 3 = remove/quit , 4 = invite/join
    local added = false;
    local eventType = { "demote" , "promote" , "invite" , "join" , "quit" , "remove" };
    local logEntryMetaData = { false };

    if GRM_G.BuildVersion >= 23000 then -- Cata 2.3
        QueryGuildEventLog();

        if index == 1 or index == 2 then
            for i = GetNumGuildEvents() , 1 , -1 do
                local type , p1, p2 , _ , year , month , day , hour = GetGuildEventInfo ( i );
                if p1 ~= nil then                                                 ---or eventType [ 2 ] == type ) and ( p2 ~= nil and p2 == playerName ) and p1 ~= nil then
                    if index == 1 and eventType [ 1 ] == type and p2 ~= nil and ( p2 == playerName or p2 == GRM.SlimName ( playerName ) ) then          -- DEMOTIONS
                        p1 = GRM.GetStringClassColorByName ( p1 ) .. GRM.SlimName ( p1 ) .. "|r";
                        p2 = GRM.GetStringClassColorByName ( p2 ) .. GRM.SlimName ( p2 ) .. "|r";
                        logEntryMetaData = { true , p1 , p2 , initRank , finRank , GRM.GetTimestampBasedOnTimePassed ( { day , month , year , hour } ) };
                        added = true;
                        break;
                    elseif index == 2 and eventType [ 2 ] == type and p2 ~= nil and ( p2 == playerName or p2 == GRM.SlimName ( playerName ) ) then      -- PROMOTIONS
                        p1 = GRM.GetStringClassColorByName ( p1 ) .. GRM.SlimName ( p1 ) .. "|r";
                        p2 = GRM.GetStringClassColorByName ( p2 ) .. GRM.SlimName ( p2 ) .. "|r";
                        logEntryMetaData = { true , p1 , p2 , initRank , finRank , GRM.GetTimestampBasedOnTimePassed ( { day , month , year , hour } ) };
                        added = true;
                        break;
                    end
                end
            end

            -- Remove or Quit
       elseif index == 3 then
            local notFound = true;
            for i = GetNumGuildEvents() , 1 , -1 do 
                local type , p1, p2 , _ , year , month , day , hour = GetGuildEventInfo ( i );
                if p1 ~= nil then 
                    if eventType [ 5 ] == type or eventType [ 6 ] == type then   -- Quit or Kicked

                        -- KICKED
                        if eventType [ 6 ] == type and p2 ~= nil and ( p2 == playerName or p2 == GRM.SlimName ( playerName ) ) then
                            p1 = GRM.GetStringClassColorByName ( p1 ) .. GRM.SlimName ( p1 ) .. "|r";
                            p2 = GRM.GetStringClassColorByName ( p2 ) .. GRM.SlimName ( p2 ).. "|r";
                            logEntryMetaData = { true , p1 , p2 , GRM.GetTimestampBasedOnTimePassed ( { day , month , year , hour } ) };
                            added = true;
                            notFound = false;

                        -- QUIT
                        elseif eventType [ 5 ] == type and ( p1 == playerName or p1 == GRM.SlimName ( playerName ) ) then
                            -- FOUND!
                            p1 = GRM.GetStringClassColorByName ( playerName ) .. GRM.SlimName ( playerName ) .. "|r";
                            logEntryMetaData = { true , p1 , nil , GRM.GetTimestampBasedOnTimePassed ( { day , month , year , hour } ) };
                            added = true;
                            notFound = false;
                        end
                        if notFound ~= true then
                            break;
                        end
                    end
                end
            end
            -- Invite
        elseif index == 4 then
            for i = GetNumGuildEvents() , 1 , -1 do 
                local type , p1, p2 , _ , year , month , day , hour = GetGuildEventInfo ( i );
                if eventType [ 3 ] == type and p1 ~= nil and p2 ~= nil and ( p2 == playerName or p2 == GRM.SlimName ( playerName ) ) then  -- invite
                    p1 = GRM.GetStringClassColorByName ( p1 ) .. GRM.SlimName ( p1 ) .. "|r";
                    p2 = GRM.GetClassHex ( class ) .. GRM.SlimName ( p2 ) .. "|r";
                    logEntryMetaData = { true , p1 , p2 , GRM.GetTimestampBasedOnTimePassed ( { day , month , year , hour } ) };
                    added = true;
                    break;
                end
            end

            -- JOINED but no invite data
            -- Possibly player joined but no record of *WHO* invited them
            if not added then
                for i = GetNumGuildEvents() , 1 , -1 do 
                    local type , p1, _ , _ , year , month , day , hour = GetGuildEventInfo ( i );  -- p1 = the player who joined
                    if eventType [ 4 ] == type and p1 ~= nil and ( p1 == playerName or p1 == GRM.SlimName ( playerName ) ) then  -- invite
                        p1 = GRM.GetClassHex ( class ) .. GRM.SlimName ( p1 ) .. "|r";
                        logEntryMetaData = { true , nil , p1 , GRM.GetTimestampBasedOnTimePassed ( { day , month , year , hour } ) };
                        added = true;
                        break;
                    end
                end
            end
        end
    end

    return added , logEntryMetaData;
end

-- Method:          GRM.RemoveNameFromAllREMAlts ( string )
-- What it Does:    Removes the player name from all potential stored points in the guild
-- Purpose:         No need to keep these points if they leave the guild as alt data is taken away.
GRM.RemoveNameFromAllREMAlts = function ( name )
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];

    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            if #player.removedAlts > 0 then
                for j = #player.removedAlts , 1 , -1 do
                    if name == player.removedAlts[j][1] then
                        table.remove ( player.removedAlts , j );
                    end
                end
            end
        end
    end

end

-- Method:          GRM.RecordKickChanges ( string , boolean , array , string )
-- What it Does:    Records and logs the changes for when a guildie either is KICKED or leaves the guild
-- Purpose:         Having its own function saves on repeating a lot of code here.
GRM.RecordKickChanges = function ( unitName , playerWasKicked , dateArray , officerThatKicked )
    local timestamp = GRM.GetTimestamp();
    local timeEpoch = time();

    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][unitName];
    local classColorCode = GRM.GetStringClassColorByName ( unitName );
    local timePassed = GRM.GetTimePlayerHasBeenMember ( unitName );
    local added = false;
    local logEntryMetaData = {};
    local listOfAlts = {};
    local tempStorage = { ( classColorCode .. unitName .. "|r" ) , playerWasKicked , timePassed };
    local isNoLongerOnServer = false;
    local playerLevel = 0;
    local customNote = "";

    -- Live detection
    if officerThatKicked ~= nil then
        added = true;
        local timeS , timeArray = GRM.GetTimestamp();
        logEntryMetaData = { true , GRM.GetStringClassColorByName ( officerThatKicked ) .. GRM.SlimName ( officerThatKicked ) .. "|r" , ( classColorCode .. unitName .. "|r" ) , { timeS , time() , timeArray } };
        timestamp = logEntryMetaData[4][1];
        timeEpoch = logEntryMetaData[4][2];
    -- Guild Event Log Detection
    else
        added , logEntryMetaData = GRM.GetGuildEventString ( 3 , unitName ); -- Kicked from the guild.
        if logEntryMetaData[1] then
            timestamp = logEntryMetaData[4][1];
            timeEpoch = logEntryMetaData[4][2];
            dateArray = logEntryMetaData[4][3];
        end
    end

    -- metaData = 4
    table.insert ( tempStorage , logEntryMetaData );
    
    -- Finding Player's record for removal of current guild and adding to the Left Guild table.
    if player ~= nil then
        -- Found!
        if player.customNote[6] ~= "" then
            customNote = player.customNote[6];
        end
        table.insert ( player.leftGuildDate , timestamp );                                       -- leftGuildDate
        table.insert ( player.leftGuildEpoch, timeEpoch );                                       -- leftGuildDateMeta

        if player.rankHistory[1][3] == 0 then
            table.insert ( player.rankHistory , { player.rankName , GRM.Trim ( string.sub ( timestamp , 1 , 10 ) ) , timeEpoch } )
        end

        table.insert ( player.rankHistory , { "|cFFC41F3BLeft Guild" , GRM.Trim ( string.sub ( timestamp , 1 , 10 ) ) , timeEpoch } );      -- Translate on show only, not here.
        player.oldRank = player.rankName;         -- old Rank on leaving.
        if #player.joinDate == 0 then                                                 -- Let it default to date addon was installed if date joined was never given
            table.insert( player.joinDate , player.rankHistory[1][2] );   -- oldJoinDate
            table.insert( player.joinDateEpoch , player.rankHistory[1][3] );   -- oldJoinDateMeta
        end

        -- If not banned, then let's ensure we reset his data.
        if not player.bannedInfo[1] then
            player.bannedInfo[1] = false;
            player.bannedInfo[2] = 0;
            player.bannedInfo[3] = false;
            player.bannedInfo[4] = "";
            player.reasonBanned = "";
        end
        
        playerLevel = player.level;
        -- Adding to LeftGuild Player history library
        GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][GRM_G.guildName][unitName] = {};
        GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][GRM_G.guildName][unitName] = GRM.DeepCopyArray ( player );
        
        -- Removing it from the alt list
        if #player.alts > 0 then

            -- Let's add them to the end of the report
            local countAlts = #player.alts;
            local isFound = false;

            for m = 1 , countAlts do
                isFound = false;

                -- Only do this if on a scan, not a live detection.
                if officerThatKicked == nil then
                    -- Verify the alt is not on the kick list already;
                    for r = 1 , #GRM_G.TempLeftGuildPlaceholder do
                        if GRM_G.TempLeftGuildPlaceholder[r][1] == player.alts[m][1] then
                            isFound = true;
                            break;
                        end
                    end
                end

                if not isFound then
                    table.insert ( listOfAlts , GRM.GetClassifiedName ( player.alts[m][1] , true ) );
                end
            end
            
            -- Let's overwrite the listOfAts
            local tempListOfAlts = GRM.DeepCopyArray ( player.alts );
            GRM.RemoveAlt ( player.alts[1][1] , unitName , false , 0 , false );                 -- removes from the current guild alt list...

            -- Set alt to main if it is only one left.

            if #tempListOfAlts == 1 then
                local tempEpochTime = 0;
                if added then
                    tempEpochTime = timeEpoch;
                end
                GRM.SetMain ( tempListOfAlts[1][1] , tempListOfAlts[1][1] , true , tempEpochTime );
            end
            
            -- Remove them from the playersLists as well...
            GRM.RemoveNameFromAllREMAlts ( unitName );

            -- Re-add the alt lists to the player now that they have left the guild...
            player = GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][GRM_G.guildName][unitName];
            if player ~= nil then
                player.alts = tempListOfAlts;         -- Stores the alt list in the left player databse. It will wipe on rejoin, but good to know...
            end
        end
        -- removing from active member library
        GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][unitName] = nil;
    end
    table.insert ( tempStorage , listOfAlts );

    -- Update the live frames too!
    if GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame:IsVisible() then
        GRM.RefreshBanListFrames();
    end
    
    local mainName , playerHasAlts = GRM.GetMainName ( unitName , false );
    if mainName ~= "" and mainName ~= unitName and playerHasAlts then
        -- Ok, if they have no mainName, let's check the left player's list for matches..
        table.insert ( tempStorage , GRM.GetClassifiedName ( mainName , true ) );

    else
        table.insert ( tempStorage , "" );
    end

    -- Now, let's add their officer/public notes.
    local p , o = "" , "";
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].addNotesToLeft then
        player = GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ][unitName];
        if player ~= nil then
            if ( player.note ~= nil and player.note ~= "" ) then
                p = player.note;                
            end
            if ( player.officerNote ~= nil and player.officerNote ~= "" and GRM.CanViewOfficerNote() ) then
                o = player.officerNote;
            end
        end
    end
    -- Let's get it added to the log
    return tempStorage[1] , tempStorage[2] , tempStorage[3] , tempStorage[4] , tempStorage[5] , tempStorage[6] , p , o , dateArray , added , isNoLongerOnServer , playerLevel , customNote;
end

-- Method:          GRM.RecordLeftGuildChanges ( name , array )
-- What it Does:    Records and saves the information when a player leaves the guild, and updates the frames
-- Purpose:         Useful for logging changes that they left the guild on their own, not just kicked.
GRM.RecordLeftGuildChanges = function ( unitName , dateArray , isLiveDetection )
    local timestamp = GRM.GetTimestamp();
    local timeEpoch = time();

    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][unitName];
    local timePassed = GRM.GetTimePlayerHasBeenMember ( unitName );
    local listOfAlts = {};
    local logEntryMetaData = { true , GRM.GetClassifiedName ( unitName , true ) , nil };
    local isNoLongerOnServer = false;
    local isFoundInEventLog = false;
    local playerLevel = 0;
    local customNote = "";
    local p , o = "" , "";  -- public/officer Notes
    local mainName = "";
    
    -- Finding Player's record for removal of current guild and adding to the Left Guild table.
    if player ~= nil then
        -- Found!

        -- Now, let's add their officer/public notes.
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].addNotesToLeft then
            if ( player.note ~= nil and player.note ~= "" ) then
                p = player.note;                
            end
            if ( player.officerNote ~= nil and player.officerNote ~= "" and GRM.CanViewOfficerNote() ) then
                o = player.officerNote;
            end
            if player.customNote[6] ~= "" then
                customNote = player.customNote[6];
            end
        end

        mainName = GRM.GetMainName ( unitName , false );

        table.insert ( player.leftGuildDate, timestamp );                                       -- leftGuildDate
        table.insert ( player.leftGuildEpoch, timeEpoch );                                       -- leftGuildDateMeta
        
        if player.rankHistory[1][3] == 0 then
            table.insert ( player.rankHistory , { player.rankName , GRM.Trim ( string.sub ( timestamp , 1 , 10 ) ) , timeEpoch } )
        end

        table.insert ( player.rankHistory , { "|cFFC41F3BLeft Guild" , GRM.Trim ( string.sub ( timestamp , 1 , 10 ) ) , timeEpoch } );      -- Translate on show only, not here.
        player.oldRank = player.rankName;         -- old Rank on leaving.
        if #player.joinDate == 0 then                                                 -- Let it default to date addon was installed if date joined was never given
            table.insert( player.joinDate , player.rankHistory[1][2] );                         -- oldJoinDate
            table.insert( player.joinDateEpoch , player.rankHistory[1][3] );   -- oldJoinDateMeta
        end

        -- If not banned, then let's ensure we reset his data.
        if not player.bannedInfo[1] then
            player.bannedInfo[1] = false;
            player.bannedInfo[2] = 0;
            player.bannedInfo[3] = false;
            player.bannedInfo[4] = "";
            player.reasonBanned = "";
        end
        playerLevel = player.level;

        -- Adding to LeftGuild Player history library
        GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][GRM_G.guildName][unitName] = player;

        -- Removing it from the alt list
        if #player.alts > 0 then
            for m = 1 , #player.alts do
                table.insert ( listOfAlts , GRM.GetClassifiedName ( player.alts[m][1] , true ) );
            end

            -- Let's overwrite the listOfAts
            local tempListOfAlts = GRM.DeepCopyArray ( player.alts );
            GRM.RemoveAlt ( player.alts[1][1] , player.name , false , 0 , false );                 -- removes from the current guild alt list...
            
            -- Remove them from the playersLists as well...
            GRM.RemoveNameFromAllREMAlts ( player.name );

            -- Re-add the alt lists to the player now that they have left the guild...
            player = GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][GRM_G.guildName][unitName];
            if player ~= nil then
                player.alts = tempListOfAlts;         -- Stores the alt list in the left player databse. It will wipe on rejoin, but good to know...
            end
        end
        -- removing from active member library
        GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][unitName] = nil;
    end
    
    -- Let's get it added to the log
    return unitName , false , timePassed , logEntryMetaData , listOfAlts , mainName , p , o , dateArray , isFoundInEventLog , isNoLongerOnServer , isLiveDetection , playerLevel , customNote;
end


-- Method:          GRM.IsRejoinAndSetDetails ( table , string , string , bool , array )
-- What it Does:    Checks the left player meta data, to the new player joining, and determins if they are rejoining, and if so, if they were banned
--                  It also manages setting the new officer/public note join date, if player uses said feature.
-- Purpose:         Special feature, scanning for changes, live tracking updates as player joins as well... and compartmentalizing this long function.
GRM.IsRejoinAndSetDetails = function( member , simpleName , tempTimeStamp , scanUpdate , tempJoinStorage )
    local guildData = GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][GRM_G.guildName];
    local useTimeStamp = false;
    local timeStamp = "";

    tempJoinStorage[5] = false;                 -- is a rejoin
    tempJoinStorage[6] = 0;                     -- Player Level
    tempJoinStorage[7] = "";                    -- The previousName
    tempJoinStorage[8] = 0;                     -- Number of Times in Guild
    tempJoinStorage[9] = false;                 -- IsBanned
    tempJoinStorage[10] = "";                   -- reasonBanned
    tempJoinStorage[11] = "";                   -- date left
    tempJoinStorage[12] = -1;                   -- How Long Ago
    tempJoinStorage[13] = "";                   -- Date originally joined
    tempJoinStorage[14] = "";                   -- Original Rank.
    tempJoinStorage[15] = "";                   -- Custom Notes

    local player;
    for formerMember in pairs ( guildData ) do -- Number of players that have left the guild.
        player = guildData[formerMember];

        if type ( player ) == "table" then
            if member.GUID == player.GUID then  -- GUID comparison check - cannot rely purely on the name.
                tempJoinStorage[5] = true;
                -- MATCH FOUND - Player is RETURNING to the guild!
                
                -- Player has name-changed?
                if member.name ~= player.name then
                    local classColorString = GRM.GetStringClassColorByName ( player.name , true );

                    -- For the logging
                    tempJoinStorage[7] = ( classColorString .. GRM.SlimName ( player.name ) .. "|r" );
                    -- Update the banned player's name
                    player.name = member.name;
                end

                
                -- number of times in the guild
                tempJoinStorage[8] = #player.joinDate;
                
                
                if tempTimeStamp ~= "1 Jan '01 12:01am" and tempTimeStamp ~= ( "1 Jan '01 00:01" .. GRM.L ( "24HR_Notation" ) ) then
                    timeStamp = tempTimeStamp;
                    useTimeStamp = true;
                else
                    timeStamp = GRM.GetTimestamp();
                end

                -- Universal Rejoin data whether banned or not
                -- Date player left the guild
                if #player.leftGuildDate > 0 then
                    tempJoinStorage[11] = player.leftGuildDate[#player.leftGuildDate];
                end
                -- How long ago that was
                if #player.leftGuildEpoch > 0 and tempJoinStorage[10] ~= "" then
                    tempJoinStorage[12] = player.leftGuildEpoch[#player.leftGuildEpoch];
                end
                -- Date originally joined the guild the first time ever
                if #player.joinDate > 0 then
                    tempJoinStorage[13] = player.joinDate[1];
                end
                -- Set original Rank
                tempJoinStorage[14] = player.oldRank;

                -- Set Custom Note details
                if player.customNote[6] ~= "" then
                    tempJoinStorage[15] = player.customNote[6];
                end

                -- isBanned
                local index = 0;
                if player.bannedInfo[1] then
                    index = 9;
                    tempJoinStorage[9] = true;
                    tempJoinStorage[10] = player.reasonBanned;
                                
                    if not scanUpdate then
                        -- Written fully out, not as necessary, but to make the next section easier to work with with specific named arguments rather than 16 indexes of an array
                        GRM.AddBanRejoinEntry ( 
                            tempJoinStorage[1] , tempJoinStorage[2] , tempJoinStorage[3] , tempJoinStorage[4] , tempJoinStorage[5] , tempJoinStorage[6] , tempJoinStorage[7] , tempJoinStorage[8] ,
                            tempJoinStorage[9] , tempJoinStorage[10] , tempJoinStorage[11] , tempJoinStorage[12] , tempJoinStorage[13] , tempJoinStorage[14] , tempJoinStorage[15]
                        );
                    else
                        useTimeStamp = true;
                    end
                else
                    index = 7;
                    -- No Ban found, player just returning!
                    -- Set member level
                    tempJoinStorage[6] = member.level;

                    if not scanUpdate then
                        -- Written fully out, not as necessary, but to make the next section easier to work with with specific named arguments rather than 16 indexes of an array
                        GRM.AddNormalRejoinEntry ( 
                            tempJoinStorage[1] , tempJoinStorage[2] , tempJoinStorage[3] , tempJoinStorage[4] , tempJoinStorage[5] , tempJoinStorage[6] , tempJoinStorage[7] , tempJoinStorage[8] ,
                            tempJoinStorage[9] , tempJoinStorage[10] , tempJoinStorage[11] , tempJoinStorage[12] , tempJoinStorage[13] , tempJoinStorage[14] , tempJoinStorage[15]
                        );
                    else
                        useTimeStamp = true;
                    end
                end
                
                -- Needs to be reported immediately rather than waiting.
                if scanUpdate then
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].joined then
                        GRM.PrintLog ( 
                            {
                                index , 
                                select ( 2 , GRM.GetJoinOrRejoinString (
                                    tempJoinStorage[1] , tempJoinStorage[2] , tempJoinStorage[3] , tempJoinStorage[4] , tempJoinStorage[5] , tempJoinStorage[6] , tempJoinStorage[7] , tempJoinStorage[8] ,
                                    tempJoinStorage[9] , tempJoinStorage[10] , tempJoinStorage[11] , tempJoinStorage[12] , tempJoinStorage[13] , tempJoinStorage[14] , tempJoinStorage[15]
                                ) )
                            }
                        );
                    end
                    GRM.AddLog (
                        {
                            index , 
                            GRM.GetJoinOrRejoinString (
                                tempJoinStorage[1] , tempJoinStorage[2] , tempJoinStorage[3] , tempJoinStorage[4] , tempJoinStorage[5] , tempJoinStorage[6] , tempJoinStorage[7] , tempJoinStorage[8] ,
                                tempJoinStorage[9] , tempJoinStorage[10] , tempJoinStorage[11] , tempJoinStorage[12] , tempJoinStorage[13] , tempJoinStorage[14] , tempJoinStorage[15]
                            ), 
                            tempJoinStorage[1] , tempJoinStorage[2] , tempJoinStorage[3] , tempJoinStorage[4] , tempJoinStorage[5] , tempJoinStorage[6] , tempJoinStorage[7] , tempJoinStorage[8] ,
                            tempJoinStorage[9] , tempJoinStorage[10] , tempJoinStorage[11] , tempJoinStorage[12] , tempJoinStorage[13] , tempJoinStorage[14] , tempJoinStorage[15]
                        }
                    );
                end

                -- Adding timestamp to new Player.
                local noteIsSet = false;
                local officerNoteIsSet = false;
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].addTimestampToNote and useTimeStamp then
                    local index;

                    if member.rosterSelection == 0 then
                        for i = 1 , GRM.GetNumGuildies() do
                            local name = GetGuildRosterInfo ( i );
                            if name == member.name then
                                index = i;
                                break;
                            end
                        end
                    else
                        index = member.rosterSelection;
                    end

                    if index ~= nil then
                        local name ,_,_,_,_,_, note , oNote = GetGuildRosterInfo( member.rosterSelection );
                        if name == member.name then
                            local t;
                            if not tempJoinStorage[1] then
                                t = GRM.GetTimestamp();
                                t = string.sub ( t , 1 , string.find ( t , "'" ) + 2 );
                            else
                                t = string.sub ( timeStamp , 1 , string.find ( timeStamp , "'" ) + 2 );
                            end
                            t = GRM.FormatTimeStamp ( t , false );
                            local noteToSet = "";
                            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeTag then
                                noteToSet = GRM_G.customHeaderRejoin .. " " .. t;
                            else
                                noteToSet = t;
                            end

                            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 1 and  GRM.CanEditOfficerNote() and ( oNote == "" or oNote == nil ) then
                                officerNoteIsSet = true;
                                GuildRosterSetOfficerNote( member.rosterSelection , noteToSet );
                            elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 2 and CanEditPublicNote() and ( note == "" or note == nil ) then
                                noteIsSet = true;
                                GuildRosterSetPublicNote ( member.rosterSelection , noteToSet );
                            elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 3 then
                                GRM.SetJoinDateToCustomNote ( member.name , noteToSet );
                            end
                        end
                    end
                end
                
                -- Make sure to include the officer and public notes if necessary as well for new players
                -- Public note
                if noteIsSet and not scanUpdate then
                    -- Add the public note!
                    if member.note ~= "" then
                        GRM.AddNoteTempLogEntry ( GRM.GetClassifiedName ( member.name , true ) , "" , member.note , select ( 2 , GRM.GetTimestamp() ) );
                    end
                end

                -- Officer Note
                if officerNoteIsSet and not scanUpdate then
                    -- Add the Officer note!
                    if member.officerNote ~= "" then
                        player.officerNote = member.officerNote;
                    end
                end

                -- Make sure the namechange is adjusted in the database or you will get a double report
                GRM.AddMemberRecord( member , true , GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][GRM_G.guildName][formerMember] );
                
                -- Removing Player from LeftGuild History (Yes, they will be re-added upon leaving the guild.)
                GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][GRM_G.guildName][formerMember] = nil;
                player = GRM_GuildMemberHistory_Save[ GRM_G.F ][GRM_G.guildName][member.name];

                -- It must be done AFTER the record has been added
                -- Promotion Info
                if not scanUpdate and member.rankIndex < ( GuildControlGetNumRanks() - 1 ) then
                    -- Promotion Obtained since joining!
                    local timestamp2 , dateArray = GRM.GetTimestamp();
                    local epochTime = time();
                    local nameOfBaseRank = GuildControlGetRankName( GuildControlGetNumRanks() );
                    local added , logEntryMetaData = GRM.GetGuildEventString ( 2 , member.name , nameOfBaseRank , member.rankName );
                    
                    -- I don't want to have an instance where I have the promotion date in the log but the join date is too old os it has fallen off,
                    -- Thus, if the exact join date cannot be determined, it will set the promotion date to be no different.
                    if useTimeStamp then

                        -- Ok data is saved! Now let's report it to the log...
                        if added then
                            timestamp2 = logEntryMetaData[6][1];
                            epochTime = logEntryMetaData[6][2];
                        else
                            logEntryMetaData = { logEntryMetaData[1] , nil , simpleName , nameOfBaseRank , member.rankName , dateArray };
                        end

                        -- Clear if set to unknown
                        player.promoteDateUnknown = false;

                        -- For SYNC
                        if not added then
                            -- Use old stamps so as not to override other player data...
                            timestamp2 = "1 Jan '01 12:01am";
                            epochTime = 978375660;
                        else
                            player.verifiedPromoteDate[1] = timestamp2;
                            player.verifiedPromoteDate[2] = epochTime;
                        end

                        
                    end

                    player.rankName = member.rankName; -- Saving new rank Info
                    player.rankIndex = member.rankIndex; -- Saving new rank Index Info

                    if player.rankHistory[1][3] == 0 then
                        player.rankHistory[1][1] = player.rankName;
                        player.rankHistory[1][2] = string.sub ( timestamp2 , 1 , string.find ( timestamp2 , "'" ) + 2 )
                        player.rankHistory[1][3] = epochTime;
                    else
                        table.insert ( player.rankHistory , { player.rankName , string.sub ( timestamp2 , 1 , string.find ( timestamp2 , "'" ) + 2 ) , epochTime } ); -- New rank, date, metatimestamp
                    end

                    -- Ok data is saved! Now let's report it to the log...
                    if added then
                        GRM.AddPromotionDateTempLogEntry ( logEntryMetaData[1] , logEntryMetaData[2] , logEntryMetaData[3] , logEntryMetaData[4] , logEntryMetaData[5] , logEntryMetaData[6][3] );
                    else
                        GRM.AddPromotionDateTempLogEntry ( logEntryMetaData[1] , nil , simpleName , nameOfBaseRank , member.rankName , select ( 2 , GRM.GetTimestamp() ) );
                    end
                end            
                break;
            end
        end
    end

    -- Set the level if not joining.
    if not tempJoinStorage[5] then
        tempJoinStorage[6] = member.level;
    end
    
    return tempJoinStorage[5] , member , simpleName , tempTimeStamp , tempJoinStorage
end

-- Method:          GRM.RecordJoinChanges ( array , string , boolean , array , boolean )
-- What it Does:    Checks and records the new player changes... are they a returning player or completely new. Were they previously banned?
-- Purpose:         Keep the methods clean by compartmentalizing this rather lengthy function. It is also useful to not double the code as this will be called to on a live tracked event.
GRM.RecordJoinChanges = function ( member , simpleName , scanUpdate , dateArray , isVerified )
    -- Check against old member list first to see if returning player!
    local rejoin = false;
    -- Use default dates, since these are auto-tagged, you don't want your data to overwrite any others, so set it as OLD...
    local tempTimeStamp = "1 Jan '01 12:01am";
    local timeEpoch = 978375660;
    local added , logEntryMetaData = GRM.GetGuildEventString ( 4 , member.name , nil , nil , member.class ); -- For determining who did the invite.
    local currentTime = GRM.GetTimestamp();
    local tempJoinStorage = {};
    local player;

    if added then
        tempTimeStamp = logEntryMetaData[4][1];
        timeEpoch = logEntryMetaData[4][2];
        tempJoinStorage = { logEntryMetaData[1] , logEntryMetaData[2] , logEntryMetaData[3] , logEntryMetaData[4][3] };
    else
        tempJoinStorage = { false , nil , simpleName , dateArray };
    end
    
    if not added and scanUpdate then
        tempTimeStamp = currentTime;
        timeEpoch = time();
        added = true;
    end
    -- { isAdded , inviter (nil) , invitedPlayer , { day , month , year , hour } }
    rejoin , member , simpleName , tempTimeStamp , tempJoinStorage  = GRM.IsRejoinAndSetDetails ( member , simpleName , tempTimeStamp , scanUpdate , tempJoinStorage )
            
    if not rejoin then
        -- New Guildie. NOT a rejoin!
        local t;
        local timeStamp;
        if tempTimeStamp ~= "1 Jan '01 12:01am" and tempTimeStamp ~= ( "1 Jan '01 00:01" .. GRM.L ( "24HR_Notation" ) ) then
            timeStamp = tempTimeStamp;
        else
            timeStamp = currentTime;
        end

        if not added then
            t = currentTime;
            t = string.sub ( t , 1 , string.find ( t , "'" ) + 2 );
        else
            t = string.sub ( timeStamp , 1 , string.find ( timeStamp , "'" ) + 2 );
        end

        t = GRM.FormatTimeStamp ( t , false );
        local finalTStamp = "";
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeTag then
            finalTStamp = GRM_G.customHeaderJoin .. " " .. t;
        else
            finalTStamp = t;
        end
        
        -- Adding timestamp to new Player.
        local currentOfficerNote = tostring ( member.officerNote );
        local currentPublicNote = tostring ( member.note );
        local noteIsSet = false;
        local officerNoteIsSet = false;

        if added and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].addTimestampToNote and ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 1 or GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 2 ) then
            local name ,_,_,_,_,_, note , oNote = GetGuildRosterInfo ( member.rosterSelection );
            if name == member.name then
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 1 and GRM.CanEditOfficerNote() and ( oNote == "" or oNote == nil ) then
                    GuildRosterSetOfficerNote( member.rosterSelection , finalTStamp );
                    officerNoteIsSet = true
                    if scanUpdate then
                        member.officerNote = finalTStamp;
                    end
                elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 2 and CanEditPublicNote() and ( note == "" or note == nil ) then
                    GuildRosterSetPublicNote ( member.rosterSelection , finalTStamp );
                    noteIsSet = true;
                    if scanUpdate then
                        member.note = finalTStamp;
                    end
                end
                -- Of note, cannot do adding custom note yet here because that relies on them being in the database. Profile needs to be built first which happens in a moment
            end
        end

        -- Adding to global saved array, adding to report 
        GRM.AddMemberRecord ( member , false , nil );

        -- now we can add the custom note
        if added and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].addTimestampToNote and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 3 then
            GRM.SetJoinDateToCustomNote ( member.name , finalTStamp );
        end

        
        if not scanUpdate then
            GRM.AddJoinEntry ( tempJoinStorage[1] , tempJoinStorage[2] , tempJoinStorage[3] , tempJoinStorage[4] , tempJoinStorage[5] , tempJoinStorage[6] );
        else

            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].joined then
                GRM.PrintLog ( 
                    {
                        8 , 
                        select ( 2 , GRM.GetJoinOrRejoinString (
                            tempJoinStorage[1] , tempJoinStorage[2] , tempJoinStorage[3] , tempJoinStorage[4] , tempJoinStorage[5] , tempJoinStorage[6]
                        ) )
                    }
                );
            end
            GRM.AddLog (
                {
                    8 , 
                    GRM.GetJoinOrRejoinString (
                        tempJoinStorage[1] , tempJoinStorage[2] , tempJoinStorage[3] , tempJoinStorage[4] , tempJoinStorage[5] , tempJoinStorage[6]
                    ) ,
                    tempJoinStorage[1] , tempJoinStorage[2] , tempJoinStorage[3] , tempJoinStorage[4] , tempJoinStorage[5] , tempJoinStorage[6]
                } 
            ) 
        end

        player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][member.name];
        -- adding join date to history and rank date.
        if player ~= nil then

            -- Add the tempTimeStamp to officer note... this avoids report spam
            -- Promo Date stamp
            if tempTimeStamp ~= "1 Jan '01 12:01am" and tempTimeStamp ~= ( "1 Jan '01 00:01" .. GRM.L ( "24HR_Notation" ) ) then
                -- Clear if set to unknown
                player.promoteDateUnknown = false;
                
                -- Join Date stamp
                -- No need to check size of table, it will be the first index as the player data was just added.
                table.insert ( player.joinDate , timeStamp );
                table.insert ( player.joinDateEpoch , timeEpoch );

                -- For Event tracking!
                local date = GRM.ConvertGenericTimestampToIntValues ( string.sub ( timeStamp , 1 , string.find ( timeStamp , "'" ) + 2 ) );
                player.events[1][1][1] = date[1];
                player.events[1][1][2] = date[2];
                player.events[1][1][3] = date[3];
            else
                -- UNVERIFIED DATA!!!!
                local tStamp = GRM.GetTimestamp();

                -- Clear if set to unknown
                player.promoteDateUnknown = false;
                
                -- Join Date stamp
                -- No need to check size of table, it will be the first index as the player data was just added.
                table.insert ( player.joinDate , tStamp );
                table.insert ( player.joinDateEpoch , time() );

                -- For Event tracking!
                local date = GRM.ConvertGenericTimestampToIntValues ( string.sub ( tStamp , 1 , string.find ( tStamp , "'" ) + 2 ) );
                player.events[1][1][1] = date[1];
                player.events[1][1][2] = date[2];
                player.events[1][1][3] = date[3];
            end
            
            if added then 
                -- For SYNC
                -- Join Date
                player.verifiedJoinDate[1] = tempTimeStamp;
                player.verifiedJoinDate[2] = timeEpoch;
                -- Promo Date
                player.verifiedPromoteDate[1] = tempTimeStamp;
                player.verifiedPromoteDate[2] = timeEpoch;

                if player.rankHistory[1][3] == 0 then
                    player.rankHistory[1][1] = player.rankName;
                    player.rankHistory[1][2] = string.sub ( tempTimeStamp , 1 , string.find ( tempTimeStamp , "'" ) + 2 )
                    player.rankHistory[1][3] = timeEpoch;
                else
                    table.insert ( player.rankHistory , { player.rankName , string.sub ( tempTimeStamp , 1 , string.find ( tempTimeStamp , "'" ) + 2 ) , timeEpoch } ); -- New rank, date, metatimestamp
                end
            end

            if not scanUpdate and added then
                if currentOfficerNote == nil or currentOfficerNote == "" then
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].addTimestampToNote then
                        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 1 and GRM.CanEditOfficerNote() and ( player.officerNote == "" or player.officerNote == nil ) then
                            player.officerNote = finalTStamp;
                        elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 2 and CanEditPublicNote() and ( player.note == "" or player.note == nil ) then
                            player.note = finalTStamp;
                        elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 3 then
                            GRM.SetJoinDateToCustomNote ( member.name , finalTStamp );
                        end
                    end

                elseif currentOfficerNote ~= nil and currentOfficerNote ~= "" then
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].addTimestampToNote then
                        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 1 and GRM.CanEditOfficerNote() and ( player.officerNote == "" or player.officerNote == nil ) then
                            player.officerNote = currentOfficerNote;
                        elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 2 and CanEditPublicNote() and ( player.note == "" or player.note == nil ) then
                            player.note = currentPublicNote;
                        elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 3 then
                            GRM.SetJoinDateToCustomNote ( member.name , finalTStamp );
                        end
                    end
                end
            end

            -- Let's check the notes
            if added then
                if noteIsSet and not scanUpdate and member.note ~= "" then
                    -- Add the public note!
                    GRM.AddNoteTempLogEntry ( GRM.GetClassifiedName ( member.name , true ) , "" , member.note , select ( 2 , GRM.GetTimestamp() ) );

                end
                -- Officer Note
                if officerNoteIsSet and not scanUpdate and member.officerNote ~= "" then
                    -- Add the Officer note!
                    GRM.AddOfficerNoteTempLogEntry ( GRM.GetClassifiedName ( member.name , true ) , "" , member.officerNote , select ( 2 , GRM.GetTimestamp() ) );
                end
            end

            -- Promotion Info
            if member.rankIndex < ( GuildControlGetNumRanks() - 1 ) then
                -- Promotion Obtained since joining!
                local timestamp2 = currentTime;
                local epochTime = time();
                local nameOfBaseRank = GuildControlGetRankName( GuildControlGetNumRanks() );
                local added , logEntryMetaData = GRM.GetGuildEventString ( 2 , member.name , nameOfBaseRank , member.rankName );
                local rankHistoryIsGood = false;

                -- I don't want to have an instance where I have the promotion date in the log but the join date is too old os it has fallen off,
                -- Thus, if the exact join date cannot be determined, it will set the promotion date to be no different.
                if tempTimeStamp ~= "1 Jan '01 12:01am" and tempTimeStamp ~= ( "1 Jan '01 00:01" .. GRM.L ( "24HR_Notation" ) ) then
                    if added then
                        timestamp2 = logEntryMetaData[6][1];
                        epochTime = logEntryMetaData[6][2];
                        rankHistoryIsGood = true;
                    end

                    -- Clear if set to unknown
                    player.promoteDateUnknown = false;

                    -- For SYNC
                    if not added then
                        -- Use old stamps so as not to override other player data...
                        local time = "";
                        if not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].twentyFourHrScale then
                            time = "12:01am";
                        end
                        timestamp2 = "1 Jan '01 " .. time;
                        epochTime = 978375660;
                    else
                        player.verifiedPromoteDate[1] = timestamp2;
                        player.verifiedPromoteDate[2] = epochTime;

                        if player.rankHistory[1][3] == 0 and rankHistoryIsGood then
                            player.rankHistory[1][1] = member.rankName;
                            player.rankHistory[1][2] = string.sub ( timestamp2 , 1 , string.find ( timestamp2 , "'" ) + 2 )
                            player.rankHistory[1][3] = epochTime;
                        else

                            -- this applies if the previous date was unable to be verified so it set it to the current date, but the 2nd promo was, setting it to earlier date.
                            -- applying the new date appropriately
                            if player.rankHistory[#player.rankHistory][3] > epochTime then 
                                player.rankHistory[#player.rankHistory][3] = epochTime;
                                player.rankHistory[#player.rankHistory][2] = string.sub ( timestamp2 , 1 , string.find ( timestamp2 , "'" ) + 2 );
                            end

                            table.insert ( player.rankHistory , { player.rankName , string.sub ( timestamp2 , 1 , string.find ( timestamp2 , "'" ) + 2 ) , epochTime } ); -- New rank, date, metatimestamp
                        end

                    end

                end
                
                player.rankName = member.rankName; -- Saving new rank Info
                player.rankIndex = member.rankIndex; -- Saving new rank Index Info
                
                -- Ok data is saved! Now let's report it to the log...
                
                if logEntryMetaData[1] then
                    GRM.AddPromotionDateTempLogEntry ( logEntryMetaData[1] , logEntryMetaData[2] , logEntryMetaData[3] , logEntryMetaData[4] , logEntryMetaData[5] , logEntryMetaData[6][3] );
                else
                    GRM.AddPromotionDateTempLogEntry ( logEntryMetaData[1] , nil , GRM.GetClassifiedName ( member.name , true ) , nameOfBaseRank , member.rankName , dateArray );
                end
            end
        end
    end

    -- This happened live and is 100% verified
    if isVerified and GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][member.name] ~= nil then
        player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][member.name];

        player.verifiedJoinDate[1] = player.joinDate[#player.joinDate];
        player.verifiedJoinDate[2] = player.joinDateEpoch[#player.joinDateEpoch];
        player.verifiedPromoteDate[1] = tostring ( player.verifiedJoinDate[1] );   -- to disassociate 
        player.verifiedPromoteDate[2] = tonumber ( player.verifiedJoinDate[2] );   -- to disassociate 

    end

    -- Live detection it refreshes the logs, not on first scan check on logon.
    if scanUpdate then
        GRM.RefreshSelectHybridFrames ( true , true , true , true );
    end
end

-- Method:          GRM.RecordCustomNoteChanges ( string , string , string , boolean )
-- What it Does:    Reports the Custom note changes to the Guild Log
-- Purpose:         For ease of reporting to the log, as a UI feature.
GRM.RecordCustomNoteChanges = function( newNote , oldNote , editorName , editedName , rebuildLog )
    -- Remove the linebreaks in the log reporting or it will be spammy. Replace with a dash
    local nNote = string.gsub ( newNote , "\n" , "-" );
    local oNote = string.gsub ( oldNote , "\n" , "-" );

    local logReportWithTime , logReport = GRM.GetCustomNoteChangeString ( nNote , oNote , GRM.GetClassifiedName ( editorName , true ) , GRM.GetClassifiedName ( editedName , true ) , select ( 2 , GRM.GetTimestamp() ) );
    -- Ok that to the log...
    if rebuildLog and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].customNote then
        GRM.PrintLog ( { 19 , logReport } );
    end

    GRM.AddLog ( { 19 , logReportWithTime , nNote , oNote , GRM.GetClassifiedName ( editorName , true ) , GRM.GetClassifiedName ( editedName , true ) , select ( 2 , GRM.GetTimestamp() ) } );
    if rebuildLog and GRM_UI.GRM_RosterChangeLogFrame:IsVisible() and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toLog"].customNote then
        GRM_G.LogNumbersColorUpdate = true;
        GRM.BuildLogComplete( true , true );
    end

    if GRM_UI.GRM_MemberDetailMetaData:IsVisible() and editedName == GRM_G.currentName and GRM_UI.GRM_MemberDetailMetaData.GRM_ConfirmCustomNoteButton:IsVisible() then
        GRM_UI.GRM_MemberDetailMetaData.GRM_ConfirmCustomNoteButton:Hide();
        GRM_UI.GRM_MemberDetailMetaData.GRM_CancelCustomNoteButton:Hide();
        GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:SetText ( GRM_G.OriginalEditBoxValue );
        GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:ClearFocus();
        GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteTextCount:Hide();
        
        if GRM_G.currentName ~= GRM_G.addonUser then
            GRM_UI.GRM_MemberDetailMetaData.GRM_SafeFromRulesButton:Show();
            local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][ GRM_G.currentName ];
            if player ~= nil then
                if player.isOnline then
                    GRM.SetGroupInviteButton ( GRM_G.currentName );
                    GRM_UI.GRM_MemberDetailMetaData.GRM_GroupInviteButton:Show();
                end
            end
        end
        
        GRM.BuildCustomNoteScrollFrame ( GRM_G.OriginalEditBoxValue );
        if GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButton:IsVisible() ~= true then            -- Does not unpause if the date still needs to be selected or canceled.
            GRM_UI.Unpause();
        end
    end
end

-- Method:          GRM.SetCustomNote();
-- What it Does:    Modifies the custom note to new one, if necessary and sends proper updates and comms
-- Purpose:         Reduce a bit of code bloat. Make the UI functions of the editbox more readable.
GRM.SetCustomNote = function()
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName];
    if player ~= nil then
        -- The trim is so that just a white space doesn't somehow count as a new note.
        if GRM.Trim ( GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:GetText() ) ~= player.customNote[6] then
            local oldNote = player.customNote[6];
            player.customNote[2] = time();
            player.customNote[3] = GRM_G.addonUser;
            player.customNote[6] = GRM.Trim ( GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:GetText() );
            GRM_G.OriginalEditBoxValue = player.customNote[6];  -- This needs to be set to handle the OnEditFocusLost logic..
            -- Handle Log reporting logic here...

            GRM.RecordCustomNoteChanges ( player.customNote[6] , oldNote , GRM_G.addonUser , player.name , true )

            -- Handle live sync SendMessage here...
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankCustom and player.customNote[1] then
                local tempMsg = GRM_G.PatchDayString .. "?GRM_CNOTE?" .. GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankCustom .. "?" .. player.customNote[4] .. "?#"  .. player.name .. "?#" .. tostring ( player.customNote[2] );
                local customNote = "";
                if #player.customNote[6] > 0 then
                    customNote = player.customNote[6];
                else
                    customNote = "X&&X";
                end
                -- Check size... if need to split
                if ( #tempMsg + #customNote + GRMsyncGlobals.sizeModifier ) > 254 then
                    -- size is too big! let's split it!
                    GRMsync.SendMessage ( "GRM_SYNC" , GRM_G.PatchDayString .. "?GRM_CNOTE?" .. GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankCustom .. "?" .. string.sub ( customNote , #customNote - 49 ) .. "&X&" , "GUILD" );
                    -- now we send all the data in followup...
                    GRMsync.SendMessage ( "GRM_SYNC" , tempMsg .. "?#" .. string.sub ( customNote , 1 , #customNote - 50 ) .. "&X&" , "GUILD" );
                else
                    GRMsync.SendMessage ( "GRM_SYNC" , tempMsg  .. "?#" .. customNote , "GUILD" );
                end
            end
        end

        if GRM.Trim ( GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:GetText() ) == "" then
            GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:SetText ( GRM.L ( "Click here to set Custom Notes" ) );
            GRM_G.OriginalEditBoxValue = GRM.L ( "Click here to set Custom Notes" );
        else
            GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:SetText ( player.customNote[6] );
        end

    end
    GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:ClearFocus();
end

-- Method:          GRM.SetJoinDateToCustomNote ( string , string )
-- What it Does:    Checks to see if new note can merge with old custom note and be less than or equal to 150 characters. If not, it does nothing
-- Purpose:         To allow the player to auto update the join date to the custom note window.
GRM.SetJoinDateToCustomNote = function( playerName , noteToSet )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][playerName];

    if player == nil then
        player = GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ][playerName];
        if player == nil then
            return;
        end 
    end

    local newNote = noteToSet;
    local oldNote = player.customNote[6];
    local goodToSet = false;
    -- Check note size...
    if oldNote == "" then
        goodToSet = true;
    else
        if string.find ( oldNote , newNote , 1 , true ) == nil then  -- No need to merge if note already there
            newNote = newNote .. "\n" .. oldNote;   -- Ok, merged the notes...
            if #newNote <= GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:GetMaxLetters() then
                goodToSet = true;
            end
        end
    end
    -- Update it, report it!
    if goodToSet then
        player.customNote[2] = time();
        player.customNote[3] = playerName;
        player.customNote[6] = newNote;
        GRM.RecordCustomNoteChanges ( newNote , oldNote , GRM_G.addonUser , playerName , true )
        -- Update the note live!
        if GRM_G.currentName == playerName then
            GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:SetText ( newNote );
        end
    end
end

-- Method:          GRM.IsAnyCustomNoteLarge()
-- What it Does:    Returns true if at least one guildie as a custom note > 100
-- Purpose:         Adjust the throttle cap.
GRM.IsAnyCustomNoteLarge = function()
    local result = false;
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];

    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            if #player.customNote[6] > 99 then
                result = true;
                break;
            end
        end
    end
    return result;
end

-- Method:          GRM.IsAnyAltActive ( table , int)
-- What it Does:    Returns true if at least one of the alts is active
-- Purpose:         No need to announce inactive return if it is just an old alt...
GRM.IsAnyAltActive = function ( listOfAlts , hours )
    local result = false;
    local inactiveHours = hours or GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].inactiveHours;
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    local player;

    for i = 1 , #listOfAlts do
        player = guildData[listOfAlts[i][1]];
        if player ~= nil then
            if player.lastOnline < inactiveHours then
                result = true;
                break;
            end
        end
    end

    return result;
end

-- Method:          GRM.IsAnyAltActiveForRecommendKicks ( table , rule  )
-- What it Does:    Returns true if at least one of the alts is active
-- Purpose:         No need to announce inactive return if it is just an old alt...
GRM.IsAnyAltActiveForRecommendKicks = function ( listOfAlts , ruleName )
    local result = false;
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    local player;

    for i = 1 , #listOfAlts do
        player = guildData[listOfAlts[i][1]];
        if player ~= nil then
            if player.lastOnline < GRM_G.NumberOfHoursTilRecommend.kick[ruleName] then
                result = true;
                break;
            end
        end
    end

    return result;
end

-- Method           GRM.RecordChanges( int , table , table )
-- What it does:    Builds all the changes, sorts them, then adds them to change report
-- Purpose:         Consolidation of data for final output report.
GRM.RecordChanges = function ( indexOfInfo , member , memberOldInfo , logEntryMetaData , dateArray )
    if GRM_G.changeHappenedExitScan or not IsInGuild() then
        GRM.ResetTempLogs();
        GRM_G.changeHappenedExitScan = false;
        GRM_G.CurrentlyScanning = false;
        return;
    end
    local simpleName = "";
    if type ( member ) == "string" then
        simpleName = GRM.GetStringClassColorByName ( member ) .. GRM.SlimName ( member ) .. "|r";
    elseif indexOfInfo ~= 10 then
        simpleName = GRM.GetStringClassColorByName ( member.name ) .. GRM.SlimName ( member.name ) .. "|r";
    else
        simpleName = GRM.GetClassColorRGB ( member.class , true ) .. GRM.SlimName ( member.name ) .. "|r";
    end

    -- 2 = Guild Rank Promotion
    if indexOfInfo == 2 then
        local goodToAdd = true;
        if logEntryMetaData[1] then

            -- Ok, ranks are same name - does this mean the rank itself has shifted, or that there are ranks with the same name?
            -- Well, let's see if there are 2 ranks the same name..
            if logEntryMetaData[4] == logEntryMetaData[5] then
                local c = 0;
                for i = 1 ,  GuildControlGetNumRanks() do
                    if GuildControlGetRankName ( i ) == logEntryMetaData[4] then
                        c = c + 1;
                    end
                end

                if c == 1 then
                    goodToAdd = false;
                end
            end
            if goodToAdd then
                GRM.AddPromotionDateTempLogEntry ( logEntryMetaData[1] , logEntryMetaData[2] , logEntryMetaData[3] , logEntryMetaData[4] , logEntryMetaData[5] , logEntryMetaData[6][3] );
            end
        else
            if memberOldInfo.rankName == member.rankName then
                local c = 0;
                for i = 1 ,  GuildControlGetNumRanks() do
                    if GuildControlGetRankName ( i ) == memberOldInfo.rankName then
                        c = c + 1;
                    end
                end

                if c == 1 then
                    goodToAdd = false;      -- This means there aren't multiple of same rank
                end
            end


            if goodToAdd then
                GRM.AddPromotionDateTempLogEntry ( logEntryMetaData[1] , nil , simpleName , memberOldInfo.rankName , member.rankName , dateArray );
            end
        end

    -- 9 = Guild Rank Demotion
    elseif indexOfInfo == 9 then

        local goodToAdd = true;
        if logEntryMetaData[1] then

            -- Ok, ranks are same name - does this mean the rank itself has shifted, or that there are ranks with the same name?
            -- Well, let's see if there are 2 ranks the same name..
            if logEntryMetaData[4] == logEntryMetaData[5] then
                local c = 0;
                for i = 1 ,  GuildControlGetNumRanks() do
                    if GuildControlGetRankName ( i ) == logEntryMetaData[4] then
                        c = c + 1;
                    end
                end

                if c == 1 then
                    goodToAdd = false;
                end
            end

            if goodToAdd then
                GRM.AddDemotionDateTempLogEntry ( logEntryMetaData[1] , logEntryMetaData[2] , logEntryMetaData[3] , logEntryMetaData[4] , logEntryMetaData[5] , logEntryMetaData[6][3] );
            end
        else
            if memberOldInfo.rankName == member.rankName then
                local c = 0;
                for i = 1 ,  GuildControlGetNumRanks() do
                    if GuildControlGetRankName ( i ) == memberOldInfo.rankName then
                        c = c + 1;
                    end
                end

                if c == 1 then
                    goodToAdd = false;
                end
            end

            if goodToAdd then
                GRM.AddDemotionDateTempLogEntry ( logEntryMetaData[1] , nil , simpleName , memberOldInfo.rankName , member.rankName , dateArray );
            end
        end

    -- 4 = level
    elseif indexOfInfo == 4 then
        GRM.AddLeveledTempLogEntry ( simpleName , logEntryMetaData , member.level , ( member.level - memberOldInfo.level ) , dateArray , ( member.level == GRM_G.LvlCap ) , member.name );

    -- 5 = note
    elseif indexOfInfo == 5 then
        GRM.AddNoteTempLogEntry ( simpleName , memberOldInfo.note , member.note , dateArray )
    -- 6 = officerNote
    elseif indexOfInfo == 6 then
        GRM.AddOfficerNoteTempLogEntry ( simpleName , memberOldInfo.officerNote , member.officerNote , dateArray );

        -- 8 = Guild Rank Name Changed to something else
    elseif indexOfInfo == 8 then
        GRM.AddRankRenameEntry ( nil , memberOldInfo.rankName , member.rankName , dateArray );

    -- 10 = New Player
    elseif indexOfInfo == 10 then
        -- Check against old member list first to see if returning player!
        GRM.RecordJoinChanges ( member , simpleName , false , dateArray );

    -- 11 = Player Left  
    elseif indexOfInfo == 11 then
        table.insert ( GRM_G.TempLeftGuildPlaceholder , { member.name , simpleName , false , dateArray } );

    -- 12 = NameChanged
    elseif indexOfInfo == 12 then
        local colorHex = GRM.GetClassHex ( memberOldInfo.class );
        GRM.AddNameChangeTempLogEntry ( ( colorHex .. simpleName .. "|r" ) , ( colorHex .. GRM.SlimName (  memberOldInfo.name ) .. "|r" ) , dateArray );

    -- 13 = Inactive Members Return!
    elseif indexOfInfo == 13 then
        GRM.AddInactiveReturnTempLogEntry ( simpleName , GRM.HoursReport ( memberOldInfo ) , dateArray );
    end
end

-- Method:          GRM.AddonPlayerRankChange ( index )
-- What it Does:    If the player's rank is changed, it does a re-check on permissions and sets up sync again.
-- purpose:         Need to update personal details immediately so sync can be processed.
GRM.AddonPlayerRankChange = function( newRankIndex )

    local needsUpdate = false;
    if newRankIndex < GRM_G.playerRankID then
        needsUpdate = true;
    end

    GRM_G.playerRankID = newRankIndex;

    GRM_UI.BuildLogFrames();
    
    -- Determine if player has access to guild chat or is in restricted chat rank - need to recheck with rank change.
    GRM_G.HasAccessToOfficerChat = false;
    GRM.RegisterGuildChatPermission();

    if GRM_UI.GRM_ToolCoreFrame ~= nil and GRM_UI.GRM_ToolCoreFrame:IsVisible() then
        if needsUpdate then
            GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "Player rank change detected, re-checking permissions and rebuilding GRM Macro Tool." ) );
        end
        GRM_UI.RefreshManagementTool();
    end

    -- Let's do a resync check as well... If permissions have changed, we should resync check em.
    -- First, RESET all..
    if needsUpdate and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled and not GRMsyncGlobals.currentlySyncing and GRM_G.HasAccessToGuildChat and not GRM_G.OnFirstLoad then
        if GRMsync.IsPlayerDataSyncCompatibleWithAnyOnline() then
            GRMsync.TriggerFullReset();
            GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "Your rank has changed. Re-evaluating sync permissions." ) );
            C_Timer.After ( 3 , GRMsync.Initialize ); -- Now, let's add a brief delay, 3 seconds, to trigger sync again
        end
    end
end

-- Method:          GRM.CheckPlayerChanges ( array )
-- What it Does:    Scans through guild roster and re-checks for any  (Will only fire if guild is found!)
-- Purpose:         Keep whoever uses the addon in the know instantly of what is going and changing in the guild.
GRM.CheckPlayerChanges = function ( roster )

    -- Extra logic to prevent dupliated scans
    if not ( GRM_G.ManualScanEnabled and not GRM_UI.GRM_ToolCoreFrame.MacroSuccess ) and ( ( GRM_G.CurrentlyScanning ) or ( ( time() - GRM_G.ScanRosterTimer ) < GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].scanDelay ) ) then
        return;
    else
        GRM_G.ScanRosterTimer = time();
    end

    if not GRM_G.CurrentlyScanning then
        GRM_G.CurrentlyScanning = true;
    else
        return;
    end
    if GRM_G.changeHappenedExitScan or not IsInGuild() then    -- This provides an escape if the player quits the guild in the middle of the scan process, or on first joining, to avoid lua error then
        GRM.ResetTempLogs();
        GRM_G.changeHappenedExitScan = false;
        GRM_G.CurrentlyScanning = false;
        return;
    end

    local newPlayerFound;
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    local player;

    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].scanEnabled or GRM_G.ManualScanEnabled then

        for x in pairs ( roster ) do
            newPlayerFound = true;
            player = guildData[x];

            if player ~= nil then
                newPlayerFound = false;
                -- No need to do any of this if they haven't logged in... the only potential change necessary to check would be public/officer note as that is the only thing that can change when offline.
                -- This boolean is all about massive resource saving
                -- if online, or hours since last logged is different or public note is different or officer note is different and you can read it.
                if roster[x].isOnline or roster[x].rankName ~= player.rankName or roster[x].rankIndex ~= player.rankIndex or roster[x].level ~= player.level or roster[x].note ~= player.note or roster[x].class ~= player.class or ( roster[x].officerNote ~= player.officerNote and GRM.CanViewOfficerNote() ) then

                    for k = 2 , 7 do
                        if k < 7 then -- CHANGE FOUND! New info and old info are not equal!
                            -- Ranks
                            if k == 3 and roster[x].rankIndex ~= player.rankIndex then -- This checks to see if guild just changed the name of a rank.

                                local added = false;
                                local timestamp , dateArray = GRM.GetTimestamp();
                                local epochTime = time();
                                local logEntryroster = {};

                                if roster[x].rankName ~= player.rankName or ( roster[x].rankName == player.rankName and not GRM_G.numRanksHasChanged ) then
                                    -- Promotion Obtained                                
                                    if roster[x].rankIndex < player.rankIndex then

                                        added , logEntryroster = GRM.GetGuildEventString ( 2 , roster[x].name , player.rankName , roster[x].rankName );
                                        if added then
                                            timestamp = logEntryroster[6][1];
                                            epochTime = logEntryroster[6][2];
                                            dateArray = logEntryroster[6][3];
                                        end
                                        GRM.RecordChanges ( 2 , roster[x] , player , logEntryroster , dateArray );
                                        
                                    -- Demotion Obtained
                                    elseif roster[x].rankIndex > player.rankIndex then
                                        added , logEntryroster = GRM.GetGuildEventString ( 1 , roster[x].name , player.rankName , roster[x].rankName );
                                        if added then
                                            timestamp = logEntryroster[6][1];
                                            epochTime = logEntryroster[6][2];
                                            dateArray = logEntryroster[6][3];
                                        end
                                        
                                        if roster[x].rankName ~= player.rankName then
                                            GRM.RecordChanges ( 9 , roster[x] , player , logEntryroster , dateArray );
                                        end
                                    end
                                    
                                    player.rankName = roster[x].rankName; -- Saving new rank Info
                                    player.rankIndex = roster[x].rankIndex; -- Saving new rank Index Info

                                    -- Clear if set to unknown
                                    player.promoteDateUnknown = false;

                                    -- For SYNC
                                    if added then
                                        player.verifiedPromoteDate[1] = timestamp;
                                        player.verifiedPromoteDate[2] = epochTime;
                                    end
                                    

                                    if roster[x].rankName ~= player.rankName then
                                        if player.rankHistory[1][3] == 0 then
                                            player.rankHistory[1][1] = player.rankName;
                                            player.rankHistory[1][2] = string.sub ( timestamp , 1 , string.find ( timestamp , "'" ) + 2 )
                                            player.rankHistory[1][3] = epochTime;
                                        else
                                            table.insert ( player.rankHistory , { player.rankName , string.sub ( timestamp , 1 , string.find ( timestamp , "'" ) + 2 ) , epochTime } ); -- New rank, date, metatimestamp
                                        end
                                    else
                                        player.rankHistory[#player.rankHistory][1] = player.rankName;
                                        player.rankHistory[#player.rankHistory][2] = string.sub ( timestamp , 1 , string.find ( timestamp , "'" ) + 2 );
                                        player.rankHistory[#player.rankHistory][3] = epochTime;
                                    end
                                    
                                    -- Update the player index if it is the player themselves that received the change in rank.
                                    if roster[x].name == GRM_G.addonUser then

                                        GRM.AddonPlayerRankChange ( roster[x].rankIndex );
                                        
                                    end

                                -- Adjust new rank index - make rank change good.
                                elseif GRM_G.numRanksHasChanged and roster[x].rankName == player.rankName then
                                    player.rankIndex = roster[x].rankIndex; -- Saving new rank Index Info
                                end

                            elseif k == 2 and roster[x].rankName ~= player.rankName and roster[x].rankIndex == player.rankIndex then

                                player.rankName = roster[x].rankName; -- Saving new Info
                                if player.rankHistory[1][3] > 0 then
                                    player.rankHistory[#player.rankHistory][1] = roster[x].rankName;   -- Adjusting the historical name if 
                                    -- We are just renaming the rank in the rankHistory - nothing else.
                                end

                            -- Level
                            elseif k == 4 and roster[x].level ~= player.level then
                                if roster[x].level > player.level and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].recordLevelUp then
                                    local needsToRecord = false;
                                    local mileStones = { 10 , 20 , 30 , 40 , 50 };
                                    local level = 0;

                                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].levelReportMin <= roster[x].level then
                                        needsToRecord = true
                                    else
                                        for i = ( #mileStones ) , 1 , -1 do
                                            if roster[x].level >= mileStones[i] and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].levelFilters[i] and player.level < mileStones[i] then
                                                needsToRecord = true;
                                                level = mileStones[i];
                                                break;
                                            end
                                        end
                                    end
                                    if needsToRecord then
                                        GRM.RecordChanges ( 4 , roster[x] , player , level , select ( 2 , GRM.GetTimestamp() ) );
                                    end
                                end
                                player.level = roster[x].level; -- Saving new Info

                            -- Note
                            elseif k == 5 then
                                -- Let's first compare the notes... make sure they are not the same
                                local isDifferent = true;
                                local newNote , oldNote = "" , "";

                                if GetCVar("profanityFilter") == "1" then                               -- The profanity filter is on!
                                    newNote = GRM.NormalizeMatureWords ( roster[x].note );
                                    oldNote = GRM.NormalizeMatureWords ( player.note );
                                else
                                    newNote = roster[x].note;
                                    oldNote = player.note;
                                end

                                if newNote == oldNote then
                                    isDifferent = false;
                                end

                                if isDifferent then
                                    GRM.RecordChanges ( 5 , roster[x] , player , nil , select ( 2 , GRM.GetTimestamp() ) );
                                    player.note = roster[x].note;
                                    -- Update metaframe
                                    if GRM_UI.GRM_MemberDetailroster ~= nil and GRM_UI.GRM_MemberDetailroster:IsVisible() and GRM_G.currentName == roster[x].name then
                                        if roster[x].note == "" then
                                            if ( CanEditPublicNote() or ( GRM_G.BuildVersion >= 80000 and GRM_G.currentName == GRM_G.addonUser ) ) then
                                                GRM_UI.GRM_MemberDetailroster[x].GRM_noteFontString1:SetText ( GRM.L ( "Click here to set a Public Note" ) );
                                            else
                                                GRM_UI.GRM_MemberDetailroster[x].GRM_noteFontString1:SetText ( GRM.L ( "Unable to Edit Public Note at Rank" ) );
                                            end
                                        else
                                            GRM_UI.GRM_MemberDetailroster[x].GRM_noteFontString1:SetText ( roster[x].note );
                                        end
                                        GRM_UI.GRM_MemberDetailroster[x].GRM_PlayerNoteEditBox:SetText ( roster[x].note );
                                    end
                                end

                            -- Officer Note
                            elseif k == 6 and GRM.CanViewOfficerNote() then
                                if roster[x].officerNote == nil or player.officerNote == nil then
                                    player.officerNote = roster[x].officerNote;
                                else
                                    local isDifferent = true;
                                    local newNote , oldNote = "" , "";

                                    if GetCVar("profanityFilter") == "1" then                               -- The profanity filter is on!
                                        newNote = GRM.NormalizeMatureWords ( roster[x].officerNote );
                                        oldNote = GRM.NormalizeMatureWords ( player.officerNote );
                                    else
                                        newNote = roster[x].officerNote;
                                        oldNote = player.officerNote;
                                    end

                                    if newNote == oldNote then
                                        isDifferent = false;
                                    end

                                    if isDifferent then
                                        -- Guild Leader! Let's check their officer note for changes for settings!
                                        GRM.RecordChanges ( 6 , roster[x] , player , nil , select ( 2 , GRM.GetTimestamp() ) );
                                        player.officerNote = roster[x].officerNote;
                                    end
                                end
                                if GRM_UI.GRM_MemberDetailroster ~= nil and GRM_UI.GRM_MemberDetailroster:IsVisible() and GRM_G.currentName == roster[x].name then
                                    if roster[x].officerNote == "" then
                                        if GRM.CanEditOfficerNote() then
                                            GRM_UI.GRM_MemberDetailroster[x].GRM_noteFontString2:SetText ( GRM.L ( "Click here to set an Officer's Note" ) );
                                        else
                                            GRM_UI.GRM_MemberDetailroster[x].GRM_noteFontString2:SetText ( GRM.L ( "Unable to Edit Officer Note at Rank" ) );
                                        end
                                    else
                                        GRM_UI.GRM_MemberDetailroster[x].GRM_noteFontString2:SetText ( roster[x].officerNote );
                                    end
                                    GRM_UI.GRM_MemberDetailroster[x].GRM_PlayerOfficerNoteEditBox:SetText (  roster[x].officerNote );
                                end
                            end

                        -- Class Change - fix error or future class change redundancy if they ever implemented
                        elseif k == 7 and roster[x].class ~= player.class then
                            player.class = roster[x].class;

                            -- TO DO IF EVER IMPLEMENTED
                            -- UPDATE THE ALT INFO CLASS AS WELL IF NEED TO CHANGE.
                        end
                    end
                end

                -- Recommendations for Offline Kicks
                if roster[x].lastOnline ~= -1 then

                    -- Report inactive returns
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].reportInactiveReturn and player.lastOnline > GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].inactiveHours and roster[x].lastOnline < GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].inactiveHours and player.lastOnline > roster[x].lastOnline then  -- Player has logged in after having been inactive for greater than given time
                        local needsToReport = true;

                        -- No need to report if any player's alts are active still...
                        if #player.alts > 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].allAltRequirement then
                            if GRM.IsAnyAltActive ( player.alts ) then
                                needsToReport = false;
                            end
                        end

                        if needsToReport then
                            GRM.RecordChanges ( 13 , roster[x].name , ( player.lastOnline - roster[x].lastOnline ) , nil , select ( 2 , GRM.GetTimestamp() ) );   -- Recording the change in hours to log
                        end
                    end

                    player.lastOnline = roster[x].lastOnline;                   -- Set new hours since last login.
                end

                -- Just straight update these everytime... No need for change check
                if ( roster[x].isOnline and player.zone ~= roster[x].zone ) or GRM_G.OnFirstLoad then
                    player.timeEnteredZone = time();   -- Resetting the time on hitting this zone.
                end
                player.zone = roster[x].zone;                               -- zone
                player.achievementPoints = roster[x].achievementPoints;     -- Achievement pts
                player.isMobile = roster[x].isMobile;                       -- isMobile
                player.guildRep = roster[x].rep;                            -- Guild Reputation
                player.isOnline = roster[x].isOnline;                       -- online Status
                player.status = roster[x].status;                           -- Active Status
                player.GUID = roster[x].GUID;                               -- GUID

                if roster[x].race ~= "" then
                    player.race = roster[x].race;    -- Race
                end
                if roster[x].sex ~= "" then
                    player.sex = roster[x].sex;    -- Sex
                end
            end

            -- NEW PLAYER FOUND! (Maybe)
            if newPlayerFound then
                table.insert ( GRM_G.newPlayers , roster[x] );
            end
        end

        -- Checking if any players left the guild
        C_Timer.After ( 1 , function()
            if GRM_G.changeHappenedExitScan or not IsInGuild() then
                GRM.ResetTempLogs();
                GRM_G.changeHappenedExitScan = false;
                GRM_G.CurrentlyScanning = false;
                return;
            end

            -- Scan the roster for missing names...
            for _ , member in pairs ( guildData ) do
                if type ( member ) == "table" then
                    if roster[member.name] == nil then         -- It exists in current database, but the new one it is missing, thus implying player is no longer in the guild.
                        table.insert ( GRM_G.leavingPlayers , member );
                    end
                end
            end

            -- Spread out the scans to avoid stutter...
            C_Timer.After ( 1 , function()
                if GRM_G.changeHappenedExitScan or not IsInGuild() then
                    GRM.ResetTempLogs();
                    GRM_G.changeHappenedExitScan = false;
                    GRM_G.CurrentlyScanning = false;
                    return;
                end

                -- Final check on players that left the guild to see if they are namechanges.GRM.CanViewOfficerNote
                local playerNotMatched = true;
                if #GRM_G.leavingPlayers > 0 and #GRM_G.newPlayers > 0 then
                    for k = 1 , #GRM_G.leavingPlayers do
                        playerNotMatched = true;
                        for j = 1 , #GRM_G.newPlayers do
                            if ( GRM_G.leavingPlayers[k] ~= nil and GRM_G.newPlayers[j] ~= nil ) and GRM_G.leavingPlayers[k].GUID == GRM_G.newPlayers[j].GUID then   -- COMPARING GUID
                                playerNotMatched = false;   -- In other words, player was found, but it's a namechange!!!!!
                                -- Match Found!!!
                                GRM.RecordChanges ( 12 , GRM_G.newPlayers[j] , GRM_G.leavingPlayers[k] , nil , select ( 2 , GRM.GetTimestamp() ) );

                                -- We need to change the name EVERYWHERE!!!
                                local tempGuild = guildData;
                                for member in pairs ( tempGuild ) do
                                    if type ( tempGuild[member] ) == "table" then
                                        if GRM_G.leavingPlayers[k].GUID == tempGuild[member].GUID then -- Mathching the Leaving player to historical index so it can be identified and new name stored.
                                            -- Need to remove him from list of alts IF he has a lot of alts...
                                            if #tempGuild[member].alts > 0 then

                                                local listOfAlts = tempGuild[member].alts;                      -- Found the match, now grabbing list of all alts
                                                local tempAlt;
                                                for m = 1 , #listOfAlts do                                      -- Cycling through full list
                                                    tempAlt = tempGuild[listOfAlts[m][1]];
                                                    if tempAlt ~= nil then
                                                        for t = 1 , #tempAlt.alts do
                                                            if tempAlt.alts[t][1] == GRM_G.leavingPlayers[k].name then
                                                                tempAlt.alts[t][1] = GRM_G.newPlayers[j].name;
                                                                break;
                                                            end
                                                        end
                                                    end
                                                end
                                            end

                                            -- Need to change the name of the players too
                                            for s in pairs ( tempGuild ) do
                                                if type ( tempGuild[s] ) == "table" then
                                                    if #tempGuild[s].removedAlts > 0 then
                                                        for t = 1 , #tempGuild[s].removedAlts do
                                                            if tempGuild[s].removedAlts[t][1] == GRM_G.leavingPlayers[k].name then
                                                                tempGuild[s].removedAlts[t][1] = GRM_G.newPlayers[j].name;
                                                                break;
                                                            end
                                                        end
                                                    end
                                                end
                                            end

                                            -- Group Info module -- update player Name change as well
                                            if GRM_GroupInfo_Save ~= nil then
                                                GRM_GroupInfo_Save[GRM_G.newPlayers[j].name] = {};
                                                GRM_GroupInfo_Save[GRM_G.newPlayers[j].name] = GRM.DeepCopyArray ( GRM_GroupInfo_Save[GRM_G.leavingPlayers[k].name] );
                                                GRM_GroupInfo_Save[GRM_G.leavingPlayers[k].name] = nil;
                                            end

                                            tempGuild[GRM_G.newPlayers[j].name] = {};
                                            tempGuild[GRM_G.newPlayers[j].name] = GRM.DeepCopyArray ( tempGuild[GRM_G.leavingPlayers[k].name] );
                                            tempGuild[GRM_G.newPlayers[j].name].name = GRM_G.newPlayers[j].name;            -- Need to update this new name too!
                                            tempGuild[GRM_G.leavingPlayers[k].name] = nil;
                                            break
                                        end
                                    end
                                end
                                -- since namechange identified, also need to remove name from GRM_G.newPlayers array now.
                                if #GRM_G.newPlayers == 1 then
                                    GRM_G.newPlayers = {}; -- Clears the array of the one name.
                                else
                                    table.remove ( GRM_G.newPlayers , j );
                                end
                                guildData = tempGuild;  -- redundant?
                                break;
                            end
                        end
                        
                        -- Player not matched! For sure this player has left the guild!
                        if playerNotMatched then
                            GRM.RecordChanges ( 11 , GRM_G.leavingPlayers[k] , GRM_G.leavingPlayers[k] , nil , select ( 2 , GRM.GetTimestamp() ) );
                        end
                    end
                elseif #GRM_G.leavingPlayers > 0 then
                    for k = 1 , #GRM_G.leavingPlayers do
                        GRM.RecordChanges ( 11 , GRM_G.leavingPlayers[k] , GRM_G.leavingPlayers[k] , nil , select ( 2 , GRM.GetTimestamp() ) );
                    end
                end
                if #GRM_G.newPlayers > 0 then
                    for k = 1 , #GRM_G.newPlayers do
                        GRM.RecordChanges ( 10 , GRM_G.newPlayers[k] , GRM_G.newPlayers[k] , nil , select ( 2 , GRM.GetTimestamp() ) );
                    end
                end

                -- Now that we have collected all the players to be kicked... Let's not spam the log with alt info by parsing it properly.
                if #GRM_G.TempLeftGuildPlaceholder > 0 then
                    for k = 1 , #GRM_G.TempLeftGuildPlaceholder do
                        GRM.AddLeftOrKickEntry ( GRM.RecordKickChanges ( GRM_G.TempLeftGuildPlaceholder[k][1] , GRM_G.TempLeftGuildPlaceholder[k][3] , GRM_G.TempLeftGuildPlaceholder[k][4] ) );
                    end
                end

                -- OK, let's close this out!!!!!
                C_Timer.After ( 1 , function()
                    -- Seeing if any upcoming notable events, like anniversaries/birthdays
                    GRM.CheckPlayerEvents();

                    -- Printing Report, and sending report to log.
                    GRM.FinalReport();

                    -- Disable manual scan if activated.
                    if GRM_G.ManualScanEnabled and GRM_UI.GRM_ToolCoreFrame.MacroSuccess then
                        GRM_G.ManualScanEnabled = false;
                        GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "Manual Scan Complete" ) , 1.0 , 0.84 , 0 );
                    end

                    if GRM_G.ManualScanEnabled and not GRM_UI.GRM_ToolCoreFrame.MacroSuccess then
                        GRM_G.ManualScanEnabled = false;
                        GRM.ValidateMacroRecordingSuccess ( true );
                    end

                end);
            end);
        end);
    else
        -- Seeing if any upcoming notable events, like anniversaries/birthdays
        GRM.CheckPlayerEvents();

        -- Printing Report, and sending report to log.
        GRM.FinalReport();

        -- Disable manual scan if activated.
        if GRM_G.ManualScanEnabled and GRM_UI.GRM_ToolCoreFrame.MacroSuccess then
            GRM_G.ManualScanEnabled = false;
            GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "Manual Scan Complete" ) , 1.0 , 0.84 , 0 );
        end
    end
end

-- Method:          GRM.GuildNameChanged( string )
-- What it Does:    Returns true if the player's guild is the same, it just changed its name
-- Purpose:         Good to know... what a pain it would be if you had to reset all of your settings
GRM.GuildNameChanged = function ( currentGuildName )
    local result = false;

        -- If the guild name is found in database but club IDs do not match, then it can be determined that this is likely a namechange, but it is coming from a guild someone was just holding on to.
    if GRM_GuildMemberHistory_Save[ GRM_G.F ][currentGuildName] == nil or ( GRM_GuildMemberHistory_Save[ GRM_G.F ][currentGuildName] ~= nil and GRM_GuildMemberHistory_Save[ GRM_G.F ][currentGuildName].grmClubID ~= GRM_G.gClubID ) then
    -- For each guild
        for guild in pairs ( GRM_GuildMemberHistory_Save[ GRM_G.F ] ) do
            if GRM_GuildMemberHistory_Save[ GRM_G.F ][guild].grmClubID == GRM_G.gClubID then
                result = true;

                GRM_PlayersThatLeftHistory_Save[GRM_G.F][currentGuildName] = {};
                GRM_PlayersThatLeftHistory_Save[GRM_G.F][currentGuildName] = GRM.DeepCopyArray ( GRM_PlayersThatLeftHistory_Save[GRM_G.F][guild] );
                GRM_PlayersThatLeftHistory_Save[GRM_G.F][guild] = nil;

                GRM_GuildMemberHistory_Save[GRM_G.F][currentGuildName] = {};
                GRM_GuildMemberHistory_Save[GRM_G.F][currentGuildName] = GRM.DeepCopyArray ( GRM_GuildMemberHistory_Save[GRM_G.F][guild] );
                GRM_GuildMemberHistory_Save[GRM_G.F][currentGuildName].grmName = currentGuildName;
                GRM_GuildMemberHistory_Save[GRM_G.F][guild] = nil;

                GRM_PlayerListOfAlts_Save[GRM_G.F][currentGuildName] = {};
                GRM_PlayerListOfAlts_Save[GRM_G.F][currentGuildName] = GRM.DeepCopyArray ( GRM_PlayerListOfAlts_Save[GRM_G.F][guild] );
                GRM_PlayerListOfAlts_Save[GRM_G.F][guild] = nil;

                GRM_LogReport_Save[GRM_G.F][currentGuildName] = {};
                GRM_LogReport_Save[GRM_G.F][currentGuildName] = GRM.DeepCopyArray ( GRM_LogReport_Save[GRM_G.F][guild] );
                GRM_LogReport_Save[GRM_G.F][guild] = nil;

                GRM_CalendarAddQue_Save[GRM_G.F][currentGuildName] = {};
                GRM_CalendarAddQue_Save[GRM_G.F][currentGuildName] = GRM.DeepCopyArray ( GRM_CalendarAddQue_Save[GRM_G.F][guild] );
                GRM_CalendarAddQue_Save[GRM_G.F][guild] = nil;

                -- Also need to change the guild's name in the saved database...
                for x in pairs ( GRM_GuildDataBackup_Save[GRM_G.F][guild] ) do
                    if type ( x ) == "string" then
                        if #GRM_GuildDataBackup_Save[GRM_G.F][guild][x] > 0 then
                            GRM_GuildDataBackup_Save[GRM_G.F][guild][x]["members"].grmName = currentGuildName;
                            GRM_GuildDataBackup_Save[GRM_G.F][guild][x]["formerMembers"].grmName = currentGuildName;
                        end
                    end
                end

                GRM_GuildDataBackup_Save[GRM_G.F][currentGuildName] = {};
                GRM_GuildDataBackup_Save[GRM_G.F][currentGuildName] = GRM.DeepCopyArray ( GRM_GuildDataBackup_Save[GRM_G.F][guild] );
                GRM_GuildDataBackup_Save[GRM_G.F][guild] = nil;
                break;
            end
        end
    end
    return result;
end

-- Method:          GRM_UI.ValidateUnverifiedDate ( table )
-- What it Does:    Checks if formatting is valid, then if not it fixes it
-- Purpose:         Cleanup some old formatting bugs with old data. Added redundency as well.
GRM.ValidateUnverifiedDate = function( player , dateVersion )

    -- If general formatting error, replace.
    if not player.rankHistory or #player.rankHistory == 0 or player.rankHistory[1][3] == 0 then
        player.rankHistory = { {  "" , "" , 0 } };
    end

    if not player[dateVersion] or #player[dateVersion] == 0 then
        player[dateVersion] = { "" , 0 };
    end

    local toRemove = false;

    for i = #player.rankHistory , 1 , -1 do

        -- add generic table if necessary
        if not type (player.rankHistory[i] ) == "table" then
            player.rankHistory[i] = {  "" , "" , 0 };

        elseif ( not player.rankHistory[i][1] or type ( player.rankHistory[i][1] ) ~= "string" ) or ( not player.rankHistory[i][2] or type ( player.rankHistory[i][2] ) ~= "string" ) or ( not player.rankHistory[i][3] or type ( player.rankHistory[i][1] ) ~= "number" ) then

            if i == #player.rankHistory then

                if player[dateVersion][1] ~= "" then
                    player.rankHistory[i][1] = player.rankName;
                    player.rankHistory[i][2] = GRM.GetCleanTimestamp ( player[dateVersion][1] );
                    player.rankHistory[i][3] = GRM.TimeStampToEpoch ( player.rankHistory[i][2] , true );
                end
            else
                -- if one is errored they all are.
                toRemove = false;

                -- We must remov
                if ( not player.rankHistory[i][1] or type ( player.rankHistory[i][1] ) ~= "string" ) or ( not player.rankHistory[i][2] or type ( player.rankHistory[i][2] ) ~= "string" ) then
                    toRemove = true;

                elseif ( not player.rankHistory[i][3] or type ( player.rankHistory[i][3] ) ~= "number" ) then

                    if player.rankHistory[i][2] ~= "" then
                        player.rankHistory[i][3] = GRM.TimeStampToEpoch ( GRM.GetCleanTimestamp ( player.rankHistory[i][2] ) , true );
                    else
                        toRemove = true;
                    end

                end

                if toRemove then
                    table.remove ( player.rankHistory , i );
                end
            end 
        end
    end

    return player;
end

-- Method:          GRM.ParseGuildRanks()
-- What it Does:    Parses the string with al the guild ranks and returns it as an array
-- Purpose:         Due to some tables scanning logic I want to keep this stored as a string and then converted to a table during your session or re-converted if the ranks are updated.
GRM.ParseGuildRanks = function()
    local ranks = {};

    for rankName in string.gmatch ( GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ].ranks , "[^||]+" ) do
        table.insert ( ranks , rankName );
    end
    return ranks;
end

-- Method:          GRM.CheckGuildRanks()
-- What it Does:    Checks for any changes in the guild rank structure of the guild and reports on them
-- Purpose:         Just extra info, especially to help make it more clear to the player why they might get spammed in their log for mass demote/promotions
GRM.CheckGuildRanks = function()
    -- If the ranks are set, let's check if they do not match now.
    local numRanks = GuildControlGetNumRanks();
    GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ].ranks = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ].ranks or GRM.GetListOfGuildRanks ( true , true , true );
    
    GRM_G.guildRankNames = GRM_G.guildRankNames or GRM.ParseGuildRanks();

    if GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ].grmNumRanks == nil or GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ].grmNumRanks == 0 then
        GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ].grmNumRanks = numRanks or 0;
    else
        if numRanks ~= GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ].grmNumRanks then
            
            GRM.AddRankRenameEntry ( ( numRanks - GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ].grmNumRanks ) , nil , nil , select ( 2 , GRM.GetTimestamp() ) );
            GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ].grmNumRanks = numRanks;
            GRM_G.numRanksHasChanged = true;

        else
            
            local rankNames = GRM.GetListOfGuildRanks ( true , true );
            local changeMade = false;

            for i = 1 , #rankNames do
                if rankNames[i] ~= GRM_G.guildRankNames[i] then
                    changeMade = true;
                    GRM.AddRankRenameEntry ( nil , GRM_G.guildRankNames[i] , rankNames[i] , select ( 2 , GRM.GetTimestamp() ) );
                end
            end

            if changeMade then
                GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ].ranks = GRM.GetListOfGuildRanks ( true , true , true );
                GRM_G.guildRankNames = GRM.ParseGuildRanks();
            end
        end
    end        
end

-- Method:          GRM.BuildNewRoster()
-- What it does:    Rebuilds the roster to check against for any changes.
-- Purpose:         To track for guild changes of course!
GRM.BuildNewRoster = function()
    local roster = {};
    -- Checking if Guild Found or Not Found, to pre-check for Guild name tag.
    local guildNotFound = false;
    if not GRM_GuildMemberHistory_Save[GRM_G.F][GRM_G.guildName] then
        guildNotFound = true;
    end

    local atLeastOne = false;

    for i = 1 , GRM.GetNumGuildies() do
        -- For guild info
        local name , rank , rankInd , level , _ , zone , note , officerNote , online , status , class , achievementPoints , _ , isMobile , _ , rep , GUID = GetGuildRosterInfo ( i );

        roster[name] = {};      -- For easy referencing.
        roster[name].name = name                                    -- 1
        roster[name].rankName = rank;                               -- 2
        roster[name].rankIndex = rankInd;                           -- 3
        roster[name].level = level;                                 -- 4
        roster[name].note = note;                                   -- 5
        if GRM.CanViewOfficerNote() then -- Officer Note permission to view.
            roster[name].officerNote = officerNote;                 -- 6
        else
            roster[name].officerNote = nil; -- Set Officer note to nil if needed due to player not being able to view. - If it is set to "" then player will think it is changing.
        end
        roster[name].class = class;                                 -- 7
        roster[name].lastOnline = GRM.GetHoursSinceLastOnline ( i , online ); -- 8 Time since they last logged in in hours.
        roster[name].zone = zone;                                   -- 9
        roster[name].achievementPoints = achievementPoints;         -- 10
        roster[name].isMobile = isMobile;                           -- 11
        roster[name].rep = rep;                                     -- 12
        roster[name].isOnline = online;                             -- 13
        roster[name].status = status;                               -- 14
        roster[name].GUID = GUID;                                   -- 15

        local race , sex = select ( 4 , GetPlayerInfoByGUID ( GUID ) );
        if race == nil or sex == nil then
            race , sex = select ( 4 , GetPlayerInfoByGUID ( GUID ) );   -- Call a second time... sometimes the server is weird and the first call produces nil, but the immediate 2nd does respond.
            if race == nil or sex == nil then
                race = "";
                sex = 1;
            end
        end

        roster[name].race = race;                                   -- 16
        roster[name].sex = sex;                                     -- 17
        roster[name].rosterSelection = i;                           -- 18

        atLeastOne = true;
    end

        -- Build Roster for the first time if guild not found.
    if atLeastOne and GRM_G.guildName ~= nil and GRM_G.guildName ~= "" then
        if guildNotFound then
            -- See if it is a Guild NameChange first!
            if GRM.GuildNameChanged ( GRM_G.guildName ) then
                local tempName = GRM.GetStringClassColorByName( GRM_G.addonUser ) .. GRM.SlimName( GRM_G.addonUser ) .. "|r";
                local logEntryWithTime , logEntry = GRM.GetGuildNameChangeString ( tempName , GRM.SlimName ( GRM_G.guildName ) , select ( 2 , GRM.GetTimestamp() ) );

                GRM.PrintLog ( { 15 , logEntry } );   
                GRM.AddLog ( { 15 , logEntryWithTime , tempName , GRM.SlimName ( GRM_G.guildName ) , select ( 2 , GRM.GetTimestamp() ) } ); 
            else
                GRM.Report ( "\n" .. GRM.L ( "Guild Roster Manager" ) .. "\n" .. GRM.L ( "Analyzing guild for the first time..." ) .. "\n" .. GRM.L ( "Building Profiles on ALL \"{name}\" members" , GRM.SlimName ( GRM_G.guildName ) ) .. "\n" );
                -- This reiterates over this, because sometimes it can have a delay. This ensures it is secure.

                local clubID = 0;
                if GRM_G.BuildVersion >= 80000 then
                    clubID = C_Club.GetGuildClubId();
                else
                    clubID = GRM.CreateCustomGUIDValue( GRM_G.guildName );
                end

                if GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ] == nil then
                    GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ] = {};
                    GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ]["grmName"] = GRM_G.guildName;
                    GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ]["grmCreationDate"] = GRM_G.guildCreationDate or "";
                    GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ]["grmNumRanks"] = GuildControlGetNumRanks() or 0;
                    GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ]["grmClubID"] = clubID or 0;
                    
                    GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ] = {};
                    GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ]["grmName"] = GRM_G.guildName;
                    GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ]["grmCreationDate"] = GRM_G.guildCreationDate;
                    
                    -- Configure and initialize the backup tables.
                    GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ] = { GRM_G.guildCreationDate };                                       -- Creates a backup index for this guild
                    local type = "Auto";
                    for i = 1 , 2 do
                        if i == 2 then
                            type = "Manual";
                        end
                        GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ][type] = {};
                        GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ][type]["date"] = "";
                        GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ][type]["epochDate"] = 0;
                        GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ][type]["members"] = {};
                        GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ][type]["formerMembers"] = {};
                        GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ][type]["log"] = {};
                    end

                    -- Make sure guild is not already added.
                    if GRM_PlayerListOfAlts_Save[GRM_G.F][GRM_G.guildName] == nil then
                        GRM_PlayerListOfAlts_Save[GRM_G.F][GRM_G.guildName] = {};
                    end

                    -- Log table
                    if GRM_LogReport_Save[ GRM_G.F ][ GRM_G.guildName ] == nil then
                        GRM_LogReport_Save[ GRM_G.F ][ GRM_G.guildName ] = {};
                    end

                    -- Calendar table
                    GRM_CalendarAddQue_Save[ GRM_G.F ][ GRM_G.guildName ] = {};

                    -- Adding properly to alts list for this guild...
                    GRM_G.NeedsToAddSelfToList = true;

                    for player in pairs ( roster ) do
                        -- Build the guild and add each player
                        GRM.AddMemberRecord ( roster[player] , false , nil );
                        if GRM_GuildMemberHistory_Save [ GRM_G.F ][ GRM_G.guildName ][player] ~= nil then
                            GRM_GuildMemberHistory_Save [ GRM_G.F ][ GRM_G.guildName ][player].lastOnline = roster[player].lastOnline;   -- Setting Timestamp for the first time only.
                        end
                    end

                end
            end
        else
            -- This is critical to do in case the guild has added or removed ranks...
            GRM.CheckGuildRanks();

            if not GRM_G.OnFirstLoad then
                C_Timer.After ( 1 , function()
                    GRM_G.ThrottleControlNum = 1;
                    -- new member and leaving members arrays to check at the end - need to reset it here.
                    GRM_G.newPlayers = {};
                    GRM_G.leavingPlayers = {};
                    GRM_G.ScanRosterTimer = 0;
                    GRM.CheckPlayerChanges ( roster );
                end);
            else
                GRM_G.ThrottleControlNum = 1;
                -- new member and leaving members arrays to check at the end - need to reset it here.
                GRM_G.newPlayers = {};
                GRM_G.leavingPlayers = {};
                GRM.CheckPlayerChanges ( roster );
            end
        end
    end
end

--------------------------------------
------ END OF METADATA LOGIC ---------
--------------------------------------


-----------------------------
--- NOTIFICATION TRACKING ---
-----------------------------

-- Method:          GRM.NotificationCheck( boolean , int , int , string )
-- What it Does:    Checks if you have any notifications you are tracking, and if so, reports the status change
-- Purpose:         Since some may wish to throttle the speed at which they scan for changes, or disable it altogether, this status check system needs to be an independent
--                  check. It also would be unwise to set a notification to tell you a player came back from being AFK, but your scan for changes was set to once per 10 minutes. Useless!
--                  This independence is not yet implemented and is intertwined into the status check system...
GRM.NotificationCheck = function( isOnline , status , i , name )
    local updateNeeded = false;

    if GRM_G.ActiveStatusQue[i][3] == 2 or GRM_G.ActiveStatusQue[i][3] == 3 then
        if GRM_G.ActiveStatusQue[i][2] ~= isOnline then
            if isOnline then
                GRM.Report ( "\n|cffff0000" .. GRM.L ( "NOTIFICATION:" ) .. "|r |CFFFFFFFF" .. GRM.L ( "{name} is now ONLINE!" , GRM.GetStringClassColorByName ( GRM_G.ActiveStatusQue[i][1] ) .. GRM.SlimName ( GRM_G.ActiveStatusQue[i][1] ) .. "|r" ) .. "\n" );
            else
                GRM.Report ( "\n|cffff0000" .. GRM.L ( "NOTIFICATION:" ) .. "|r |CFFFFFFFF" .. GRM.L ( "{name} is now OFFLINE!" , GRM.GetStringClassColorByName ( GRM_G.ActiveStatusQue[i][1] ) .. GRM.SlimName ( GRM_G.ActiveStatusQue[i][1] ) .. "|r" ) .. "\n" );
            end
            table.remove ( GRM_G.ActiveStatusQue , i );
            updateNeeded = true;
        end
    else
        if status == 0 then
            if isOnline then
                GRM.Report ( "\n|cffff0000" .. GRM.L ( "NOTIFICATION:" ) .. "|r |CFFFFFFFF" .. GRM.L ( "{name} is No Longer AFK or Busy!" , GRM.GetStringClassColorByName ( GRM_G.ActiveStatusQue[i][1] ) .. GRM.SlimName ( GRM_G.ActiveStatusQue[i][1] ) .. "|r" ) .. "\n" );
            else
                GRM.Report ( "\n|cffff0000" .. GRM.L ( "NOTIFICATION:" ) .. "|r |CFFFFFFFF" .. GRM.L ( "{name} is No Longer AFK or Busy, but they Went OFFLINE!" , GRM.GetStringClassColorByName ( GRM_G.ActiveStatusQue[i][1] ) .. GRM.SlimName ( GRM_G.ActiveStatusQue[i][1] ) .. "|r" )  .. "\n" );
            end
            table.remove ( GRM_G.ActiveStatusQue , i );
            updateNeeded = true;
        end
    end

    GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name].status = status;
    GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name].isOnline = isOnline;

    if updateNeeded and GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
        GRM.PopulateMemberDetails ( GRM_G.currentName );
    end

end

-- Method:          GRM.NotificationIndependentChecker()
-- What it Does:    Activates a notification check loop if any notifications are out there or are outstanding
-- Purpose          Keep the notifications on their own separate loop from the scanning so they update rather quickly.
GRM.NotificationIndependentChecker = function()
    local timer = 10;

    for i = 1 , #GRM_G.ActiveStatusQue do
        for j = 1 , GRM.GetNumGuildies() do
            -- For guild info
            local name , _ , _ , _ , _ , _ , _ , _ , isOnline , status = GetGuildRosterInfo ( j );

            if name == GRM_G.ActiveStatusQue[i][1] then
                GRM.NotificationCheck ( isOnline , status , i , name );
                break;
            end

        end
    end

    -- Re-Check if notifications still are on there.
    if #GRM_G.ActiveStatusQue > 0 then
        if ( GRM_G.BuildVersion < 80000 and GuildFrame:IsVisible() ) or ( GRM_G.BuildVersion >= 80000 and CommunitiesFrame:IsVisible() ) then
            timer = 1;  -- if these windows are open the server authorizes player guild roster updates on the fly (less than 1 second)
        end

        C_Timer.After ( timer , function()     -- Guild Roster only updates once per 10 seconds
            GRM.GuildRoster();
            C_Timer.After ( 1 , function()
                GRM.NotificationIndependentChecker();
            end);
        end);
    end
end
        
----------------------
-- EVENT TRACKING!!!!!
----------------------

-- Method:          GRM.SetBirthdayFrameLogic()
-- What it Does:    Sets the birthdate and resets the frames as needed
-- Purpose:         To keep presentation clean!
GRM.SetBirthdayFrameLogic = function()
    local day = GRM_G.dayIndex;
    local month = GRM_G.monthIndex;
    local timeStampFormat = tostring ( day ) .. " " .. monthEnum2 [ tostring ( month ) ] .. " '19";      -- The year is just a placeholder to be parsed out...
    GRM.SetBirthday ( GRM_G.currentName , day , month , 1 , timeStampFormat , time() );

    -- Birthday Text logic...
    GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:SetText ( GRM.FormatTimeStamp ( timeStampFormat , false , true ) );
    GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:Show();
    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:Hide();

    -- Promo Window

    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName];
    if player ~= nil then
        if player.rankHistory[1][3] == 0 and not player.promoteDateUnknown then
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:Hide();
            GRM_UI.GRM_MemberDetailMetaData.GRM_SetPromoDateButton:Show();
        else
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:Show();
        end
    end

    -- Frame Logic
    GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenuSelected:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitCancelButton:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButton:Hide();

    if GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame:IsVisible() then
        GRM.RefreshAuditFrames ( true , true );
    end
end

-- Method:          GRM.SetBirthday ( string , int , int , int , string , int , boolean , string , boolean )
-- What it Does:    Sets the player's birthday
-- Purpose:         To take advantage of the player birthdate feature!
GRM.SetBirthday = function ( name , day , month , year , date , timeStamp , isSync , sender , isFullSync )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];
    if player ~= nil then
        if player.events[2][4] < timeStamp then
            player.events[2][1] = { day , month , year };
            player.events[2][2] = false;
            player.events[2][3] = date;
            player.events[2][4] = timeStamp;

            if player.birthdayUnknown then
                player.birthdayUnknown = false
            end

            GRM.RemoveFromCalendarQue ( player.name , 2 , nil );

            -- Check Alts as well
            GRM.SetBirthdayForAltGrouping ( name , day , month , year , date , timeStamp );

            -- Now, send the details out...
            if not isFullSync then
                if not isSync then
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncBDays and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled then
                        local syncRankFilter = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank;
                        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportAllRanks then
                            syncRankFilter = GuildControlGetNumRanks() - 1;
                        end
                        GRMsync.SendMessage ( "GRM_SYNC" , GRM_G.PatchDayString .. "?GRM_BDAY?" .. syncRankFilter .. "?" .. name .. "?" .. tostring ( day ) .. "?" .. tostring ( month ) .. "?" .. date .. "?" .. tostring ( timeStamp ) , "GUILD" );
                    end
                    GRM.Report ( GRM.L ( "{name}'s Birthday has been set: {custom1}" , GRM.GetClassifiedName ( name , true ) , nil , nil , GRM.FormatTimeStamp ( date , false , true ) ) );
                elseif isSync then
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncChatEnabled then
                        GRM.Report ( GRM.L ( "{name} has set {name2}'s Birthday: {custom1}" , GRM.GetClassifiedName ( sender , true ) , GRM.GetClassifiedName ( name , true ) , nil , GRM.FormatTimeStamp ( date , false , true ) ) );
                    end
                end
            else
                GRMsyncGlobals.updateCount = GRMsyncGlobals.updateCount + 1;
                GRMsyncGlobals.upatesEach[6] = GRMsyncGlobals.upatesEach[6] + 1;
            end

            -- Update frames if looking at them on the spot...
            if GRM_UI.GRM_MemberDetailMetaData:IsVisible() and name == GRM_G.currentName and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showBDay then
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:Hide();
                GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:SetText ( GRM.FormatTimeStamp ( date , false , true ) );
                GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:Show();
            end

            if GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame:IsVisible() then
                GRM.RefreshAddEventFrame()
            end
        end
    end
end

-- Method:          GRM.SetBirthdayForAltGrouping ( string , int , int , int , string , int )
-- What it Does:    Sets all of the alts to the same birthday as well when there is a modification
-- Purpose:         Ensure birthdays are set for all alts, as it is assumed it is one player
GRM.SetBirthdayForAltGrouping = function ( playerName , day , month , year , date , timeStamp )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][playerName];

    if player ~= nil then
        for i = 1 , #player.alts do

            local tempAlt = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][player.alts[i][1]];
            if tempAlt ~= nil then
                -- Alt found!
                tempAlt.events[2][1] = { day , month , year };
                tempAlt.events[2][2] = false;
                tempAlt.events[2][3] = date;
                tempAlt.events[2][4] = timeStamp;

                if tempAlt.birthdayUnknown then
                    tempAlt.birthdayUnknown = false
                end

                GRM.RemoveFromCalendarQue ( tempAlt.name , 2 , nil );

                -- Update frames if looking at them on the spot...
                if GRM_UI.GRM_MemberDetailMetaData:IsVisible() and tempAlt.name == GRM_G.currentName and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showBDay then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:Hide();
                    GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:SetText ( GRM.FormatTimeStamp ( date , false , true ) );
                    GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:Show();
                end
            end
        end
    end
end
 
-- Method:          GRM.SyncBirthdayWithNewAlt ( string )
-- What it Does:    Sets the new alt to the same birthday as the previous alts
-- Purpose:         Keep alts all in sync on the birthday.
GRM.SyncBirthdayWithNewAlt = function ( newAlt )
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    local player = guildData[newAlt];

    if player ~= nil then
        if #player.alts > 0 then
            local tempAlt = guildData[player.alts[1][1]];
            if tempAlt ~= nil then
                if tempAlt.events[2][3] ~= "" or ( tempAlt.events[2][3] == "" and #tempAlt.alts == 1 and not tempAlt.isMain and player.events[2][3] ~= "" ) then -- We only need to check the first alt as all should have same
                    
                    local updatePos1 = newAlt;
                    local updatePos2 = tempAlt.name;
                    -- reverse them if the player I am overriding is the main.                                                                                                                                                          -- Neither of them are set as mains... so 1 with date takes priority
                    if #player.alts == 1 and ( player.isMain or ( not player.isMain and not tempAlt.isMain and player.events[2][4] ~= 0 ) ) then
                        updatePos1 = tempAlt.name;
                        updatePos2 = newAlt;
                    end
                    
                    guildData[updatePos1].events[2][1] = guildData[updatePos2].events[2][1];
                    guildData[updatePos1].events[2][2] = guildData[updatePos2].events[2][2];
                    guildData[updatePos1].events[2][3] = guildData[updatePos2].events[2][3];
                    guildData[updatePos1].events[2][4] = guildData[updatePos2].events[2][4];

                    if guildData[updatePos1].birthdayUnknown then
                        guildData[updatePos1].birthdayUnknown = false
                    end

                    GRM.RemoveFromCalendarQue ( guildData[updatePos1].name , 2 , nil );

                    -- Update frames if looking at them on the spot...
                    if GRM_UI.GRM_MemberDetailMetaData:IsVisible() and guildData[updatePos1].name == GRM_G.currentName and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showBDay and guildData[updatePos1].events[2][3] ~= "" then
                        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:Hide();
                        GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:SetText ( GRM.FormatTimeStamp ( guildData[updatePos1].events[2][3] , false , true ) );
                        GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:Show();
                    end

                    if GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame:IsVisible() then
                        GRM.RefreshAddEventFrame()
                    end

                -- Only purge the date if being added to a group with no date of more than just you, OR, if the "main" does not have a date...
                elseif tempAlt.events[2][3] == "" and ( #tempAlt.alts > 1 or ( #tempAlt.alts == 1 and tempAlt.isMain ) ) then
                    -- New alt grouping does not have a birthday seet, so let's remove the birthday of incoming alt if set...
                    player.events[2] = { { 0 , 0 , 0 } , false , "" , 0 };
                    if GRM_UI.GRM_MemberDetailMetaData:IsVisible() and player.name == GRM_G.currentName and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showBDay then
                        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:Show();
                        GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:Hide();
                    end
                end

            end
        end
    end
end

-- Method:          GRM.ResetBirthdayForAltGroup ( string , boolean , int , string )
-- What it Does:    Resets the birthdays for the player and all the alts connected in the group
-- Purpose:         Birthday is universal of an alt grouping... keeps them together.
-- Note:            It does leave a timstamp of the change so that you will not resync the player data again from other players, if you wanted to remove the bday.
--                  This is a unique removal to just yourself.
GRM.ResetBirthdayForAltGroup = function ( name , isLiveSync , num , sender , isUnknown )

    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];
    if player ~= nil then
        -- First, change the given player's data and reset it

        local timestamp = num;
        if timestamp == 0 then
            timestamp = time();
        end
        local updateUI = function ( textName )
            if not isUnknown and GRM_UI.GRM_MemberDetailMetaData:IsVisible() and GRM_G.currentName == textName then
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:Show();
                GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:Hide();
            end
        end

        player.events[2] = { { 0 , 0 , 0 } , false , "" , timestamp };
        if not isUnknown then
            player.birthdayUnknown = false;
        end

        updateUI ( name );
        GRM.RemoveFromCalendarQue ( player.name , 2 , nil );
        for j = 1 , #player.alts do
            local tempAlt = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][player.alts[j][1]];
            if tempAlt ~= nil then
                tempAlt.events[2] = { { 0 , 0 , 0 } , false , "" , timestamp };
                GRM.RemoveFromCalendarQue ( tempAlt.name , 2 , nil );

                if not isUnknown then
                    player.birthdayUnknown = false;
                end

                updateUI ( player.alts[j][1] );
            end
        end

        -- No need to run this
        if not isLiveSync then
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncBDays and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled then
                local syncRankFilter = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank;
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportAllRanks then
                    syncRankFilter = GuildControlGetNumRanks() - 1;
                end
                GRMsync.SendMessage ( "GRM_SYNC" , GRM_G.PatchDayString .. "?GRM_BDAYREM?" .. syncRankFilter .. "?" .. name .. "?" .. tostring ( timestamp ) , "GUILD" );
            end
        elseif not isUnknown then
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncChatEnabled then
                if #player.alts > 0 then
                    GRM.Report ( GRM.L ( "{name}'s alt grouping has had their Birthday removed by: {name2}" , GRM.GetClassifiedName ( name , true ) , GRM.GetClassifiedName ( sender , true ) ) );
                else
                    GRM.Report ( GRM.L ( "{name}'s Birthday has been removed by: {name2}" , GRM.GetClassifiedName ( name , true ) , GRM.GetClassifiedName ( sender , true ) ) );
                end
            end                
        end

        if not isUnknown and GRM_UI.GRM_MemberDetailMetaData:IsVisible() and GRM_G.currentName == name then
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:Show();
            GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:Hide();
        end

    end
end

-- Method:          GRM.CleanupBirthdays ( int , int )
-- What it Does:    Searches for all matches of this birthdate, and resets them to default empty value
-- Purpose:         Help cleanup a bug introduced from 1.33
GRM.CleanupBirthdays = function ( day , month , forceOthers , g )
    if type ( day ) == "number" and type ( month ) == "number" then
        if day > 0 and day < 32 and month > 0 and month < 13 then
            local guildData = g or GRM_GuildMemberHistory_Save[GRM_G.F][GRM_G.guildName];
            local count = 0;
            local timeStamp = 0;
            if forceOthers then
                timeStamp = time();
            end
            for member in pairs ( guildData ) do
                if type ( guildData[member] ) == "table" then
                    if guildData[member].events[2][1][1] == day and guildData[member].events[2][1][2] == month then
                        guildData[member].events[2] = { { 0 , 0 , 0 } , false , "" , timeStamp };
                        count = count + 1;
                    end
                end
            end
            if count > 0 then
                GRM.Report ( GRM.L ( "{num} birthdays have been reset." , nil , nil , count ) );
            else
                GRM.Report ( GRM.L ( "No player was found to have that birthday." ) );
            end

        else
            if not ( day > 0 and day < 32 ) then
                GRM.Report ( GRM.L ( "The day cannot be {num}. It must be a number between 1 and 31" , nil , nil , day ) );
            end
            if not ( month > 0 and month < 13 ) then
                GRM.Report ( GRM.L ( "The month cannont be {num}. must be a number between 1 and 12" , nil , nil , month ) );
            end
        end
    else
        if type ( day ) ~= "number" then
            GRM.Report ( GRM.L ( "{num} is not a valid day of the month! It must be a number between 1 and 31" , nil , nil , day ) );
        end
        if type ( month ) ~= "number" then
            GRM.Report ( GRM.L ( "{num} is not a valid index of the month of the year! It must be a number between 1 and 12" , nil , nil , month ) );
        end
    end
end

-- Method:          GRM.GetCleanTimestamp ( string )
-- What it Does:    Returns the timestamp formatted properly
-- Purpose:         Clean the timestamp for proper parsing and formatting.
GRM.GetCleanTimestamp = function ( timestamp )
    return string.match ( timestamp , "%d.+'%d%d" );
end

-- Method:          GRM.GetEventYear ( string )
-- What it Does:    Returns the year of the given event from timestamp
-- Purpose:         Keep code clutter down, put this block in reusable form.
GRM.GetEventYear = function ( timestamp )
    -- timestamp format = "Day month year hour min"
    local result = 0;
    if timestamp ~= "" and timestamp ~= nil then
        result = tonumber ( string.sub ( string.match ( timestamp , "'%d%d" ) , 2 ) ) + 2000;
    end
    return result;
end

-- Method:          GRM.GetEventMonth ( string )
-- What it Does:    Returns the 3 letter string of the name of the month of the event.
-- Purpose:         Again, avoid code cludder. For event tracking, knowing exact date is essential.
GRM.GetEventMonth = function ( timestamp )
    if timestamp == "" or timestamp == nil then
        return nil;
    else
        return string.match ( timestamp , "%a+" );
    end
end

-- Method:          GRM.GetEventDay ( string )
-- What it Does:    Returns the number of the day, as a string, based on day of the month for given event timestamp
-- Purpose:         Important to know what day event should happen on.
GRM.GetEventDay = function ( timestamp )
    if timestamp == "" or timestamp == nil then
        return nil;
    else
        return tonumber ( string.match ( timestamp , "%d+" ) );
    end
end

-- Method:          GRM.ConvertMonthToSelectedCalendarMonth ( int )
-- What it Does:    The current month calendar equals 0. However, if the player event is July, and the current month on calendary is July, one is 0 and one is 7
--                  This takes the player's event month, substracts the current month, thus finding the index you want to place the month (Example, August is month index 1, since it is +1 of this month)
-- Purpose:         For event tracking for the calendar event add buttons
GRM.ConvertMonthToSelectedCalendarMonth = function ( monthIndex )
    return monthIndex - GRM.GetCurrentCalendarTime().month;
end

-- Method:          GRM.IsCalendarEventAlreadyAdded ( string , int , int , int )
-- What it Does:    Returns true if the event has already been added to the calendar 
-- Purpose:         If the player wipes his save history, it does not wipe what is added to in-game calendar. This just double-checks to avoid double adding.
GRM.IsCalendarEventAlreadyAdded = function ( eventName , year , month , day )
    eventName = GRM.SlimName( string.sub ( eventName , 1 , ( string.find ( eventName , " " ) - 1 ) ) ) .. string.sub ( eventName , string.find ( eventName , " " ) , #eventName ); -- necessary for x-realm compatibility to get slimname
    local result = false;
    local monthIndex = 0;

    for i = 0 , 17 do                       -- Let's get to the right month on the calendar
        local monthInfo = C_Calendar.GetMonthInfo( i );
        if monthInfo.month == month and monthInfo.year == year then
            monthIndex = i;
            break;
        end
    end
    for i = 1 , C_Calendar.GetNumDayEvents ( monthIndex , day ) do         -- Let's look at all the events on the day of the event
        if eventName == C_Calendar.GetDayEvent ( monthIndex , day , i ).title then
            result = true;
            break;
        end
    end
    return result;
end

-- Method:          GRM.IsOnAnnouncementList ( string , int , string )
-- What it Does:    returns true if the player is in the que to add to the calendar
-- Purpose:         Avoid double adding to que, and basic logic checking.
GRM.IsOnAnnouncementList = function ( name , index , eventName )
    local result = false;
    for i = 1 , #GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName] do
        if GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][1] == name then
            if index < 3 and GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][7] == index then              -- Helps to check against index so people of multiple localizations can be compared without compatibility issues
                result = true;
            elseif index > 2 and GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][2] == eventName then      -- Custom events are not localized as the "titles" will be unique to what the user put.
                result = true;
            end
            if result == true then
                break;
            end
        end
    end
    return result;
end

-- Method:          GRM.RemoveFromCalendarQue ( string , int , string )
-- What it Does:    Removes the player/event from the global Calendar Add Que table
-- Purpose:         Keep the Que Clean
GRM.RemoveFromCalendarQue = function ( name , index , eventName )
    for i = 1 , #GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName] do
        if GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][1] == name then

            if index < 3 and GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][7] == index then      -- position 7 will may either be 1 or 2
                table.remove ( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName] , i );
                break;
            elseif index > 2 and GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][2] == eventName then
                table.remove ( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName] , i );
                break;
            end

        end
    end
end

-- Method:          GRM.CalendarQueCheck ()
-- What it Does:    It checks the Add Que list, if the event is already on the calendar, then it removes it from the addque list.
-- Purpose:         In case other players add items to the calendar, this keeps it clean.
GRM.CalendarQueCheck = function ()
    if GRM_G.BuildVersion >= 30000 then
        local tempQue = GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName];

        for i = #tempQue , 1 , -1 do
            if GRM.IsCalendarEventAlreadyAdded ( tempQue[i][2] , tempQue[i][5] , GRM.ConvertMonthToSelectedCalendarMonth ( tempQue[i][3] ) , tempQue[i][4] ) then
                table.remove ( tempQue , i );
            end
        end
    end
end

-- Method:          GRM.GetAnniversaryLogReport( string , string , int )
-- What it Does:    Returns the proper string of the annivesary events, both formats. One for UI display, and one for readabilty
-- Purpose:         For the addon feature of reporting and adding the anniversary to the calendar.
GRM.GetAnniversaryLogReport = function ( name , class , numYears )
    local result , result2;
    local classifiedName = GRM.GetClassHex ( class ) .. GRM.SlimName ( name ) .. "|r";

    if numYears == 1 then
        result = ( GRM.FormatTimeStamp ( GRM.GetTimestamp() , true ) .. " : " .. GRM.L ( "{name} will be celebrating {num} year in the Guild!" , classifiedName , nil , numYears ) );
        result2 = GRM.L ( "{name} will be celebrating {num} year in the Guild!" , classifiedName , nil , numYears );
    else
        result = ( GRM.FormatTimeStamp ( GRM.GetTimestamp() , true ) .. " : " .. GRM.L ( "{name} will be celebrating {num} years in the Guild!" , classifiedName , nil , numYears ) );
        result2 = GRM.L ( "{name} will be celebrating {num} years in the Guild!" , classifiedName , nil , numYears );
    end
    return result , result2;
end

-- Method:          GRM.GetBirthdayLogReport( string , string )
-- What it Does:    Returns the proper string of the birthday events, both formats. One for UI display, and one for readabilty
-- Purpose:         For the addon feature of reporting and adding the anniversary to the calendar.
GRM.GetBirthdayLogReport = function ( name , class )
    local classifiedName = GRM.GetClassHex ( class ) .. GRM.SlimName ( name ) .. "|r";
    local result = ( GRM.FormatTimeStamp ( GRM.GetTimestamp() , true ) .. " : " .. GRM.L ( "It's almost time to celebrate {name}'s Birthday!" , classifiedName ) );
    local result2 = GRM.L ( "It's almost time to celebrate {name}'s Birthday!" , classifiedName );

    return result , result2;
end

-- Method:          GRM.GetCustomEventReport( string , int , string )
-- What it Does:    Returns the proper string of the custom events, both formats. One for UI display, and one for readabilty
-- Purpose:         For the addon feature of reporting and adding the anniversary to the calendar.
-- GRM.GetCustomEventReport = function ( name , numYears , eventDate )
--     local result , result2;

--     return result , result2;
-- end

-- Method:          GRM.ResetPlayerEvent ( string , int , string )
-- What it Does:    It checks if the event is on the eventsLog announcecment, and then if so, removes it.
-- Purpose:         Events log needs to be adjusted as the player adjusts the settings. This is used for many conditions, so it keeps it in one reusable function.
GRM.ResetPlayerEvent = function ( name , index , eventName )
    if GRM.IsOnAnnouncementList ( name , index , eventName ) then
        GRM.RemoveFromCalendarQue ( name , index , eventName );
    end
end

-- Method:          GRM.CleanupEventsFromplayers ()
-- What it Does:    If the player is not found in the guild but they are still on the event list, just dump the event list.
-- Purpose:         Cleanup events QoL
GRM.CleanupEventsFromplayers = function ()
    local cleanupHappened = false;
    for i = #GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName] , 2 , -1 do
        local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][ GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName][i][1] ];
        if player == nil then
            table.remove ( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName] , i );
            cleanupHappened = true;
        end
    end

    if cleanupHappened and GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame:IsVisible() then
        GRM.RefreshAddEventFrame()
    end
end

-- Method:          GRM.CheckPlayerEvents ()
-- What it Does:    Scans through all players'' "events" of the given guild and updates if any are pending
-- Purpose:         Event Management for Anniversaries, Birthdays, and Custom Events
GRM.CheckPlayerEvents = function ()

    -- Calendar did not yet exist... until WOTLK
    if GRM_G.BuildVersion < 30000 then
        return;
    end

    -- including anniversary, birthday , and custom
    local month , day , year = select ( 2 , GRM.CalendarGetDate() );
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    local eventMonthIndex , eventDay , eventYear , isLeapYear , logReport2;
    local title = "";

    -- Quickly cleanup the list if necessary
    GRM.CleanupEventsFromplayers();
    local player;
    local playerSlimName;

    for member in pairs ( guildData ) do
        if type ( guildData[member] ) == "table" then
            player = guildData[member];
            playerSlimName = GRM.SlimName ( player.name );

            -- Player identified, now let's check his event info!
            for r = 1 , #player.events do          -- Loop all events to check!
                eventDay = player.events[r][1][1];
                eventMonthIndex = player.events[r][1][2];
                eventYear = player.events[r][1][3];
                isLeapYear = GRM.IsLeapYear ( year );
                logReport2 = "";            -- Clean of the string fluff, for just adding details into the event description.
                
                --  Quick Leap Year Check
                if ( eventDay == 29 and eventMonthIndex == 2 ) and not isLeapYear then  -- If Event is Feb 29th Leap year, and reporting year is not, then put event in Mar 1st.
                    eventMonthIndex = 3;
                    eventDay = 1;
                end
                -- Check status - another player might have already added it to the list.
                -- indexOfEvent: 1 = anniversary , 2 = birthday , 3 = custom 
                title = "";
                if r > 2 then
                    title = player.events[r][3];
                end

                if not player.events[r][2] and player.events[r][1][1] ~= 0 and ( r > 2 or ( r < 3 and ( not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].onlyAnnounceForMain or player.isMain ) ) ) then        -- if it has already been reported, then we are good!
                    
                    local daysTil = -1;
                    local isValidMonth = ( eventMonthIndex == ( month + 1 ) or ( ( month + 1 ) == 13 and eventMonthIndex == 1 ) );

                    if eventMonthIndex == month then
                        -- Same month! Check if it was a date before today.
                        daysTil = eventDay - day;
                    elseif isValidMonth then
                        -- It is the next month after!
                        local daysLeftInMonth = daysInMonth [ tostring ( month ) ] - day;
                        if month == 2 and GRM.IsLeapYear ( year ) then
                            daysLeftInMonth = daysLeftInMonth + 1;
                        end
                        daysTil = eventDay + daysLeftInMonth;
                    end

                    -- Now, let's check if it needs to be reported!
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].calendarAnnouncements and ( ( ( isValidMonth or not isValidMonth ) and daysTil >= 0 and daysTil <= GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].eventAdvanceDays ) or ( isValidMonth and ( daysTil <= GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].eventAdvanceDays ) ) ) then
                        -- YES! It needs reporting! It is within the threshold!
                        -- Configure some of the dates
                        local numYears = year - eventYear;
                        if month == 12 and eventMonthIndex == 1 then
                            numYears = numYears + 1;
                        end
                        
                        if ( eventDay == 29 and eventMonthIndex == 2 ) and not isLeapYear then    -- If anniversary happened on leap year date, and the current year is NOT a leap year, then put it on 1 Mar.
                            eventDay = 1;
                            eventMonthIndex = 3;
                        end

                        -- Join Date Anniversary -- Let's see if player has it set to ONLY announce anniversary event on Calendar for a player's "main"
                        if r == 1 and numYears ~= 0 then

                            title = GRM.L ( "{name}'s Anniversary!" , playerSlimName );
                            logReport2 = select ( 2 , GRM.GetAnniversaryLogReport ( player.name , player.class , numYears ) );
                            GRM.AddEventEntry ( r , player.name ,  player.class , eventDay , eventMonthIndex , isLeapYear , select ( 2 , GRM.GetTimestamp() ) , numYears );

                        elseif r == 2 then
                            title = GRM.L ( "{name}'s Birthday!" , playerSlimName );
                            logReport2 = select ( 2 , GRM.GetBirthdayLogReport ( player.name , player.class ) );
                            GRM.AddEventEntry ( r , player.name ,  player.class , eventDay , eventMonthIndex , isLeapYear , select ( 2 , GRM.GetTimestamp() ) );

                        elseif r > 2 then
                        -- CUSTOM EVENT!
                            -- logReport , logReport2 = GRM.GetCustomEventReport ( player.name , numYears , eventDate );
                        end
                        
                        -- Now, let's add it to the calendar!!!
                        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].allowEventsToCalendar and logReport2 ~= "" and ( GRM_G.BuildVersion < 30000 or ( GRM_G.BuildVersion >= 30000 and CanEditGuildEvent() ) ) then
                            local finalYear = year;
                            if month == 12 and eventMonthIndex == 1 then
                                finalYear = finalYear + 1;
                            end 

                            local isAddedAlready = GRM.IsCalendarEventAlreadyAdded ( title , finalYear , GRM.ConvertMonthToSelectedCalendarMonth ( eventMonthIndex ) , eventDay  );
                            if not isAddedAlready and not GRM.IsOnAnnouncementList ( player.name , r , title ) then
                                -- { playerName , EventTitle , monthIndex , day , year , Description, indexOfEvent } 
                                table.insert ( GRM_CalendarAddQue_Save[GRM_G.F][GRM_G.guildName] , { player.name , title , eventMonthIndex , eventDay , finalYear , logReport2 , r } );
                            end
                        end
                        -- This has been reported, save it!
                        player.events[r][2] = true;
                    end                  
                    
                -- Resetting the event report to false if parameters meet
                elseif player.events[r][2] then                                                   -- It is still true! Event has been reported! Let's check if time has passed sufficient to wipe it to false
                    local cleanupHappened = false;
                    if ( month == eventMonthIndex and eventDay - day < 0 ) or ( eventMonthIndex - month > 1 ) or month > eventMonthIndex then     -- Event is behind us now
                        local needsCleanup = true;

                        if month > eventMonthIndex then
                            if ( eventMonthIndex == 1 and month == 12 ) then
                                local daysLeftInMonth = daysInMonth [ tostring ( month ) ] - day;
                                if month == 2 and GRM.IsLeapYear ( year ) then
                                    daysLeftInMonth = daysLeftInMonth + 1;
                                end

                                if ( eventDay + daysLeftInMonth ) <= GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].eventAdvanceDays then
                                    needsCleanup = false;
                                end
                            end
                        end
                        if needsCleanup then
                            player.events[r][2] = false;
                            GRM.ResetPlayerEvent ( player.name , r , title );
                            cleanupHappened = true;
                        end
                    elseif month == eventMonthIndex and eventDay - day > GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].eventAdvanceDays then      -- Setting back to false;
                        player.events[r][2] = false;
                        GRM.ResetPlayerEvent ( player.name , r , title );
                        cleanupHappened = true;
                    elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].onlyAnnounceForMain and not player.isMain then
                        player.events[r][2] = false;
                        GRM.ResetPlayerEvent ( player.name , r , title );
                        cleanupHappened = true;
                    -- Cleanup
                    elseif month + 1 == eventMonthIndex then
                        local daysLeftInMonth = daysInMonth [ tostring ( month ) ] - day;
                        if month == 2 and GRM.IsLeapYear ( year ) then
                            daysLeftInMonth = daysLeftInMonth + 1;
                        end
                        if eventDay + daysLeftInMonth > GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].eventAdvanceDays then
                            player.events[r][2] = false;
                            GRM.ResetPlayerEvent ( player.name , r , title );
                            cleanupHappened = true;
                        end
                    end
                    if cleanupHappened and GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame:IsVisible() then
                        GRM.RefreshAddEventFrame()
                    end
                end
            end
        end
    end
end

-- Method:          GRM.AddAnnouncementToCalendar ( string , int , int , int , string )
-- What it Does:    Adds the announcement to the in-game calendar, if player has permissions to do so.
-- Purpose:         CalendarAddEvent() is a protected function thus it needs to be triggered by a player in-game action, so it will
--                  be linked to a button on the "GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame" window. Again, this cannot be activated, it WILL NOT WORK without 
--                  in-game action to remove protection on function
GRM.AddAnnouncementToCalendar = function ( title , eventMonthIndex , eventDay , year , description )
    C_Calendar.CloseEvent()                           -- Just in case previous event was never closed, either by other addons or by player
    local month, day = select ( 2 , GRM.CalendarGetDate() );
    local hourServer , minServer = GetGameTime();
    local hour = 0;                                 -- 24hr scale, on when to add it...
    local min = 5;

    if eventMonthIndex == month and eventDay == day then      -- Add current time now!
        hour = hourServer;
        min = minServer;

        local tempMin = min;
        min = min - ( min % 5 ) + 5;    -- To get incrememnt by 5
        if min == 60 then
            if tempMin <= 55 then
                min = 55;
            else
                min = tempMin;
            end
        end
    end

    -- C_Calendar.NewGuildAnnouncement();
    C_Calendar.CreateGuildAnnouncementEvent()
    C_Calendar.EventSetDate ( eventMonthIndex , eventDay , year );
    C_Calendar.EventSetTitle ( title );
    C_Calendar.EventSetDescription ( GRM.RemoveStringColoring ( description ) ); -- No need to include the date at the end.
    C_Calendar.EventSetTime ( hour , min );    
    C_Calendar.EventSetType ( CALENDAR_EVENTTYPE_OTHER );     -- 5 = announcement
    C_Calendar.AddEvent();
    C_Calendar.CloseEvent();
end

--------------------------------
-------- LOG CONTROLS ----------
--------------------------------

-- Method:          GRM.RemoveItemFromLog ( index , bool , bool )
-- What it Does:    Finds the matching string in the log, and removes it from the tempFiltered Log and the core log.
-- Purpose:         For quick log cleanup.
-- Notes:           Worth mentioning, one of the reasons it is handled this way is because you want to clear the lines but not do a full refresh of the log, which takes
--                  processing, but more importantly, resets the slider to the top. This allows you to clear as you move and continue on.
GRM.RemoveItemFromLog = function ( index , buildLog , buildComplete )

    local tempIndex = GRM_G.fullLogMatch[index][5];         -- This is identifying index. If repeated, it is a multi-line input to the log entry.
    local tempText = GRM_G.fullLogMatch[index][6];

    local i = 1;
    local isFound = false;

    -- Remove it from the tempLog
    while i <= #GRM_G.fullLogMatch and ( not isFound or ( isFound and GRM_G.fullLogMatch[i][5] == tempIndex ) ) do
        if GRM_G.fullLogMatch[i][5] == tempIndex then
            isFound = true;
            table.remove ( GRM_G.fullLogMatch , i );
        else
            i = i + 1;
        end
    end

    -- Shift all values by 1, since we are removing
    if i > 1 then
        for j =  ( i - 1 ) , #GRM_G.fullLogMatch do
            GRM_G.fullLogMatch[j][5] = GRM_G.fullLogMatch[j][5] - 1;
        end
    end

    -- Remove from core. 
    for i = #GRM_LogReport_Save[GRM_G.F][GRM_G.guildName] , 1 , -1 do
        if GRM_LogReport_Save[GRM_G.F][GRM_G.guildName][i][2] == tempText then
            table.remove ( GRM_LogReport_Save[GRM_G.F][GRM_G.guildName] , i );
            break;
        end
    end

    if buildLog and GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame:IsVisible() then
        GRM_G.LogNumbersColorUpdate = true;
        GRM.BuildLogComplete( true , buildComplete );
    end
end

-- Method:          GRM.ClearAllLogLinesWithinRange ( int , int )
-- What it Does:    Parse through all of the fontstrings visible, then matches them to the log and purges them
-- Purpose:         For log cleanup and UI features for the user
GRM.ClearAllLogLinesWithinRange = function ( start , stop )
    
    if start == 0 then
        start = 1;
    end

    local totalCount = stop - start + 1;
    local log = GRM_G.fullLogMatch;
    local index = 0;

    for i = 1 , #log do
        if index ~= log[i][4] and log[i][5] >= start and log[i][5] <= stop then
            index = log[i][4]
            -- Next entry!
            table.remove ( GRM_LogReport_Save[GRM_G.F][GRM_G.guildName] , log[i][4] );
            -- You need to keep incrementing because the table will be shrinking.

        elseif log[i][5] > stop then
            break;      -- easy escape to not process everything!
        end
    end

    GRM_G.CurrentTotalCount = GRM_G.CurrentTotalCount - totalCount;

    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogFrameNumEntriesText:SetText ( GRM.L ( "Total Entries: {num}" , nil , nil , GRM_G.CurrentTotalCount ) );

    if totalCount == 1 then
        GRM.Report ( GRM.L ( "1 entry has been removed from the log" ) );
    else
        GRM.Report ( GRM.L ( "{num} entries have been removed from the log" , nil , nil , totalCount ) );
    end

end

-- Method:          GRM.ResetLogReport()
-- What it Does:    Deletes the guild Log
-- Purpose:         In case player wishes to reset guild Log information.
GRM.ResetLogReport = function()
    if #GRM_LogReport_Save[GRM_G.F][GRM_G.guildName] == 0 then
        GRM.Report ( GRM.L ( "There are No Log Entries to Delete, silly {name}!" , GRM.GetClassifiedName ( GRM_G.addonUser , true ) ) );
    else
        GRM.Report ( GRM.L ( "Guild Log has been RESET!" ) );
        GRM_LogReport_Save[GRM_G.F][GRM_G.guildName] = {};

        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogEditBox:SetText( GRM.L ( "Search Filter" ) );
        if GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then    -- if frame is open, let's rebuild it!
            GRM.BuildLogComplete( true , true );
        end
    end
end

-- Method:          GRM.LogSetLastValue()
-- What it Does:    Triggers the setting fo all values at index 40 of the core log
-- Purpose:         On the single line shift, you end up with 1 on the edge that needs to be built
GRM.LogSetLastValue = function()
    GRM.SetLogValues ( #GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons , GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.Offset );
end

-- Method:          GRM.LogSetFirstValue()
-- What it Does:    Triggers the setting fo all values at index 1 of the core Log
-- Purpose:         On the single line shift, you end up with 1 on the edge that needs to be built
GRM.LogSetFirstValue = function()
    GRM.SetLogValues ( 1 , GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.Offset - #GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons + 1 );
end

-- Method:          GRM.LogToolHybridShiftDown()
-- What it Does:    Shifts all the values down one line to give the illusion of scrolling when in reality this is just a hybrid scrollframe
-- Purpose:         Clean scrolling
GRM.LogToolHybridShiftDown = function()
    if #GRM_G.fullLogMatch > 25 then
        local fontStrings = GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons;
        local MouseOverButton = 0;
        -- Shift them down...
        for i = 1 , #fontStrings - 1 do
            fontStrings[i][2]:SetText( fontStrings[i+1][2]:GetText() );
            fontStrings[i][2]:SetTextColor ( fontStrings[i+1][2]:GetTextColor ( fontStrings[i+1][2]:GetText() ) );
            fontStrings[i][3]:SetText( fontStrings[i+1][3]:GetText() );
            fontStrings[i][3]:SetTextColor ( fontStrings[i+1][3]:GetTextColor ( fontStrings[i+1][3]:GetText() ) );
            fontStrings[i][4] = fontStrings[i+1][4]

            if MouseOverButton == 0 and fontStrings[i][1]:IsMouseOver() then
                MouseOverButton = i;
            end
        end
        GRM.LogSetLastValue();
        if MouseOverButton > 0 then
            GRM.RefreshLogTooltip( fontStrings[MouseOverButton][1] );
        end
    end
end

-- Method:          GRM.LogToolHybridShiftUP()
-- What it Does:    Shifts all the values up one line to give the illusion of scrolling when in reality this is just a hybrid scrollframe
-- Purpose:         Clean scrolling
GRM.LogToolHybridShiftUP = function()
    if #GRM_G.fullLogMatch > 25 then
        local fontStrings = GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons;
        local MouseOverButton = 0;
        -- Shift them down...
        for i = #fontStrings , 2 , -1 do
            fontStrings[i][2]:SetText( fontStrings[i-1][2]:GetText() );
            fontStrings[i][2]:SetTextColor ( fontStrings[i-1][2]:GetTextColor ( fontStrings[i-1][2]:GetText() ) );
            fontStrings[i][3]:SetText( fontStrings[i-1][3]:GetText() );
            fontStrings[i][3]:SetTextColor ( fontStrings[i-1][3]:GetTextColor ( fontStrings[i-1][3]:GetText() ) );
            fontStrings[i][4] = fontStrings[i-1][4]
            -- Configure new SetPoints due to varying string sizes

            if MouseOverButton == 0 and fontStrings[i][1]:IsMouseOver() then
                MouseOverButton = i;
            end
        end
        GRM.LogSetFirstValue();
        if MouseOverButton > 0 then
            GRM.RefreshLogTooltip( fontStrings[MouseOverButton][1] );
        end
    end
end

-- Method:          GRM.ConfigureChangesHeader()
-- What it Does:    Configures the changes header to add it
-- Purpose:         To Clear the changes header
GRM.ConfigureChangesHeader = function()
    local neededToAddChanges = false;
    if GRM_G.FirstTimeViewed and GRM_G.IndexOfLastLogEntry ~= #GRM_LogReport_Save[GRM_G.F][GRM_G.guildName] and #GRM_LogReport_Save[GRM_G.F][GRM_G.guildName] > 0 then
        neededToAddChanges = true
        -- Fontstring rules
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.GRM_ChangesHeader:SetJustifyH ( "LEFT" );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.GRM_ChangesHeader:SetWidth ( 561 );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.GRM_ChangesHeader:SetSpacing ( 7 );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.GRM_ChangesHeader:SetWordWrap ( false );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.GRM_ChangesHeader:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 10.79 );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.GRM_ChangesHeader:SetText ( GRM.AddNewChangesHeader() );
        
        -- Set Point to the top
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.GRM_ChangesHeader:SetPoint ( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame , 0 , - 5 );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[1][1]:SetPoint( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame , 0 , - 5 - math.floor ( GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.GRM_ChangesHeader:GetHeight() ) - 7 );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.GRM_ChangesHeader:Show();
    end
    return neededToAddChanges;
end

-- Method:          GRM.SetLogValues ( int , int )
-- What it Does:    Sets the fontstring values for the lineNumber count and the actual strings for display
-- Purpose:         So we have a functioning hybrid scrollframe on the core log.
GRM.SetLogValues = function ( indString , indLog )
    if indString <= #GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons then

        local logFontString = GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[indString][2];
        local logCount = GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[indString][3];
        local logEntry = GRM_G.fullLogMatch[indLog][2];
        local r , g , b = GRM.GetMessageRGB ( GRM_G.fullLogMatch[indLog][1] );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[indString][4] = indLog;

        -- Reset the sizing based on settings
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showLineNumbers then
            
            if GRM_G.fullLogMatch[indLog][1] == 98 then
                logCount:SetTextColor ( 1 , 1 , 1 , 0 );
                GRM_G.OldLogHeaderIsOn = true;
            elseif GRM_G.fullLogMatch[indLog][1] == 97 then
                logCount:SetTextColor ( 1 , 1 , 1 , 0 );
            elseif GRM_G.fullLogMatch[indLog][3] and not ( indLog > 1 and GRM_G.fullLogMatch[indLog][4] == GRM_G.fullLogMatch[indLog - 1][4] and GRM_G.fullLogMatch[indLog - 1][1] ~= 97 ) then                                               -- if true it should be red, indicating selection
                logCount:SetTextColor ( 1 , 0 , 0 , 1 );
            elseif indLog > 1 and GRM_G.fullLogMatch[indLog][4] == GRM_G.fullLogMatch[indLog - 1][4] and GRM_G.fullLogMatch[indLog - 1][1] ~= 97 then
                -- Hide the count
                logCount:SetTextColor ( 1 , 1 , 1 , 0 );
            else
                logCount:SetTextColor ( 1 , 1 , 1 , 1) ;
            end
            
            logCount:SetText ( GRM_G.fullLogMatch[indLog][5] .. ")" );
            logCount:Show();
            if GRM_G.OldLogHeaderIsOn and indLog > ( #GRM_LogReport_Save[GRM_G.F][GRM_G.guildName] - GRM_G.IndexOfLastLogEntry ) then
                indLog = indLog - 1;
            end
        else
            logCount:Hide();
        end

        logFontString:SetTextColor ( r , g , b , 1 );
        logFontString:SetText ( logEntry );
        logFontString:Show();

        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[indString][1]:Show();

        if GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogTooltip:IsVisible() and GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[indString][1]:IsMouseOver() then 
            GRM.RefreshLogTooltip ( GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[indString][1] );
        end
    end
end

-- Method:          GRM.GetOperatorsFromText ( string )
-- What it Does:    Adds operator controls to the search strings of the log
-- Purpose:         Quality of life improvement on searching the log
GRM.GetOperatorsFromText = function ( text )
    local operators = {};
    local operatorControls = { string.char (124) };

    for i = 1 , #operatorControls do
        if string.find ( text , operatorControls[i] , 1 , true ) ~= nil then
            table.insert ( operators , i );
            -- text = string.gsub ( text , operatorControls[i] , "" );     -- Clear Operator after having been found
        end
    end

    return GRM.Trim ( text ) , operators;
end

-- Method:          GRM.GetSearchLog ( bool , string )
-- What it Does:    Pre-builds all the values to display based on the various filter parameters.
-- Purpose:         Make building a hybridscrollframe for the log that much simpler, and keeps it from reprocessing over and over.
GRM.GetSearchLog = function ( isSearch , searchString )
    local result = {};
    local needsToAddSearchString = false;
    local trueString = false;
    local index = 0;
    local logTxt = "";
    local totalCount = 0;
    local gLog = GRM_LogReport_Save[GRM_G.F][GRM_G.guildName];

    -- Keept it simple and reusable add tool
    local addLogEntry = function( tempLog , text , uneditedText , ind , point , count )
        if string.find ( text , "\n" ) ~= nil then
            while string.find ( text , "\n" ) ~= nil do
                table.insert ( tempLog , { ind , string.sub ( text , 1 , string.find ( text , "\n" ) -1 ) , false , point , count , uneditedText } );
                text = string.sub ( text , string.find ( text , "\n" ) + 1 );
            end
        end
        table.insert ( tempLog , { ind , text , false , point , count , uneditedText } );
        return tempLog;
    end

    -- Set possible first Entry
    if GRM_G.FirstTimeViewed and GRM_G.IndexOfLastLogEntry < #gLog and #gLog > 0 and GRM_G.IndexOfLastLogEntry ~= #gLog then
        table.insert ( result , { 97 , " " , false , 0 , 0 , GRM.AddNewChangesHeader() } );
        table.insert ( result , { 97 , GRM.AddNewChangesHeader() , false , 0 , 0 , GRM.AddNewChangesHeader() } );
    end

    local i = #gLog;
    local changesEndAdded = false;
    while i > 0 do
        trueString = false;
        needsToAddSearchString = false;
        if GRM_G.FirstTimeViewed and not changesEndAdded and GRM_G.IndexOfLastLogEntry < #gLog and #gLog > 0 and i == GRM_G.IndexOfLastLogEntry and GRM_G.IndexOfLastLogEntry ~= #gLog then
            
            -- OLD LOG HEADER
            table.insert ( result , { 98 , GRM.AddOldLogHeader() , false , 0 , 0 , GRM.AddOldLogHeader() } );
            table.insert ( result , { 98 , " " , false , 0 , 0 , GRM.AddOldLogHeader() } );
            changesEndAdded = true;
        else
            -- Check buttons
            index = gLog[i][1];
            logTxt = gLog[i][2];

            -- first, determine if this is a searchString
            if isSearch and string.find ( logTxt , "\000" ) == nil and #searchString > 0 then

                -- Is this an operator search?
                if string.find ( searchString , "^" , 1 , true ) ~= nil and string.sub ( searchString , 1 , 1 ) == "^" then

                    -- Pending feature...
                    -- YES, this is operator control!!!
                    -- local operators = {};
                    searchString = GRM.GetOperatorsFromText ( string.sub ( searchString , 2 ) );

                    if string.find ( string.lower ( string.gsub ( logTxt , "|r" , "" ) ) , searchString , 1 , true ) ~= nil then -- Comparing 2 non-case-sensitive strings
                        needsToAddSearchString = true;
                    end

                elseif string.find ( string.lower ( string.gsub ( logTxt , "|r" , "" ) ) , searchString , 1 , true ) ~= nil then -- Comparing 2 non-case-sensitive strings
                    needsToAddSearchString = true;
                end
            end
            if index == 1 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toLog"].promotion then      -- Promotion 
                trueString = true;
            elseif index == 2 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toLog"].demotion then  -- Demotion
                trueString = true;
            elseif index == 3 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toLog"].leveled then  -- Leveled
                local num = "";
                if string.find ( logTxt , "***" ) == nil then
                    num = GRM.Trim ( string.match ( logTxt , " %d+ " ) );
                else
                    num = string.match ( string.match ( logTxt , "cffafffdc%d+" ) , "%d+" );
                end
                if tonumber ( num ) >= GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].levelReportMin then
                    trueString = true;
                end
                
            elseif index == 4 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toLog"].note then  -- Note
                trueString = true;
            elseif index == 5 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toLog"].officerNote then  -- OfficerNote
                trueString = true;
            elseif index == 6 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toLog"].rankRename then  -- rankRename
                trueString = true;
            elseif ( index == 7 or index == 8 ) and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toLog"].joined then  -- Join/Rejoin
                trueString = true;
            elseif index == 10 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toLog"].left then -- Left Guild
                trueString = true;
            elseif index == 11 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toLog"].nameChange then -- NameChange
                trueString = true;
            elseif index == 14 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toLog"].inactiveReturn then -- Return from inactivity
                trueString = true;
            elseif index == 15 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toLog"].eventAnnounce then -- Event Announcement
                trueString = true;
            elseif ( index == 16 or index == 22 or index == 23 ) and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toLog"].recommend then -- recommendations
                trueString = true;
            elseif ( index == 17 or index == 18 or index == 20 or index == 21 ) and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toLog"].banned then  -- ban info
                trueString = true;
            elseif ( index == 9 or index == 12 or index == 13 ) and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toLog"].joined then  -- Banned Rejoin
                trueString = true;
            elseif index == 19 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toLog"].customNote then
                trueString = true;
            end
            if trueString then
                if ( isSearch and needsToAddSearchString) or ( not isSearch ) then
                    totalCount = totalCount + 1;
                    result = addLogEntry ( result , logTxt , logTxt , index , i , totalCount );
                end
            end
            i = i - 1;
        end
    end

    return result , totalCount;
end

-- Method:          GRM.SetColoredLines()
-- What it Does:    Sets all of the instances in the temp filtered log as true or false, indicating which lines should be red or not.
-- Purpose:         Line coloring!!!
GRM.SetColoredLines = function()
    local number = tonumber ( GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogExtraOptionsFrame.GRM_LogExtraEditBox1:GetText() );
    local number2 = tonumber ( GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogExtraOptionsFrame.GRM_LogExtraEditBox2:GetText() );
    local clearAll = false;

    if ( number2 < number) then
        clearAll = true;
    end

    if not ( number == 0 and number == number2 ) and number <= number2 or clearAll then
        for i = 1 , #GRM_G.fullLogMatch do
            if not clearAll and GRM_G.fullLogMatch[i][5] >= number and GRM_G.fullLogMatch[i][5] <= number2 then
                GRM_G.fullLogMatch[i][3] = true;
            else
                GRM_G.fullLogMatch[i][3] = false
            end
        end
    end   
end

-- Method:          GRM.BuildLogComplete()
-- What it Does:    Checks the editbox and sees whether to build the log normal, or to auto-rebuild the log based on the custom text filter.
-- Purpose:         The Call to rebuild the log is done about 50 times. This cleans up the code bloat.
GRM.BuildLogComplete = function( UIControl , fullRefresh )
    if GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then

        if GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogEditBox:GetText() ~= "" and GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogEditBox:GetText() ~= GRM.L ( "Search Filter" ) then
            GRM.BuildLog ( GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogEditBox:GetText() , fullRefresh );
        else
            GRM.BuildLog( nil , fullRefresh );
        end
        
        -- Set Focus on the bar automatically...
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].autoFocusSearch and not GRM_G.OnFirstLoad and not UIControl and not GRM_G.SearchFocusControl then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogEditBox:SetFocus();
            GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogEditBox:SetText ( "" );
        end
    end
end

-- Method:          GRM.BuildHybridLog( string )
-- What it Does:    Builds the guildLog frame details for the scrollframe
-- Purpose:         You aren't tracking all of that info for nothing!
GRM.BuildLog = function ( searchString , fullRefresh )
    local isSearch = false;
    local hybridScrollFrameButtonCount = 25;        -- Exactly 25 buttons
    local buttonHeight = 17.08
    local buttonWidth = GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollFrame:GetWidth() - 5;

    if searchString ~= nil and type ( searchString ) == "string" then
        searchString = string.lower ( searchString ); -- Remove case sensitivity
        isSearch = true;
    end

    -- Only rebuild the log if necessary based on new parameters.
    if fullRefresh then        
        GRM_G.fullLogMatch , GRM_G.CurrentTotalCount = GRM.GetSearchLog ( isSearch , searchString );
    end

    local log = GRM_G.fullLogMatch;
    
    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.Offset = GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.Offset or hybridScrollFrameButtonCount;

    if GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.Offset < hybridScrollFrameButtonCount then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.Offset = hybridScrollFrameButtonCount;
    elseif GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.Offset > hybridScrollFrameButtonCount and GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.Offset > #log then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.Offset = #log;
    end

    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons = GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons or {};
    
    for i = 1 , #log do
        -- Build HybridScrollFrame Buttons
        if i <= hybridScrollFrameButtonCount then
            if not GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i] then
                local button = CreateFrame ( "Button" , "LogButton" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i] = {
                    button ,
                    button:CreateFontString ( "GRM_LogEntry_" .. i ) ,
                    button:CreateFontString ( "GRM_LogCount_" .. i , "OVERLAY" , "GameFontWhiteTiny" ),
                    0   -- index of the log
                };

                if i == 1 then
                    button:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame , "TOP" , -1 , 0 );
                else 
                    button:SetPoint ( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i-1][1] , "BOTTOMLEFT" , 0 , 0 );
                end

                GRM.BuildCoreLogFontstrings ( i , buttonHeight , buttonWidth );
            end
        end
        if ( i >= ( GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.Offset - hybridScrollFrameButtonCount + 1 ) and i <= GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.Offset ) then
            GRM.SetLogValues ( i - ( GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.Offset - hybridScrollFrameButtonCount ) , i );
        end
    end

    -- Remove the buffer on one of them due to the top not needing it.
    -- Hide unused buttons...
    for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons do
        if i > #log then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i][1]:Hide();
        else
            GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i][1]:Show();
        end
    end

    GRM.SetHybridScrollFrameSliderParameters ( 
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame , GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollFrame , GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollFrameSlider , 
        buttonWidth , buttonHeight , buttonHeight * #log , #log , GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons , 
        GRM.LogToolHybridShiftDown , GRM.LogToolHybridShiftUP , hybridScrollFrameButtonCount
    );

    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogFrameNumEntriesText:SetText ( GRM.L ( "Total Entries: {num}" , nil , nil , GRM_G.CurrentTotalCount ) );
end

-- Method:          GRM.BuildCoreLogFontstrings ( int , float , int )
-- What it Does:    Initiates the fontstring values for the Core Log
-- Purpose:         Compartmentalize the code for easier call back to.
GRM.BuildCoreLogFontstrings = function ( ind , size , buttonWidth , isResizeAction )
    local button = GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[ind][1];
    local logFontString = GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[ind][2];
    local logCount = GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[ind][3];
    local modifier = 7;
    local stringWidth = buttonWidth - 5
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showLineNumbers then
        modifier = 8.5;
        stringWidth = buttonWidth - 45
        logFontString:SetPoint ( "LEFT" , button , "LEFT" , 36 , 0 )
    else
        logFontString:SetPoint ( "LEFT" , button , "LEFT" , 0 , 0 )
    end

    logCount:SetPoint ( "LEFT" , button , "LEFT" , 0 , 0 );
    logCount:SetJustifyH ( "LEFT" );
    logCount:SetWidth ( 35 );
    logCount:SetTextColor ( 1 , 1 , 1 );
    logCount:SetWordWrap ( false );
    logCount:SetFont ( GRM_G.FontChoice , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logFontSize + size - modifier );

    logFontString:SetJustifyH ( "LEFT" );
    logFontString:SetWidth ( stringWidth );
    logFontString:SetWordWrap ( false );

    logFontString:SetFont ( GRM_G.FontChoice , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logFontSize + size - modifier );

    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogTooltip.GRM_LogTooltipText1:SetFont ( GRM_G.FontChoice , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logFontSize + size - modifier );

    if not isResizeAction then
        button:SetSize ( buttonWidth , size );
        button:RegisterForDrag ( "LeftButton" );
        
        -- Tooltips
        button:SetScript ( "OnEnter" , function ( self )
            GRM.RefreshLogTooltip ( self );
        end);
        button:SetScript ( "OnLeave" , function ()
            GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogTooltip:Hide();
        end);
        button:SetScript ( "OnDragStart" , function()
            GRM_UI.GRM_RosterChangeLogFrame:StartMoving();
        end);
        button:SetScript ( "OnDragStop" , function()
            GRM_UI.GRM_RosterChangeLogFrame:StopMovingOrSizing();
            GRM_UI.SaveCorePosition();
        end);

        -- Log One Click Behavior
        button:SetScript ( "OnClick" , function ( self , buttonClicked ) 
            if buttonClicked == "LeftButton" then
                if IsControlKeyDown() and IsShiftKeyDown() and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].shiftClickRemove and not GRM_UI.GRM_RosterConfirmFrame:IsVisible() then
                    for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons do
                        if GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i][1] == self then
                            if GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i][1]:IsMouseOver() then
                                GRM.RemoveItemFromLog ( GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i][4] , true , true , GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i][5] );
                            end
                            break;
                        end
                    end
                end
            end
        end);
    end

end

-- Method:          GRM.RefreshLogTooltip()
-- What it Does:    Refreshes the tooltip of the button moused over in the log that has text that is too long
-- Purpose:         Enhanced log features
GRM.RefreshLogTooltip = function( button )
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showTooltip then
        for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons do
            if GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i][1] == button then
                local text = GRM_G.fullLogMatch[GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i][4]][6];

                GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogTooltip.GRM_LogTooltipText1:SetText ( text );
                local width = GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogTooltip.GRM_LogTooltipText1:GetWidth();

                local r , g , b = GRM.GetMessageRGB ( GRM_G.fullLogMatch[GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i][4]][1] );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogTooltip:SetOwner ( button , "ANCHOR_CURSOR" );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogTooltip:AddLine ( "|CFFE6CC7F" .. GRM.L ( "Full Log Message:" ) );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogTooltip:AddLine ( " " );

                if width > ( GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i][2]:GetWidth() + 200 ) then
                    local index = string.find ( text , "%s" , math.floor ( #text / 2 ) );
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogTooltip:AddLine ( string.sub ( text , 1 , index ) , r , g , b );
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogTooltip:AddLine ( string.sub ( text , index + 1 ) , r , g , b );
                else
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogTooltip:AddLine ( text , r , g , b );
                end

                GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogTooltip:Show();
                break;
            end
        end
    end
end

-- Method:          GRM.ResetLogStringPoints ( bool )
-- What it Does:    Sets point of all the built fontstrings to account for spacing needed for the count or not and resets the size width
-- Purpose:         Aesthetics on the log
GRM.ResetLogStringPoints = function ( countEnabled )
    local position = 0;
    local modifier = 7;
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showLineNumbers then
        modifier = 8.5;
    end
    local width = GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollFrame:GetWidth() - 10;
    if countEnabled then
        position = 36;
        width = GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollFrame:GetWidth() - 50;
    end

    for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i][2]:SetPoint ( "LEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i][1] , "LEFT" , position , 0 );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i][2]:SetWidth ( width );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i][2]:SetFont ( GRM_G.FontChoice , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logFontSize + 17.08 - modifier );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons[i][3]:SetFont ( GRM_G.FontChoice , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logFontSize + 17.08 - modifier );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogTooltip.GRM_LogTooltipText1:SetFont ( GRM_G.FontChoice , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].logFontSize + 17.08 - modifier );
    end
end

-- Method:          GRM.AddOldLogHeader()
-- What it Does:    Returns the string of the Old Log header
-- Purpose:         Cleaner Log reporting.
GRM.AddOldLogHeader = function ()
    return ( "|cffffd600----------   " .. GRM.L ( "OLD LOG" ) .. "  ----------|r" );
end

-- Method:          GRM.AddNewChangesHeader()
-- What it Does:    Returns the string of the "Changes" header since last logged in the log
-- Purpose:         Cleaner log reporting
GRM.AddNewChangesHeader = function ()
    return ( "|cffffd600----------  " .. GRM.L ( "CHANGES" ) .. "  ----------|r" )
end

-- Method:          GRM.ExportMemberDetailsHeaders()
-- What it Does:    Creates a Headers Export
-- Purpose:         So player can easily build export headers for the data.
GRM.ExportMemberDetailsHeaders = function ( returnString )
    local scrollWidth = GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogScrollFrame:GetWidth() - 3;
    local headers = { GRM.L ( "Name" ) , GRM.L ( "Rank" ) , GRM.L ( "Level" ) , GRM.L ( "Class" ) , GRM.L ( "Race" ) , GRM.L ( "Sex" ) , GRM.L ( "Last Online (Days)" ) , GRM.L ( "Main/Alt" ) , GRM.L ( "Player Alts" ) , GRM.L ( "Join Date" ) , GRM.L ( "Promo Date" ) , GRM.L ( "Rank History" ) , GRM.L ( "Birthday" ) , GRM.L ( "Guild Rep" ) , GRM.L ( "Public Note" ) , GRM.L ( "Officer Note" ) , GRM.L ( "Custom Note" ) };
    local delimiter = "";
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportDelimiter[1] then
        delimiter = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportDelimiter[2];
    end
    local completeString = "";
    local index = 1;

    for i = 1 , #headers do
        if i == 5 or i == 6 or i == 9 or i == 12 then
            if ( i == 5 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[15] ) then
                completeString = completeString .. headers[i] .. delimiter;
            elseif ( i == 6 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[16] ) then
                completeString = completeString .. headers[i] .. delimiter;
            elseif ( i == 9 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[14] ) then
                completeString = completeString .. headers[i] .. delimiter;
            elseif ( i == 12 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[18] ) then
                completeString = completeString .. headers[i] .. delimiter;
            end
        else
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[index] then
                completeString = completeString .. headers[i] .. delimiter;
            end
            index = index + 1;
        end
    end

    if #completeString > 0 then
        local modifier = 1;

        if delimiter == "||" or delimiter == "\\t" or delimiter == "::" then
            modifier = 2;
        end
        completeString = string.sub ( completeString , 1 , #completeString - modifier );  -- eliminate the last delimiter
    end

    if not returnString and completeString ~= "" then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogFrameEditBox:SetText ( completeString );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogFrameEditBox:HighlightText ( 0 );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogFrameEditBox:Show();
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLoadingText:Hide();
        C_Timer.After ( 2 , function()
            GRM.ExportScrollSliderConfigure( scrollWidth );
        end);
    elseif returnString then
        return completeString;
    end
end

-- Method:          GRM.convertToArrayFormat()
-- What it Does:    Converts the format into a usable one for sync and also exporting in a sorted, proper order.
-- Purpose:         Clean UI management for the user.
GRM.convertToArrayFormat = function()
    GRMsyncGlobals.guildData = {};
    GRMsyncGlobals.formerGuildData = {};
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    local i = 1;

    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            GRMsyncGlobals.guildData[i] = player;
            i = i + 1;
        end
    end
    sort ( GRMsyncGlobals.guildData , function ( a , b ) return a.name < b.name end );

    guildData = GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ];

    i = 1;
    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            GRMsyncGlobals.formerGuildData[i] = player;
            i = i + 1;
        end
    end
    sort ( GRMsyncGlobals.formerGuildData , function ( a , b ) return a.name < b.name end );
    -- table.insert ( GRMsyncGlobals.formerGuildData , 1 , { GRM_G.guildName } );
end

-- Method:          GRM.BuildExportMemberDetails( int )
-- What it Does:    Exports the player details as a text stirng with a delimitter
-- Purpose:         Allow the player to easily export the information.
GRM.BuildExportMemberDetails = function( currentMembers )
    local scrollWidth = GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogScrollFrame:GetWidth() - 3;
    local completeString = "";
    local delimiter = "";
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportDelimiter[1] then
        delimiter = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportDelimiter[2];
    end
    local altList = {};
    local altString = "";
    local isMergedRealm = GRM.IsMergedRealmServer();
    local playerDetails = "";
    local sex = "";
    local separator = ",";
    if delimiter == separator then
        separator = ";";
    end
    local rankHistory = "";

    -- Build the arrays to use.
    GRM.convertToArrayFormat();
    local roster = GRMsyncGlobals.guildData;
    if not currentMembers then
        roster = GRMsyncGlobals.formerGuildData;
    end

    local num1 = tonumber ( GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportRangeEditBox1:GetText() );
    local num2 = tonumber ( GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportRangeEditBox2:GetText() );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogFrameEditBox:SetText ( "" );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogFrameEditBox:SetSize ( scrollWidth , 12 );   -- Default Size at one line

    local mainOnly , altOnly = false , false;

    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[17] then
        mainOnly = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[19];

        if not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[19] then
            altOnly = true;
        end
    end

    -- Validates if it should be added to the string based on the conditions
    local isValid = function ( player )
        local result = true;

        if mainOnly and not player.isMain then
            result = false;
        elseif altOnly and ( player.isMain or ( not player.isMain and not GRM.PlayerIsAnAlt ( player ) ) ) then
            result = false;
        end 

        return result;
    end

    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].columnHeaders and num1 == 1 then
        completeString = GRM.ExportMemberDetailsHeaders ( true ) .. "\n";
    end

    for i = num1 , num2 do
        playerDetails = "";
        sex = "";
        if num1 <= #roster then
            
            if isValid ( roster[i] ) then

                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[1] then
                    if isMergedRealm then
                        playerDetails = playerDetails .. roster[i].name .. delimiter;                                   -- name
                    else
                        playerDetails = playerDetails .. GRM.SlimName ( roster[i].name ) .. delimiter;                  -- name
                    end
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[2] then
                    playerDetails = playerDetails .. roster[i].rankName .. delimiter;                                   -- rank
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[3] then
                    playerDetails = playerDetails .. roster[i].level .. delimiter;                                      -- level
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[4] then
                    playerDetails = playerDetails .. C_CreatureInfo.GetClassInfo ( classFileIDEnum[roster[i].class] ).className .. delimiter;   -- class
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[15] then
                    if roster[i].race ~= "" then 
                        playerDetails = playerDetails .. C_CreatureInfo.GetRaceInfo ( raceIDEnum[roster[i].race] ).raceName .. delimiter;   -- Race
                    else
                        playerDetails = playerDetails .. GRM.L ( "Unknown" ) .. delimiter;
                    end
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[16] then
                    if roster[i].sex ~= 1 then
                        sex = GRM.GetSex ( roster[i].sex );
                        playerDetails = playerDetails .. sex .. delimiter;   -- Sex
                    else
                        playerDetails = playerDetails .. GRM.L ( "Unknown" ) .. delimiter;
                    end
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[5] then
                    if roster[i].lastOnline < 24 then
                        playerDetails = playerDetails .. GRM.Round ( roster[i].lastOnline / 24 , 2 ) .. delimiter;         -- Last Online  (in days, round to 2)
                    else
                        playerDetails = playerDetails .. math.floor ( ( roster[i].lastOnline / 24 ) + 0.5 ) .. delimiter;  -- keep it rounded to the nearest integer
                    end
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[6] then                                   -- Main or Alt
                    if roster[i].isMain then
                        playerDetails = playerDetails .. GRM.L ( "Main" ) .. delimiter;
                    else

                        if GRM.PlayerIsAnAlt ( roster[i] ) then
                            playerDetails = playerDetails .. GRM.L ( "Alt" ) .. delimiter;
                        else
                            playerDetails = playerDetails .. delimiter;
                        end
                    end
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[14] then                                  -- Player Alts
                    if #roster[i].alts > 0 then
                        altList = {};
                        altString = "";

                        for j = 1 , #roster[i].alts do
                            table.insert ( altList , roster[i].alts[j][1] );
                            if roster[i].alts[j][5] then
                                altList[#altList] = altList[#altList] .. GRM.L ( "(main)" );
                            end
                        end
                        sort ( altList );
                        
                        for j = 1 , #altList do
                            if j < #altList then
                                altString = altString .. altList[j] .. separator;
                            else
                                altString = altString .. altList[j];            -- No need to add a comma at the end of the list
                            end
                        end

                        playerDetails = playerDetails .. altString .. delimiter;
                    else
                        playerDetails = playerDetails .. delimiter;
                    end
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[7] then                                   -- Join Date
                    if roster[i].joinDateUnknown then
                        playerDetails = playerDetails .. GRM.L ( "Unknown" ) .. delimiter;
                    else
                        if #roster[i].joinDate == 0 then
                            playerDetails = playerDetails .. delimiter;
                        else
                            playerDetails = playerDetails .. ( GRM.DateUntrustedTag ( roster[i].verifiedJoinDate ) .. GRM.FormatTimeStamp ( roster[i].joinDate[#roster[i].joinDate] , false ) ) .. delimiter;
                        end
                    end
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[8] then                                   -- Promo Date
                    if roster[i].promoteDateUnknown then
                        playerDetails = playerDetails .. GRM.L ( "Unknown" ) .. delimiter;
                    else
                        if roster[i].verifiedPromoteDate[1] == "" then      --- Promotion has never been recorded!
                            playerDetails = playerDetails .. delimiter;
                        else
                            playerDetails = playerDetails .. ( GRM.DateUntrustedTag ( roster[i].verifiedPromoteDate ) .. GRM.FormatTimeStamp ( roster[i].verifiedPromoteDate[1] , false ) ) .. delimiter;
                        end
                    end
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[18] then                                   -- Rank History
                    rankHistory = GRM.GetRankHistory ( roster[i] , delimiter );
                    if #rankHistory == 0 then
                        playerDetails = playerDetails .. delimiter
                    else
                        playerDetails = playerDetails .. rankHistory .. delimiter;
                    end
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[9] then                                   -- Birthday
                    if roster[i].birthdayUnknown then
                        playerDetails = playerDetails .. GRM.L ( "Unknown" ) .. delimiter;
                    else
                        -- Checking button vs text
                        if roster[i].events[2][1][1] ~= 0 then
                            playerDetails = playerDetails .. GRM.FormatTimeStamp ( roster[i].events[2][3] , false , true ) .. delimiter;
                        else
                            playerDetails = playerDetails .. delimiter;
                        end
                    end
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[10] then
                    if GRM_G.BuildVersion >= 40000 then                                                             -- Reputation (Cataclysm or higher)
                        playerDetails = playerDetails .. GRM.GetReputationTextLevel ( roster[i].guildRep , false ) .. delimiter;
                    else
                        playerDetails = playerDetails .. delimiter;
                    end
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[11] then                                  -- Public Note
                    if roster[i].note ~= nil and roster[i].note ~= "" then
                        playerDetails = playerDetails .. string.gsub ( roster[i].note , delimiter , "" ) .. delimiter;
                    else
                        playerDetails = playerDetails .. delimiter;
                    end
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[12] then                                  -- Officer Note
                    if GRM.CanViewOfficerNote() and roster[i].officerNote ~= nil and roster[i].officerNote ~= "" then
                        playerDetails = playerDetails .. string.gsub ( roster[i].officerNote , delimiter , "" ) .. delimiter;
                    else
                        playerDetails = playerDetails .. delimiter;
                    end
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportFilters[13] then                                  -- Custom Note
                    if roster[i].customNote[6] ~= "" then
                        playerDetails = playerDetails .. string.gsub ( string.gsub ( roster[i].customNote[6] , "\n" , separator ) , delimiter , "" ) .. delimiter;
                    else 
                        playerDetails = playerDetails .. delimiter;
                    end
                end                    

                if #playerDetails > 0 then
                    local modifier = 1;

                    if delimiter == "||" or delimiter == "\\t" or delimiter == "::" then
                        modifier = 2;
                    end
                    playerDetails = string.sub ( playerDetails , 1 , #playerDetails - modifier );  -- eliminate the last delimiter
                    if i < num2 then
                        completeString = completeString .. playerDetails .. "\n";
                    else
                        completeString = completeString .. playerDetails;               -- Don't need to add the line break at the end.
                    end 
                end
            end
        else
            break;
        end
    end

    if completeString ~= "" then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogFrameEditBox:HighlightText ( 0 );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogFrameEditBox:Show();
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogFrameEditBox:SetText ( completeString );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLoadingText:Hide();
        
        C_Timer.After ( 2 , function()
            GRM.ExportScrollSliderConfigure( scrollWidth );
        end);
    end
end

-- Method:          GRM.DelimiterFix ( string , bool )
-- What it Does:    Replaces any commas with a semicolon
-- Purpose:         To allow comma delimiter in the export details
GRM.DelimiterFix = function ( entry , isComma )
    local result = "";

    if isComma then
        result = string.gsub ( entry , "," , ";" );     -- Use semicolon in place of all commas if player is using comma as delimiter
    else
        result = entry;
    end    

    return result;
end

-- Method:          GRM.BuildExportLogFrame()
-- What it Does:    Exactly as named... adds the entire guild log from the given guild, parses out the coloring, and makes it easy to copy and paste it
-- Purpose:         To allow players the ability to export their logs to a file somewhere to keep their system from getting too clutters.
GRM.BuildExportLogFrame = function()
    local scrollWidth = GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogScrollFrame:GetWidth() - 3;
    local completeString = "";
    local limit = 0;
    local delimiter = "";
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportDelimiter[1] then
        delimiter = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportDelimiter[2];
    end

    -- To be able to use comma as delimiter we will need to substitue something else in place of comma.
    local isComma = false;
    if delimiter == "," then
        isComma = true;
    end

    local num1 = tonumber ( GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportRangeEditBox1:GetText() );
    local num2 = tonumber ( GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportRangeEditBox2:GetText() );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogFrameEditBox:SetText ( "" );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogFrameEditBox:SetSize ( scrollWidth , 12 );   -- Default Size at one line

    local log = GRM_G.fullLogMatch;
    
    if #log > 0 then
        
        limit = num2;

        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLoadingText:SetText ( GRM.L ( "Building Log for Export..." ) );
        
        
        local i = num1;
        local newCount = true;
        local j;

        -- Need to find a starting point.
        if i <= limit then
            for k = i , #log do
                if log[k][5] == num1 then
                    j = k;
                    break;
                end
            end
        end

        local additionalNotes = false;
        local entry = "";
        
        while i <= limit do
            newCount = true;
            additionalNotes = false;
            while j <= #log and ( log[j][5] == i or log[j][5] == 0 ) do

                if newCount and j <= #log and log[j][5] ~= 0 then
                    newCount = false;
                    entry = GRM.DelimiterFix ( log[j][2] , isComma );

                    if i == 1 then
                        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showLineNumbers then
                            
                            if additionalNotes or ( not additionalNotes and string.find ( entry , GRM.L ( "Additional Notes:" ) , 1 , true ) ~= nil ) then      -- Don't want to delimit breakup the custom note
                                additionalNotes = true;
                                completeString = completeString .. i .. ") " .. GRM.RemoveStringColoring ( entry );
                            else
                                completeString = completeString .. i .. ") " .. GRM.RemoveStringColoring ( entry ) .. delimiter;
                            end
                        else
                            completeString = completeString .. GRM.RemoveStringColoring ( entry ) .. delimiter;
                        end
                    else
                        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showLineNumbers then
                            
                            if additionalNotes or ( not additionalNotes and string.find ( entry , GRM.L ( "Additional Notes:" ) , 1 , true ) ~= nil ) then      -- Don't want to delimit breakup the custom note
                                additionalNotes = true;
                                completeString = completeString .. "\n" .. i .. ") " .. GRM.RemoveStringColoring ( entry );
                            else
                                completeString = completeString .. "\n" .. i .. ") " .. GRM.RemoveStringColoring ( entry ) .. delimiter;
                            end
                        else
                            completeString = completeString .. "\n" .. GRM.RemoveStringColoring ( entry ) .. delimiter;
                        end
                    end
                else
                    if log[j][5] ~= 0 then
                        entry = GRM.DelimiterFix ( log[j][2] , isComma );

                        if additionalNotes or ( not additionalNotes and string.find ( entry , GRM.L ( "Additional Notes:" ) , 1 , true ) ~= nil ) then
                            additionalNotes = true;
                            if string.find ( entry , GRM.L ( "Additional Notes:" ) , 1 , true ) ~= nil then
                                completeString = completeString .. GRM.RemoveStringColoring ( entry ) .. delimiter;
                            else
                                completeString = completeString .. GRM.RemoveStringColoring ( entry );
                            end
                        else
                            completeString = completeString .. GRM.RemoveStringColoring ( entry ) .. delimiter;
                        end
                    end
                end
                if j == #log or ( log[j+1] ~= nil and log[j+1][5] ~= i ) then
                    completeString = string.sub ( completeString , 1 , #completeString - 1 );
                end
                j = j + 1;
                
            end

            i = i + 1;
        end
    end

    if limit == 0 then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLoadingText:SetText ( GRM.L ( "The Log is Currently Empty for This Guild" ) );
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogFrameEditBox:Hide();
    GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLoadingText:Show();
    if completeString ~= "" then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogFrameEditBox:SetText ( completeString );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogFrameEditBox:HighlightText ( 0 );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogFrameEditBox:Show();
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLoadingText:Hide();
        C_Timer.After ( 2 , function()
            GRM.ExportScrollSliderConfigure( scrollWidth );
        end);
    end
end

-- Method:          GRM.BuildExportDelimiterDropdownMenu()
-- What it Does:    Builds the dropdown menu values for the delimiters on the export window
-- Purpose:         Allow Delimiter Selection
GRM.BuildExportDelimiterDropdownMenu = function()
    local delimiters = { ";" , "~" , "," , "::" , "||" , "\\t" };
    local buffer = 6;
    local height = 0;

    -- Initiate the buttons holder
    GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_DelimiterDropdownMenu.Buttons = GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_DelimiterDropdownMenu.Buttons or {};

    for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_DelimiterDropdownMenu.Buttons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_DelimiterDropdownMenu.Buttons[i][1]:Hide();
    end
    
    for i = 1 , #delimiters do
        if not GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_DelimiterDropdownMenu.Buttons[i] then
            local tempButton = CreateFrame ( "Button" , "GRM_DelimiterButton" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_DelimiterDropdownMenu );
            GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_DelimiterDropdownMenu.Buttons[i] = { tempButton , tempButton:CreateFontString ( "GRM_DelimiterText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) }
        end

        local DelimiterButton = GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_DelimiterDropdownMenu.Buttons[i][1];
        local DelimiterButtonText = GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_DelimiterDropdownMenu.Buttons[i][2];
        DelimiterButton:SetWidth ( 60 );
        DelimiterButton:SetHeight ( 16 );
        DelimiterButton:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        
        DelimiterButtonText:SetText ( "|cffffd600\"|r " .. delimiters[i] .. " |cffffd600\"|r" );
        DelimiterButtonText:SetTextColor ( 0.0 , 0.8 , 1.0 , 1.0 );
        DelimiterButtonText:SetWidth ( 55 );
        DelimiterButtonText:SetWordWrap ( false );
        DelimiterButtonText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 16 );
        DelimiterButtonText:SetPoint ( "CENTER" , DelimiterButton );
        DelimiterButtonText:SetJustifyH ( "CENTER" );

        if i == 1 then
            DelimiterButton:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_DelimiterDropdownMenu , 0 , -7 );
            height = height + DelimiterButton:GetHeight();
        else
            DelimiterButton:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_DelimiterDropdownMenu.Buttons[i - 1][1] , "BOTTOM" , 0 , -buffer );
            height = height + DelimiterButton:GetHeight() + buffer;
        end

        DelimiterButton:SetScript ( "OnClick" , function( self , button ) 
            if button == "LeftButton" then
                local parsedNumber = tonumber ( string.match ( self:GetName() , "(%d+)" ) );

                GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportDelimiter[2] = GRM.Trim ( string.gsub ( GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_DelimiterDropdownMenu.Buttons[parsedNumber][2]:GetText() , "|cffffd600\"|r" , "" ) );

                GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_DelimiterDropdownMenuSelected.GRM_DelimiterDropdownMenuText:SetText ( DelimiterButtonText:GetText() );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_DelimiterDropdownMenu:Hide();
                GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_DelimiterDropdownMenuSelected:Show();
                GRM.SyncSettings();
            end
        end);
        DelimiterButton:Show();
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_DelimiterDropdownMenu:SetHeight ( height + 15 );
end

-- Method:          GRM.ExportScrollSliderConfigure ( int , float )
-- What it Does:    Used in the building of the xport frame for the log. This sets the slider values.
-- Purpose:         Kept seperate so it can run recrusively on re-checking if necessary.
GRM.ExportScrollSliderConfigure = function ( scrollWidth )
    local scrollHeight = GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogFrameEditBox:GetHeight();
    GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogFrameEditBox:SetSize ( scrollWidth , scrollHeight + 10 );
    local scrollMax = ( scrollHeight - ( GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogScrollFrame:GetHeight() - 5 ) ) + GRM_G.FontModifier + 12;
    if scrollMax < 0 then
        scrollMax = 0;
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogScrollFrameSlider:SetMinMaxValues ( 0 , scrollMax );
    -- Mousewheel Scrolling Logic
    GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogScrollFrame:EnableMouseWheel( true );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogScrollFrame:SetScript( "OnMouseWheel" , function( _ , delta )
        local current = GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogScrollFrameSlider:GetValue();
        
        if IsShiftKeyDown() and delta > 0 then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogScrollFrameSlider:SetValue ( 0 );
        elseif IsShiftKeyDown() and delta < 0 then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogScrollFrameSlider:SetValue ( scrollMax );
        elseif delta < 0 and current < scrollMax then
            if IsControlKeyDown() then
                GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogScrollFrameSlider:SetValue ( current + 60 );
            else
                GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogScrollFrameSlider:SetValue ( current + 20 );
            end
        elseif delta > 0 and current > 1 then
            if IsControlKeyDown() then
                GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogScrollFrameSlider:SetValue ( current - 60 );
            else
                GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLogScrollFrameSlider:SetValue ( current - 20 );
            end
        end
    end);
end

-- Method:          GRM.ReconfigureLogMessage()
-- What it Does:    Reprocesses some logic everytime the log filters are adjusted
-- Purpose:         Live frame manipulation and updates.
GRM.ReconfigureLogMessage = function()
    if #GRM_G.fullLogMatch == 0 then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLoadingText:SetText ( GRM.L ( "The Log is Currently Empty for This Guild" ) );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLoadingText:Show();
    elseif GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLoadingText:IsVisible() then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.GRM_ExportLoadingText:Hide();
    end
end


-- Method:          GRM.BuildCustomNoteScrollFrame ( string )
-- What it Does:    Builds the custom note scroll frame
-- Purpose:         To have a customizable scrollable scrollframe!!!
GRM.BuildCustomNoteScrollFrame = function( customNote )
    if GRM_G.previousNote ~= customNote then
        GRM_G.previousNote = customNote;
        local scrollHeight = 0;
        local scrollWidth = 123;

        if customNote == "" and not GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:HasFocus() then
            customNote = GRM.L ( "Click here to set Custom Notes" );
        end
        GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:SetText ( customNote );

        -- Slight delay needed before you can do a GetHeight()
        C_Timer.After ( 0.05 , function()
            scrollHeight = GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:GetHeight();
            GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:SetSize ( scrollWidth , scrollHeight + 10 );

            local scrollMax = ( scrollHeight - 80 ) + GRM_G.FontModifier + 5;
            if scrollMax < 0 then
                scrollMax = 0;                
            end
            GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteScrollFrameSlider:SetMinMaxValues ( 0 , scrollMax );
            -- Mousewheel Scrolling Logic
            GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteScrollFrame:EnableMouseWheel( true );
            GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteScrollFrame:SetScript( "OnMouseWheel" , function( _ , delta )
                local current = GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteScrollFrameSlider:GetValue();
                
                if IsShiftKeyDown() and delta > 0 then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteScrollFrameSlider:SetValue ( 0 );
                elseif IsShiftKeyDown() and delta < 0 then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteScrollFrameSlider:SetValue ( scrollMax );
                elseif delta < 0 and current < scrollMax then
                    if IsControlKeyDown() then
                        GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteScrollFrameSlider:SetValue ( current + 60 );
                    else
                        GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteScrollFrameSlider:SetValue ( current + 20 );
                    end
                elseif delta > 0 and current > 1 then
                    if IsControlKeyDown() then
                        GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteScrollFrameSlider:SetValue ( current - 60 );
                    else
                        GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteScrollFrameSlider:SetValue ( current - 20 );
                    end
                end
            end);
        end);
    end
end

------------------------------------
---- BEGIN OF FRAME/UI LOGIC -------
---- General Framebuild Methods ----
------------------------------------


-- Method:          GRM.OnDropMenuClickDay()
-- What it Does:    Upon clicking any item in a drop down menu, this sets the ID of that item as defaulted choice
-- Purpose:         General use clicking logic for month based drop down menu.
GRM.OnDropMenuClickDay = function ()
    GRM_G.dayIndex = tonumber ( GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenuSelected.GRM_DayText:GetText() );
    GRM.InitializeDropDownDay();
end

-- Method:          GRM.OnDropMenuClickMonth()
-- What it Does:    Recalculates the logic of number days to show.
-- Purpose:         General use clicking logic for month based drop down menu.
GRM.OnDropMenuClickMonth = function ()
    GRM_G.monthIndex = monthsFullnameEnum [ GRM.OrigL ( GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected.GRM_MonthText:GetText() ) ];
    GRM.InitializeDropDownDay();
end

-- Method:          GRM.OnDropMenuClickYear()
-- What it Does:    Upon clicking any item in a drop down menu, this sets the ID of that item as defaulted choice
-- Purpose:         General use clicking logic for year based drop down menu.
GRM.OnDropMenuClickYear = function ()
    GRM_G.yearIndex = tonumber ( GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenuSelected.GRM_YearText:GetText() );
    GRM.InitializeDropDownDay();
end

-- Method:          GRM.InitializeDropDownDay ()
-- What it Does:    Initializes the Drop Down "Day" select window with values based on selected month
-- Purpose:         UI feature for easy date select.
GRM.InitializeDropDownDay = function ()
    local shortMonth = 30;
    local longMonth = 31;
    local febMonth = 28;
    local leapYear = 29;
    local yearDate = 0;

    yearDate = GRM_G.yearIndex;
    local isDateALeapyear = GRM.IsLeapYear(yearDate);
    local numDays;
    
    if GRM_G.monthIndex == 1 or GRM_G.monthIndex == 3 or GRM_G.monthIndex == 5 or GRM_G.monthIndex == 7 or GRM_G.monthIndex == 8 or GRM_G.monthIndex == 10 or GRM_G.monthIndex == 12 then
        numDays = longMonth;
    elseif GRM_G.monthIndex == 2 and isDateALeapyear then
        numDays = leapYear;
    elseif GRM_G.monthIndex == 2 then
        numDays = febMonth;
    else
        numDays = shortMonth;
    end
      
    -- populating the frames!
    local buffer = 3;
    local height = 0;
    GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenu.Buttons = GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenu.Buttons or {};

    -- Resetting the buttons!
    for i = 1 , #GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenu.Buttons do
        GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenu.Buttons[i][1]:Hide();
    end
    
    for i = 1 , numDays do
        if not GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenu.Buttons[i] then
            local tempButton = CreateFrame ( "Button" , "DayOfTheMonth" .. i , GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenu );
            GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenu.Buttons[i] = { tempButton , tempButton:CreateFontString ( "DayOfTheGRM_MonthText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) }
        end

        local DayButtons = GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenu.Buttons[i][1];
        local DayButtonsText = GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenu.Buttons[i][2];
        DayButtons:SetWidth ( 24 );
        DayButtons:SetHeight ( 10 );
        DayButtons:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        DayButtonsText:SetText ( i );
        DayButtonsText:SetWidth ( 25 );
        DayButtonsText:SetWordWrap ( false );
        DayButtonsText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 9 );
        DayButtonsText:SetPoint ( "CENTER" , DayButtons );
        DayButtonsText:SetJustifyH ( "CENTER" );

        if i == 1 then
            DayButtons:SetPoint ( "TOP" , GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenu , 0 , -7 );
            height = height + DayButtons:GetHeight();
        else
            DayButtons:SetPoint ( "TOP" , GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenu.Buttons[i - 1][1] , "BOTTOM" , 0 , -buffer );
            height = height + DayButtons:GetHeight() + buffer;
        end

        DayButtons:SetScript ( "OnClick" , function( _ , button ) 
            if button == "LeftButton" then
                GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenuSelected.GRM_DayText:SetText ( DayButtonsText:GetText() );
                GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenu:Hide();
                GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenuSelected:Show();
                GRM.OnDropMenuClickDay();
            end
        end); 

        DayButtons:Show();
    end
    GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenu:SetHeight ( height + 15 );
end

-- Method:          GRM.InitializeDropDownYear()
-- What it Does:    Initializes the year select drop-down OnDropMenuClick
-- Purpose:         Easy way to set when player joined the guild.         
GRM.InitializeDropDownYear = function ()
    -- Year Drop Down
    local currentYear = select ( 4 , GRM.CalendarGetDate() );
    local yearStamp = currentYear;

    -- populating the frames!
    local buffer = 2;
    local height = 0;
    GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenu.Buttons = GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenu.Buttons or {};

    -- Resetting the buttons!
    for i = 1 , #GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenu.Buttons do
        GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenu.Buttons[i][1]:Hide();
    end
    
    -- Game wasn't released until early 2004
    for i = 1 , currentYear - 2003 do
        if not GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenu.Buttons[i] then
            local tempButton = CreateFrame ( "Button" , "YearIndexButton" .. i , GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenu );
            GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenu.Buttons[i] = { tempButton , tempButton:CreateFontString ( "YearIndexButtonText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) }
        end

        local YearButtons = GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenu.Buttons[i][1];
        local YearButtonsText = GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenu.Buttons[i][2];
        YearButtons:SetWidth ( 40 );
        YearButtons:SetHeight ( 10 );
        YearButtons:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        YearButtonsText:SetText ( yearStamp );
        YearButtonsText:SetWidth ( 32 );
        YearButtonsText:SetWordWrap ( false );
        YearButtonsText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 9 );
        YearButtonsText:SetPoint ( "CENTER" , YearButtons );
        YearButtonsText:SetJustifyH ( "CENTER" );

        if i == 1 then
            YearButtons:SetPoint ( "TOP" , GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenu , 0 , -7 );
            height = height + YearButtons:GetHeight();
        else
            YearButtons:SetPoint ( "TOP" , GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenu.Buttons[i - 1][1] , "BOTTOM" , 0 , -buffer );
            height = height + YearButtons:GetHeight() + buffer;
        end

        YearButtons:SetScript ( "OnClick" , function( _ , button ) 
            if button == "LeftButton" then
                GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenuSelected.GRM_YearText:SetText ( YearButtonsText:GetText() );
                GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenu:Hide();
                GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenuSelected:Show();
                GRM.OnDropMenuClickYear();
            end
        end); 
        yearStamp = yearStamp - 1                       -- Descending the year by 1
        YearButtons:Show();
    end
    GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenu:SetHeight ( height + 15 );

end

-- Method:          GRM.InitializeDropDownMonth(self,level)
-- What it Does:    Initializes month drop select menu
-- Purpose:         Date select for Officer Note "Join Date"
GRM.InitializeDropDownMonth = function ()
    -- Month Drop Down
    local months = { "January" , "February" , "March" , "April" , "May" , "June" , "July" , "August" , "September" , "October" , "November" , "December" };
    
    -- populating the frames!
    local buffer = 3;
    local height = 0;
    GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenu.Buttons = GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenu.Buttons or {};

    -- Resetting the buttons!
    for i = 1 , #GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenu.Buttons do
        GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenu.Buttons[i][1]:Hide();
    end
    
    for i = 1 , #months do
        if not GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenu.Buttons[i] then
            local tempButton = CreateFrame ( "Button" , "monthIndex" .. i , GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenu );
            GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenu.Buttons[i] = { tempButton , tempButton:CreateFontString ( "monthIndexText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) }
        end

        local MonthButtons = GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenu.Buttons[i][1];
        local MonthButtonsText = GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenu.Buttons[i][2];
        MonthButtons:SetWidth ( 83 );
        MonthButtons:SetHeight ( 10 );
        MonthButtons:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        MonthButtonsText:SetText ( GRM.L ( months[i] ) );
        MonthButtonsText:SetWidth ( 83 );
        MonthButtonsText:SetWordWrap ( false );
        MonthButtonsText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 9 );
        MonthButtonsText:SetPoint ( "CENTER" , MonthButtons );
        MonthButtonsText:SetJustifyH ( "CENTER" );

        if i == 1 then
            MonthButtons:SetPoint ( "TOP" , GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenu , 0 , -7 );
            height = height + MonthButtons:GetHeight();
        else
            MonthButtons:SetPoint ( "TOP" , GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenu.Buttons[i - 1][1] , "BOTTOM" , 0 , -buffer );
            height = height + MonthButtons:GetHeight() + buffer;
        end

        MonthButtons:SetScript ( "OnClick" , function( _ , button ) 
            if button == "LeftButton" then
                GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected.GRM_MonthText:SetText ( MonthButtonsText:GetText() );
                GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenu:Hide();
                GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected:Show();
                GRM.OnDropMenuClickMonth();
            end
        end); 

        MonthButtons:Show();
    end
    GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenu:SetHeight ( height + 15 );
end

-- Method:          GRM.SetJoinDate ()
-- What it Does:    Sets the player's join date properly, be it the first time, a modified time, or an edit.
-- Purpose:         For so many uses! Anniversary tracking, for editing the date, and so on...
GRM.SetJoinDate = function ()
    local name = GRM_G.currentName;
    local dayJoined = tonumber ( GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenuSelected.GRM_DayText:GetText() );
    local yearJoined = tonumber ( GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenuSelected.GRM_YearText:GetText() );
    local IsLeapYearSelected = GRM.IsLeapYear ( yearJoined );
    local buttonText = GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButtonTxt:GetText();

    if GRM.IsValidSubmitDate ( dayJoined , monthsFullnameEnum [ GRM.OrigL ( GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected.GRM_MonthText:GetText() ) ] , yearJoined, IsLeapYearSelected ) then
        local rankButton = false;
        local showBdayText = false;
        local formatBdayStamp = "";
        local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];

        if player ~= nil then
            local joinDate = ( "Joined: " .. dayJoined .. " " .. string.sub ( GRM.OrigL ( GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected.GRM_MonthText:GetText() ) , 1 , 3 ) .. " '" ..  string.sub ( yearJoined , 3 ) );
            local finalTStamp = ( string.sub ( joinDate , 9 ) .. " 12:01am" );
            local finalEpochStamp = GRM.TimeStampToEpoch ( joinDate , true );
            -- For metadata tracking
            if buttonText == GRM.L ( "Edit Join Date" ) then
                table.remove ( player.joinDate , #player.joinDate );  -- Removing previous instance to replace
                table.remove ( player.joinDateEpoch , #player.joinDateEpoch );
            end
            table.insert( player.joinDate , finalTStamp );      -- oldJoinDate
            table.insert( player.joinDateEpoch , finalEpochStamp ) ;    -- oldJoinDateMeta
            
            -- For sync
            player.verifiedJoinDate[1] = finalTStamp;
            player.verifiedJoinDate[2] = time();

            -- If it was unKnown before
            player.joinDateUnknown = false;

            if player.events[2][1][1] ~= 0 then
                showBdayText = true;
                formatBdayStamp = GRM.FormatTimeStamp ( player.events[2][3] , false , true );
            end

            -- For UI
            GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:SetText ( GRM.FormatTimeStamp ( dayJoined .. " " .. string.sub ( GRM.OrigL ( GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected.GRM_MonthText:GetText() ) , 1 , 3 ) .. " '" ..  string.sub ( yearJoined , 3 ) ) );
            
            -- Update timestamp to officer note.
            local noteDestination = "none";
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].addTimestampToNote then
                for h = 1 , GRM.GetNumGuildies() do
                    local guildieName ,_,_,_,_,_, note , oNote = GetGuildRosterInfo( h );
                    if name == guildieName then

                        local noteDate = "";
                        local t = GRM.FormatTimeStamp ( finalTStamp , false );
                        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeTag then
                            noteDate = GRM_G.customHeaderJoin .. " " .. t;
                        else
                            noteDate = t;
                        end

                        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 1 and GRM.CanEditOfficerNote() and ( oNote == "" or oNote == nil ) then
                            noteDestination = "officer";
                            GuildRosterSetOfficerNote( h , noteDate );
                            GRM_UI.GRM_MemberDetailMetaData.GRM_noteFontString2:SetText ( noteDate );
                            GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerOfficerNoteEditBox:SetText ( noteDate );
                        elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 2 and CanEditPublicNote() and ( note == "" or note == nil ) then
                            noteDestination = "public";
                            GuildRosterSetPublicNote ( h , noteDate );
                            GRM_UI.GRM_MemberDetailMetaData.GRM_noteFontString1:SetText ( noteDate );
                            GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerNoteEditBox:SetText ( noteDate );
                        elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 3 then
                            noteDestination = "custom";
                            GRM.SetJoinDateToCustomNote ( name , noteDate );
                        end
                        break;
                    end
                end
            end

            -- Gotta update the event tracker date too!
            local date = GRM.ConvertGenericTimestampToIntValues ( string.sub ( joinDate , 9 ) );
            player.events[1][1][1] = date[1];
            player.events[1][1][2] = date[2];
            player.events[1][1][3] = date[3];
            player.events[1][2] = false;  -- Gotta Reset the "reported already" boolean!
            GRM.RemoveFromCalendarQue ( player.name , 1 , nil );

            -- Should definitely refresh the frame here if necessary... prevent mouse tooltip errors
            if GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame:IsVisible() then
                GRM.RefreshAddEventFrame()
            end

            if player.rankHistory[1][3] == 0 and not player.promoteDateUnknown then
                rankButton = true;
            end

            -- Need player index to get this info.
            if player.isOnline then
                if player.zone ~= nil then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoZoneText:SetText ( player.zone );                                       -- Zone
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText2:SetText ( GRM.GetTimePassedInZone ( player.timeEnteredZone ) );                 -- Time Passed
                end
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoText:Show();
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoZoneText:Show();
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText1:Show();
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText2:Show();
            end

            -- Let's send the changes out as well!
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled then
                local syncRankFilter = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank;
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportAllRanks then
                    syncRankFilter = GuildControlGetNumRanks() - 1;
                end
                GRMsync.SendMessage ( "GRM_SYNC" , GRM_G.PatchDayString .. "?GRM_JD?" .. syncRankFilter .. "?" .. name .. "?" .. joinDate .. "?" .. finalTStamp .. "?" .. tostring ( finalEpochStamp ) .. "?" .. tostring ( player.verifiedJoinDate[2] ) .. "?" .. noteDestination , "GUILD");
            end
        end

        GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenuSelected:Hide();
        GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected:Hide();
        GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenuSelected:Hide();
        GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitCancelButton:Hide();
        GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButton:Hide();
        GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:Show();
        if rankButton then
            GRM_UI.GRM_MemberDetailMetaData.GRM_SetPromoDateButton:Show();
        else
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:Show();
        end

        -- Birthday Logic restoration
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showBDay and not GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:IsVisible() and not GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:IsVisible() then
            if showBdayText then
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:Hide();
                GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:SetText ( formatBdayStamp );
                GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:Show();
            else
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:Show();
                GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:Hide();
            end
        end

        GRM_UI.Unpause();
        -- Update the Audit Frames!
        if GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame:IsVisible() then
            GRM.RefreshAuditFrames ( true , true );
        end
    end
end

-- Method:          GRM.SyncJoinDatesOnAllAlts()
-- What it Does:    Tales the player name and makes ALL of their alts share the same timestamp on joining.
-- Purpose:         Ease for the addon user to be able to sync the join dates among all alts rather than have to manually do them 1 at a time.6
GRM.SyncJoinDatesOnAllAlts = function ( playerName )
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    local player = guildData[playerName];

    if player ~= nil then
        -- now, let's check the alt info.
        local finalTStamp = player.joinDate[#player.joinDate]
        local finalTStampEpoch = player.joinDateEpoch[#player.joinDateEpoch]
        local syncEpochStamp = time();
        local joinDate = "Joined: " .. string.sub ( finalTStamp , 1 , string.find ( finalTStamp , "'" ) + 2 );
        local tempAlt;

        -- Let's cycle through the alts now.
        for j = 1 , #player.alts do
            -- Now, need to match the alt to the real database

            tempAlt = guildData[player.alts[j][1]];

            if tempAlt ~= nil then
                -- Let's match the values now...
                if tempAlt.joinDate[ #tempAlt.joinDate ] ~= nil or #tempAlt.joinDate > 0 then
                    -- Removing old date
                    table.remove ( tempAlt.joinDate , #tempAlt.joinDate );
                    table.remove ( tempAlt.joinDateEpoch , #tempAlt.joinDateEpoch );
                end
                -- Adding the new stamps
                table.insert( tempAlt.joinDate , finalTStamp );      -- oldJoinDate
                table.insert( tempAlt.joinDateEpoch , finalTStampEpoch ) ;    -- oldJoinDateMeta

                -- For sync timestamp checking...
                tempAlt.verifiedJoinDate[1] = finalTStamp;
                tempAlt.verifiedJoinDate[2] = syncEpochStamp;

                -- If it was unKnown before
                tempAlt.joinDateUnknown = false;

                -- Let's set those officer/public notes as well!
                local noteDestination = "none";
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].addTimestampToNote and ( GRM.CanEditOfficerNote() or CanEditPublicNote() ) then
                    for h = 1 , GRM.GetNumGuildies() do
                        local guildieName ,_,_,_,_,_, note , oNote = GetGuildRosterInfo( h );
                        if tempAlt.name == guildieName then
                            local noteDate = "";
                            local t = GRM.FormatTimeStamp ( finalTStamp , false );
                            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeTag then
                                noteDate = GRM_G.customHeaderJoin .. " " .. t;
                            else
                                noteDate = t;
                            end
                            
                            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 1 and GRM.CanEditOfficerNote() and ( oNote == "" or oNote == nil ) then
                                noteDestination = "officer";
                                GuildRosterSetOfficerNote( h , noteDate );
                            elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 2 and CanEditPublicNote() and ( note == "" or note == nil ) then
                                noteDestination = "public";
                                GuildRosterSetPublicNote ( h , noteDate );
                            elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 3 then
                                noteDestination = "custom";
                                GRM.SetJoinDateToCustomNote ( guildieName , noteDate );
                            end  
                            break;
                        end
                    end
                end
                
                -- Gotta update the event tracker date too!
                local date = GRM.ConvertGenericTimestampToIntValues ( string.sub ( joinDate , 9 ) );
                tempAlt.events[1][1][1] = date[1];
                tempAlt.events[1][1][2] = date[2];
                tempAlt.events[1][1][3] = date[3];
                tempAlt.events[1][2] = false;  -- Gotta Reset the "reported already" boolean!
                -- Update the Calendar Que since anniversary dates might be changed as a result
                GRM.RemoveFromCalendarQue ( tempAlt.name , 1 , nil );

                -- To Avoid the spam, we are going to treat this like a SYNC message
                -- Let's send the changes out as well!
                
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled then
                    local syncRankFilter = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank;
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportAllRanks then
                        syncRankFilter = GuildControlGetNumRanks() - 1;
                    end
                    GRMsync.SendMessage ( "GRM_SYNC" , GRM_G.PatchDayString .. "?GRM_JDSYNCUP?" .. GRM_G.addonUser .. "?" .. syncRankFilter .. "?" .. tempAlt.name .. "?" .. joinDate .. "?" .. finalTStamp .. "?" .. finalTStampEpoch .. "?" .. tostring ( syncEpochStamp ) .. "?" .. noteDestination , "GUILD");
                end

            end
        end
    end

    -- Update the Audit Frames!
    if GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame:IsVisible() then
        GRM.RefreshAuditFrames ( true , true );
    end

    -- Should definitely refresh the frame here if necessary... prevent mouse tooltip errors
    if GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame:IsVisible() then
        GRM.RefreshAddEventFrame()
    end

end

-- Method:          GRM.SyncJoinDateUsingEarliest()
-- What it Does:    Syncs the join date of the grouping of alts to all be the same as the alt with the earliest join date
-- Purpose:         For join date syncing and time-saving for the player
GRM.SyncJoinDateUsingEarliest = function()
    GRM.SyncJoinDatesOnAllAlts ( GRM.GetAltWithOldestJoinDate ( GRM_G.currentName )[1] );
end

-- Method:          GRM.SyncJoinDateUsingMain()
-- What it Does:    Syncs the join date of the grouping of alts to all be the same as the alt with the player's main
-- Purpose:         For join date syncing and time-saving for the player
GRM.SyncJoinDateUsingMain = function()
    GRM.SyncJoinDatesOnAllAlts ( GRM.GetPlayerMain ( GRM_G.currentName ) );
end

-- Method:          GRM.SyncJoinDateUsingCurrentSelected()
-- What it Does:    Syncs the join date of the grouping of alts to all be the same as the alt with the currently selected player on the roster
-- Purpose:         For join date syncing and time-saving for the player
GRM.SyncJoinDateUsingCurrentSelected = function()
    GRM.SyncJoinDatesOnAllAlts ( GRM_G.currentName );
end

-- Method:          GRM.GetPlayerMain ( string )
-- What it Does:    Returns the full player of the toon's main, or himself if he is main, or nil if no main.
-- Purpose:         Useful lookup for many purposes...
GRM.GetPlayerMain = function ( playerName )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][playerName];
    local result = playerName;

    if player ~= nil and not player.isMain then
        -- if player has no alts
        if #player.alts == 0 then
            result = nil;
        elseif #player.alts > 0 then
            local isFound = false
            for j = 1 , #player.alts do
                if player.alts[j][5] then
                    result = player.alts[j][1];
                    isFound = true;
                    break;
                end
            end
            if not isFound then
                result = nil;
            end
        end
    end
    return result;
end 

-- Method:          GRM.GetAltWithOldestJoinDate ( string )
-- What it Does:    Returns the name of the player with the oldest join date in his grouping of main/alts
-- Purpose:         When syncing join dates among a grouping of alts, it would be nice to have an option to sync to the oldest join date.
GRM.GetAltWithOldestJoinDate = function ( playerName )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][ playerName ];
    local oldestPlayer = { playerName , 0 };
    local oldestJoinDateString = "";

    if player ~= nil then
        oldestPlayer[2] = player.joinDateEpoch[#player.joinDateEpoch];
        oldestJoinDateString = player.joinDate[#player.joinDate];

        -- if isMain or the player has no alts
        if #player.alts > 0 then
            for j = 1 , #player.alts do
                local alt = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][ player.alts[j][1] ];
                if alt ~= nil then
                    if #alt.joinDateEpoch > 0 then
                        if alt.joinDateEpoch[#alt.joinDateEpoch] < oldestPlayer[2] then
                            oldestPlayer = { player.alts[j][1] , alt.joinDateEpoch[#alt.joinDateEpoch] };
                            oldestJoinDateString = alt.joinDate[#alt.joinDate];
                        end
                    end
                end
            end
        end
    end
    oldestPlayer[2] = oldestJoinDateString;
    return oldestPlayer;
end

-- Method:          GRM.IsAltJoinDatesSynced()
-- What it Does:    Returns true if the player has already sync'd all of the alt data.
-- Purpose:         Quality of Life... no need to ask the player to sync alt data if already sync'd
GRM.IsAltJoinDatesSynced = function ( playerName )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][playerName];
    local result = false;

    if player ~= nil then
        -- if isMain or the player has no alts
        if #player.alts > 0 and #player.joinDate > 0 then
            -- Cycle through each alt...
            local isNotSync = false;
            local tempAlt;

            for j = 1 , #player.alts do
                tempAlt = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][ player.alts[j][1] ];
                if tempAlt ~= nil then
                    if #tempAlt.joinDate == 0 or ( #tempAlt.joinDate > 0 and tempAlt.joinDate[#tempAlt.joinDate] ~= player.joinDate[#player.joinDate] ) then
                        isNotSync = true;
                    end
                end
                if isNotSync then
                    break;
                end
            end
            if not isNotSync then
                result = true;
            end
        end
    end
    return result;
end

-- Method:          GRM.PlayerOrAltHasJD ( string )
-- What it Does:    Returns true if the player or any of his alts has the join date set... unknown counts as NOT set
-- Purpose:         On the mouseover of the join date, it would not be useful to give the option to sync join dates if not at least 1 alt in the group has the JD set
--                  In other words, it is about a good user experience and not giving them options that are unnecessary and useless...
-- NOTE:            Note, if the player has NO alts... it will return as false.
GRM.PlayerOrAltHasJD = function ( playerName )
    local result = false
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][playerName];

    if player ~= nil then
        if #player.alts > 0 then                -- The player needs to have at least one alt.
            if #player.joinDate > 0 then        -- Player Has a join date!
                result = true;
            else
                local tempAlt;
                -- player does not have a JD... let's check if any of the alts do.
                for i = 1 , #player.alts do       -- cycle through the alts
                    tempAlt = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][ player.alts[i][1] ];
                    if tempAlt ~= nil then
                        if #tempAlt.joinDate > 0 then
                            result = true;
                        end
                    end
                    if result then
                        break;
                    end
                end
            end
        end
    end
    return result;
end

-- Method:          GRM.PlayerHasJoinDate ( string )
-- What it Does:    Returns true if the current selected player has a listed Join Date, with the given JoinDate
-- Purpose:         For the Join Date selection, this will let us know if an option to sync to the selected player's join date should be given.
GRM.PlayerHasJoinDate = function ( playerName )
    local result = { false , "" };
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][playerName];

    if player ~= nil then
        if #player.joinDate > 0 then       -- Player Has a join date!
            result = { true , player.joinDate[#player.joinDate] };
        end
    end
    return result;
end

-- Method:          GRM.SetPromoDate()
-- What it Does:    Set's the date the player was promoted to the current rank
-- Purpose:         Date tracking and control of rank promotions.
GRM.SetPromoDate = function ()
    local name = GRM_G.currentName;
    local dayJoined = tonumber ( GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenuSelected.GRM_DayText:GetText() );
    local yearJoined = tonumber ( GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenuSelected.GRM_YearText:GetText() );
    local IsLeapYearSelected = GRM.IsLeapYear ( yearJoined );

    if GRM.IsValidSubmitDate ( dayJoined , monthsFullnameEnum [ GRM.OrigL ( GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected.GRM_MonthText:GetText() ) ] , yearJoined, IsLeapYearSelected ) then
        local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];

        if player ~= nil then
            local promotionDate = ( "Promoted: " .. dayJoined .. " " ..  string.sub ( GRM.OrigL ( GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected.GRM_MonthText:GetText() ) , 1 , 3 ) .. " '" ..  string.sub ( yearJoined , 3 ) );
            -- Promo Save Data
            
            player.rankHistory[#player.rankHistory][1] = player.rankName;
            player.rankHistory[#player.rankHistory][2] = GRM.GetCleanTimestamp ( promotionDate );
            player.rankHistory[#player.rankHistory][3] = GRM.TimeStampToEpoch ( promotionDate , true );
            
            -- For SYNC
            player.verifiedPromoteDate[1] = GRM.GetCleanTimestamp ( promotionDate );
            player.verifiedPromoteDate[2] = time();
            
            -- If player had it set to "unknown before"
            player.promoteDateUnknown = false;
            
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:SetText ( GRM.L ( "Promoted:" ) .. " " .. GRM.FormatTimeStamp ( dayJoined .. " " .. string.sub ( GRM.OrigL ( GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected.GRM_MonthText:GetText() ) , 1 , 3 ) .. " '" .. string.sub ( yearJoined , 3 ) ) );

            if player.isOnline then
                if player.zone ~= nil then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoZoneText:SetText ( player.zone );                                     -- Zone
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText2:SetText ( GRM.GetTimePassedInZone ( player.timeEnteredZone ) );              -- Time Passed
                end
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoText:Show();
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoZoneText:Show();
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText1:Show();
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText2:Show();
            end

            -- Send the details out for others to pickup!
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled then
                local syncRankFilter = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank;
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportAllRanks then
                    syncRankFilter = GuildControlGetNumRanks() - 1;
                end
                GRMsync.SendMessage ( "GRM_SYNC" , GRM_G.PatchDayString .. "?GRM_PD?" .. syncRankFilter .. "?" .. name .. "?" .. promotionDate.. "?" .. tostring( player.verifiedPromoteDate[2] ) , "GUILD");
            end
        end

        GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenuSelected:Hide();
        GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected:Hide();
        GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenuSelected:Hide();
        GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitCancelButton:Hide();
        GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButton:Hide();
        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:Show();

        GRM_UI.Unpause();
        -- Update Audit Frames.
        if GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame:IsVisible() then
            GRM.RefreshAuditFrames ( true , true );
        end
    end
end

-- Method:          GRM.SetAllIncompleteJoinUnknown()
-- What it Does:    Sets the join date of every player in the guild who does not have it yet set as "unknown"
-- Purpose:         More just quality of life information and UI feature. Useful than manually going to them all to set as unknown...
GRM.SetAllIncompleteJoinUnknown = function()
    if not ( GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetJoinUnkownButton.GRM_SetJoinUnkownButtonText:GetText() == GRM.L ( "All Complete" ) ) then
        if time() - GRM_G.buttonTimer1 >= 2 then

            local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];

            if GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetJoinUnkownButton.GRM_SetJoinUnkownButtonText:GetText() == GRM.L ( "Set Incomplete to Unknown" ) then
                -- Ok, let's go through ALL guildies and clear it!
                for _ , player in pairs ( guildData ) do
                    if type ( player ) == "table" then

                        -- if not "unknown" already, and if it doesn't have an established join date
                        if not player.joinDateUnknown and #player.joinDate == 0 then
                            GRM.ClearJoinDateHistory ( player.name , true );
                            GRM.DateSubmitCancelResetLogic( true , "join" , true , player.name );
                        elseif GRM_UI.GRM_MemberDetailMetaData:IsVisible() and GRM_G.currentName == player.name then
                            GRM_G.pause = false;
                            GRM.ClearAllFrames( true );
                            GRM.PopulateMemberDetails ( player.name );
                            GRM_UI.GRM_MemberDetailMetaData:Show();
                            if GRM_UI.MemberDetailFrame and GRM_UI.MemberDetailFrame:IsVisible() then
                                GRM_UI.MemberDetailFrame:Hide();
                            end
                            if GuildFrame and GRM_UI.MemberDetailFrameClassic and GRM_UI.MemberDetailFrameClassic:IsVisible() then
                                GRM_UI.MemberDetailFrameClassic:Hide();
                            end
                            GRM_G.pause = true;
                        end
                    end
                end
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetJoinUnkownButton.GRM_SetJoinUnkownButtonText:SetText ( GRM.L ( "Clear All Unknown" ) );
            else
                for _ , player in pairs ( guildData ) do
                    if type ( player ) == "table" then

                        -- if not "unknown" already, and if it doesn't have an established join date
                        if player.joinDateUnknown then
                            GRM.ClearJoinDateHistory ( player.name , false );
                            GRM.DateSubmitCancelResetLogic( false , "join" , true , player.name );
                        elseif GRM_UI.GRM_MemberDetailMetaData:IsVisible() and GRM_G.currentName == player.name then
                            GRM_G.pause = false;
                            GRM.ClearAllFrames( true );
                            GRM.PopulateMemberDetails ( player.name );
                            GRM_UI.GRM_MemberDetailMetaData:Show();
                            if GRM_UI.MemberDetailFrame and GRM_UI.MemberDetailFrame:IsVisible() then
                                GRM_UI.MemberDetailFrame:Hide();
                            end
                            if GuildFrame and GRM_UI.MemberDetailFrameClassic and GRM_UI.MemberDetailFrameClassic:IsVisible() then
                                GRM_UI.MemberDetailFrameClassic:Hide();
                            end
                            GRM_G.pause = true;
                        end
                    end
                end
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetJoinUnkownButton.GRM_SetJoinUnkownButtonText:SetText ( GRM.L ( "Set Incomplete to Unknown" ) );
            end
            GRM.RefreshAuditFrames ( true , true );
            GRM_G.buttonTimer1 = time();
        else
            GRM.Report ( GRM.L ( "Please Wait {num} more Seconds" , nil , nil , math.floor ( 2 - ( time() - GRM_G.buttonTimer1 ) ) ) );
        end
    end
end

-- Method:          GRM.SetAllIncompletePromoUnknown()
-- What it Does:    Sets the promo date of every player in the guild who does not have it yet set to an unknown value
-- Purpose:         More just quality of life information and UI feature. Useful than manually going to them all...
GRM.SetAllIncompletePromoUnknown = function()
    if not ( GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetPromoUnkownButton.GRM_SetPromoUnkownButtonText:GetText() == GRM.L ( "All Complete" ) ) then
        if time() - GRM_G.buttonTimer2 >= 2 then

            local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
            local player;

            if GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetPromoUnkownButton.GRM_SetPromoUnkownButtonText:GetText() == GRM.L ( "Set Incomplete to Unknown" ) then
                for member in pairs ( guildData ) do
                    if type ( guildData[member] ) == "table" then
                        player = guildData[member];

                        -- if not "unknown" already, and if it doesn't have an established join date
                        if not player.promoteDateUnknown and player.verifiedPromoteDate[1] == "" then
                            GRM.ClearPromoDateHistory ( player.name , true );
                            GRM.DateSubmitCancelResetLogic( true , "promo" , true , player.name );
                        end
                    end
                end
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetPromoUnkownButton.GRM_SetPromoUnkownButtonText:SetText ( GRM.L ( "Clear All Unknown" ) );
            else
                for member in pairs ( guildData ) do
                    if type ( guildData[member] ) == "table" then
                        player = guildData[member];

                        if player.promoteDateUnknown then
                            GRM.ClearPromoDateHistory ( player.name , false );
                            GRM.DateSubmitCancelResetLogic( false , "promo" , true , player.name );
                        end
                    end
                end
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetPromoUnkownButton.GRM_SetPromoUnkownButtonText:SetText ( GRM.L ( "Set Incomplete to Unknown" ) );
            end
            GRM.RefreshAuditFrames ( true , true );
            GRM_G.buttonTimer2 = time();
        else
            GRM.Report ( GRM.L ( "Please Wait {num} more Seconds" , nil , nil , math.floor ( 2 - ( time()-GRM_G.buttonTimer2 ) ) ) );
        end
    end
end

-- Method:          GRM.SetAllIncompleteBdayUnknown()
-- What it Does:    Sets the Birth date of every player in the guild who does not have it yet set to an unknown value
-- Purpose:         More just quality of life information and UI feature. Useful than manually going to them all...
GRM.SetAllIncompleteBdayUnknown = function()
    if not ( GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetBdayUnkownButton.GRM_SetBdayUnkownButtonText:GetText() == GRM.L ( "All Complete" ) ) then
        if time() - GRM_G.buttonTimer3 >= 3 then

            local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
            local player;

            if GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetBdayUnkownButton.GRM_SetBdayUnkownButtonText:GetText() == GRM.L ( "Set Incomplete to Unknown" ) then
                for member in pairs ( guildData ) do
                    if type ( guildData[member] ) == "table" then
                        player = guildData[member];

                        -- if not "unknown" already, and if it doesn't have an established bday
                        if not player.birthdayUnknown and player.events[2][1][1] == 0 then
                            player.birthdayUnknown = true
                            if GRM_UI.GRM_MemberDetailMetaData:IsVisible() and GRM_G.currentName == player.name then
                                GRM_G.pause = false;
                                GRM.ClearAllFrames( true );
                                GRM.PopulateMemberDetails ( player.name );
                                GRM_UI.GRM_MemberDetailMetaData:Show();
                                if GRM_UI.MemberDetailFrame and GRM_UI.MemberDetailFrame:IsVisible() then
                                    GRM_UI.MemberDetailFrame:Hide();
                                end
                                if GuildFrame and GRM_UI.MemberDetailFrameClassic and GRM_UI.MemberDetailFrameClassic:IsVisible() then
                                    GRM_UI.MemberDetailFrameClassic:Hide();
                                end
                                GRM_G.pause = true;
                            end
                        end
                    end
                end
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetBdayUnkownButton.GRM_SetBdayUnkownButtonText:SetText ( GRM.L ( "Clear All Unknown" ) );
            else
                for member in pairs ( guildData ) do
                    if type ( guildData[member] ) == "table" then
                        player = guildData[member];

                        player.birthdayUnknown = false;
                        if GRM_UI.GRM_MemberDetailMetaData:IsVisible() and GRM_G.currentName == player.name then
                            GRM_G.pause = false;
                            GRM.ClearAllFrames( true );
                            GRM.PopulateMemberDetails ( player.name );
                            GRM_UI.GRM_MemberDetailMetaData:Show();
                            if GRM_UI.MemberDetailFrame and GRM_UI.MemberDetailFrame:IsVisible() then
                                GRM_UI.MemberDetailFrame:Hide();
                            end
                            if GuildFrame and GRM_UI.MemberDetailFrameClassic and GRM_UI.MemberDetailFrameClassic:IsVisible() then
                                GRM_UI.MemberDetailFrameClassic:Hide();
                            end
                            GRM_G.pause = true;
                        end
                    end
                end
                GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame.GRM_SetBdayUnkownButton.GRM_SetBdayUnkownButtonText:SetText ( GRM.L ( "Set Incomplete to Unknown" ) );
            end
            GRM.RefreshAuditFrames ( true , true );
            GRM_G.buttonTimer3 = time();
        else
            GRM.Report ( GRM.L ( "Please Wait {num} more Seconds" , nil , nil , math.floor ( 2 - ( time()-GRM_G.buttonTimer3 ) ) ) );
        end
    end
end

-- Method:          GRM.DateSubmitCancelResetLogic( boolean , string , boolean , string )
-- What it Does:    Resets the logic on what occurs with the cancel button, since it will have multiple uses.
-- Purpose:         Resource efficiency. No need to make new buttons for everything! This reuses the button, just resets the click logic in join date submit cancel event.
GRM.DateSubmitCancelResetLogic = function( isUnknown , date , isAudit , playerName )
    local buttonText = GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButtonTxt:GetText();
    local joinDateText = GRM.L ( "Set Join Date" );
    local promoDateText = GRM.L ( "Set Promo Date" );
    local bDayDateText = GRM.L ( "Set Birthday" );
    local editDateText = GRM.L ( "Edit Promo Date" );
    local editJoinText = GRM.L ( "Edit Join Date" );
    local name = GRM_G.currentName;
    local showJoinText = false;
    local showBdayText = false;
    local formatBdayStamp = "";

    -- For the audit
    if isAudit then
        if date == "join" then
            buttonText = joinDateText;
        elseif date == "promo" then
            buttonText = promoDateText;
        elseif date == "bday" then
            buttonText = bDayDateText;
        end
    end

    -- To save values properly.
    if isAudit and playerName ~= nil then
        name = playerName;
    end
    
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];
    -- Need player index to get this info.

    if player ~= nil then
        if player.name == GRM_G.currentName then
            if ( player.promoteDateUnknown or player.verifiedPromoteDate[1] ~= nil ) then
                GRM_G.rankDateSet = true;
            end
            if player.joinDateUnknown or #player.joinDate ~= 0 then
                showJoinText = true;
            end
            
            if isUnknown and date == "bday" then
                player.birthdayUnknown = true
            end
            if player.birthdayUnknown or player.events[2][1][1] ~= 0 then
                showBdayText = true;
                if not player.birthdayUnknown then
                    formatBdayStamp = GRM.FormatTimeStamp ( { player.events[2][1][1] , player.events[2][1][2] , player.events[2][1][3] } , false , true );
                else
                    formatBdayStamp = GRM.L ( "Unknown" );
                end
            end
        end
            
        if isUnknown then
            if date == "join" then
                player.joinDateUnknown = true;
            elseif date == "promo" then
                player.promoteDateUnknown = true
                if player.name == GRM_G.currentName then
                    GRM_G.rankDateSet = true;
                end
            end
        end

        if not isAudit or ( GRM_UI.GRM_MemberDetailMetaData:IsVisible() and GRM_G.currentName == playerName ) then
            if player.isOnline then
                if player.zone ~= nil then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoZoneText:SetText ( player.zone );                                     -- Zone
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText2:SetText ( GRM.GetTimePassedInZone ( player.timeEnteredZone ) );              -- Time Passed
                end
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoText:Show();
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoZoneText:Show();
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText1:Show();
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText2:Show();
            end
        end
    end

    -- Determine which information needs to repopulate.
    if GRM_UI.GRM_MemberDetailMetaData:IsVisible() and name == GRM_G.currentName then

        -- Join Date
        if joinDateText == buttonText or editJoinText == buttonText then
            if isUnknown and date == "join" then
                GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:SetText( GRM.L ( "Unknown" ) );
                GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:Show();
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailJoinDateButton:Hide();
            else
                if buttonText == editJoinText then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:Show();
                else
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailJoinDateButton:Show();
                end
            end
            
        -- Promo Date
        elseif buttonText == promoDateText then
            if not isUnknown then
                GRM_UI.GRM_MemberDetailMetaData.GRM_SetPromoDateButton:Show();
            elseif date == "promo" then
                GRM_UI.GRM_MemberDetailMetaData.GRM_SetPromoDateButton:Hide();
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:SetText ( GRM.L ( "Promoted:" ) .. " " .. GRM.L ( "Unknown" ) );
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:Show();
            end

            if name == GRM_G.currentName then
                if showJoinText then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:Show();
                else
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailJoinDateButton:Show();
                end
            end
            
        -- promo Date - Edit
        elseif buttonText == editDateText then
            if isUnknown and date == "promo" then
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:SetText ( GRM.L ( "Promoted:" ) .. " " .. GRM.L ( "Unknown" ) );
            end
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:Show();
        end
        -- Birthday Logic
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showBDay and not GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:IsVisible() and not GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:IsVisible() then
            if showBdayText then
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:Hide();
                GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:SetText ( formatBdayStamp );
                GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:Show();
            else
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:Show();
                GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:Hide();
            end
        end

        --RANK PROMO DATE
        if not GRM_G.rankDateSet then      --- Promotion has never been recorded!
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:Hide();
            GRM_UI.GRM_MemberDetailMetaData.GRM_SetPromoDateButton:Show();
        else
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:Show();
        end

        if not isAudit then
            GRM_UI.Unpause();
        end
    end

    -- Close the rest
    GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenuSelected:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenuSelected:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButton:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitCancelButton:Hide();
end

-- Method:          GRM.SetDateButtonConfiguration ( int )
-- What it Does:    Formats the date submit configuration of buttons for clean submission
-- Purpose:         Only some date inputs need ALL, including the year. Birthday only needs day and month.
GRM.SetDateButtonConfiguration = function ( formatNum )
    if formatNum == 1 then
        if GRM_G.formatNumber ~= formatNum then
            GRM_G.formatNumber = formatNum;
            
            -- Formatting
            GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected:SetPoint ( "TOP" , GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankTxt , "BOTTOM" , -60 , -4 );
            GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButton:SetPoint ( "TOP" , GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected , "BOTTOM" , -20 , -3 );
            GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitCancelButton:SetPoint ( "LEFT" , GRM_UI.GRM_MemberDetailMetaData.GRM_SetUnknownButton , "RIGHT" , 6 , 0 );

        end
        GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenuSelected:Show();
        GRM_UI.GRM_MemberDetailMetaData.GRM_SetUnknownButton:Show();
    elseif formatNum == 2 then
        if GRM_G.formatNumber ~= formatNum then
            GRM_G.formatNumber = formatNum;

            -- Formatting
            GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected:SetPoint ( "TOP" , GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankTxt , "BOTTOM" , -15 , -4 );
            GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButton:SetPoint ( "TOP" , GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected , "BOTTOM" , -65 , -3 );
            GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitCancelButton:SetPoint ( "LEFT" , GRM_UI.GRM_MemberDetailMetaData.GRM_SetUnknownButton , "RIGHT" , 3 , 0 );            

        end
        GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenuSelected:Hide();
        GRM_UI.GRM_MemberDetailMetaData.GRM_SetUnknownButton:Show();
    end

    GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenuSelected:Show();
    GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected:Show();
    GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButton:Show();
    GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitCancelButton:Show();
end

-- Method:          GRM.GetRecordedDate ( string )
-- What it Does:    Returns either today's date, if no date of the corresponding date is stored, or, returns the date of the stored item
-- Purpose:         Continuity on editing dates and presenting dates.
GRM.GetRecordedDate = function( buttonName )
    local month , day , currentYear = select ( 2 , GRM.CalendarGetDate() );
    local dateTable = {};
    
    if buttonName == "PromoRank" and not GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].promoteDateUnknown and GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].verifiedPromoteDate[1] ~= "" then
        dateTable = GRM.ConvertGenericTimestampToIntValues ( GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].verifiedPromoteDate[1] );
        day = dateTable[1];
        month = dateTable[2];
        currentYear = dateTable[3];
    elseif buttonName == "JoinDate" and not GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].joinDateUnknown and #GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].joinDate > 0 then
        dateTable = GRM.ConvertGenericTimestampToIntValues ( GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].joinDate[#GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].joinDate] );
        day = dateTable[1];
        month = dateTable[2];
        currentYear = dateTable[3];
    elseif buttonName == "Birthday" and not GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].birthdayUnknown and GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].events[2][1][1] ~= 0 then
        day = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].events[2][1][1];
        month = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].events[2][1][2];
    end

    return day , month , currentYear;
end

-- Method:          GRM.SetDateSelectFrame( string , frameObject, string )
-- What it Does:    On Clicking the "Set Join Date" button this logic presents itself
-- Purpose:         Handle the event to modify when a player joined the guild. This is useful for anniversary date tracking.
--                  It is also necessary because upon starting the addon, it is unknown a person's true join date. This allows the gleader to set a general join date.
GRM.SetDateSelectFrame = function ( buttonName )
    local day , month , currentYear = GRM.GetRecordedDate ( buttonName );
    local months = { "January" , "February" , "March" , "April" , "May" , "June" , "July" , "August" , "September" , "October" , "November" , "December" };
    local formatNumber = 0;

    -- Day
    GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenuSelected.GRM_DayText:SetText ( day );
    GRM_G.dayIndex = day;

    -- Month
    GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected.GRM_MonthText:SetText ( GRM.L ( months [ month ] ) );
    GRM_G.monthIndex = month;
    
    -- Year
    GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenuSelected.GRM_YearText:SetText ( currentYear );
    GRM_G.yearIndex = currentYear;
    
    -- Set function of the buttons to corresponding action.
    if buttonName == "PromoRank" then
        formatNumber = 1;

        GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButtonTxt:SetText ( GRM.L ( "Set Promo Date" ) );
        GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButton:SetScript("OnClick" , function( _ , button )
            if button == "LeftButton" and not GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenu:IsVisible() then
                GRM.SetPromoDate();
            end
        end);
    elseif buttonName == "JoinDate" then
        formatNumber = 1;
        GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButtonTxt:SetText ( GRM.L ( "Set Join Date" ) );
        GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButton:SetScript("OnClick" , function( _ , button )
            if button == "LeftButton" then
                GRM.SetJoinDate();
            end
        end);
    elseif buttonName == "Birthday" then
        formatNumber = 2;
        GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButtonTxt:SetText ( GRM.L ( "Set Birthday" ) );
        GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButton:SetScript("OnClick" , function( _ , button )
            if button == "LeftButton" then
                GRM.SetBirthdayFrameLogic();
            end
        end);
    end

    GRM.SetDateButtonConfiguration ( formatNumber );
end

-- Method:          GRM.IsGuildieAnOfficer ( string )
-- What it Does:    It returns true if the player is tagged as an officer in the guild. It does this by grabbing their current rank status, getting the flags of that rank permission, returning index 9
-- Purpose:         Useful to know if player is an officer.
GRM.IsGuildieAnOfficer = function ( name )
    local id = GRM.GetGuildMemberRankID ( name );

    -- If can edit both public and officer notes is where I set the standard
    return ( C_GuildInfo.GuildControlGetRankFlags ( id + 1 )[10] and C_GuildInfo.GuildControlGetRankFlags ( id + 1 )[12] ); -- It has to be + 1 as the indexing starts at 1, though the rank ID starts at 0;
end

-- Method:          GRM.GetRankIndex ( string , 2Darray )
-- What it Does:    Returns the index of the guild rank...
-- Purpose:         Flow control of drop down menus.
GRM.GetRankIndex = function ( rankName , button )
    GRM.BuildRankList();
    local index = -1;
    
    if button == nil then
        for i = 1 , #GuildRanks do
            if GuildRanks[i] == rankName then
                index = i - 1;
                break;
            end
        end
    else
        local buttonName = button:GetName();
        if tonumber ( string.sub ( buttonName , #buttonName - 1 ) ) == nil then -- If nil then it is only 1 number
            index = tonumber ( string.sub ( buttonName , #buttonName ) ) - 1;
        else
            index = tonumber ( string.sub ( buttonName , #buttonName - 1 ) ) - 1;
        end
    end
    return index;
end

-- Method:          GRM.BuildRankList()
-- What it Does:    It builds into an array all of the dropdown buttons from the guild rank dropdown.
-- Purpose:         Needed to initialize dropdown logic...
GRM.BuildRankList = function()
    -- Let's put all of the buttons in an array, and let's set some rules.
    for i = 1 , GuildControlGetNumRanks() do
        GuildRanks[i] = GuildControlGetRankName( i );
    end
end


-- Method:          GRM.OnRankChange ( string , string , string , string )
-- What it Does:    Logic on Rank Drop down select in main frame
-- Purpose:         UI feature and UX
GRM.OnRankChange = function ( formerRank , newRank , promotedName , promoterName )
    -- Build Buttons Profile
    local newRankIndex = GRM.GetRankIndex ( newRank , nil );
    local formerRankIndex = GRM.GetRankIndex ( formerRank , nil );

    if newRankIndex ~= formerRankIndex then
        -- Save the data!
        local timestamp = GRM.GetTimestamp();
        local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][promotedName];

        if player ~= nil then
            local formerRankName = player.rankName;                               -- For the reporting string!

            if player.rankHistory[1][3] == 0 then
                player.rankHistory[1][1] = formerRankName;
                player.rankHistory[1][2] = string.sub ( timestamp , 1 , string.find ( timestamp , "'" ) + 2 );
                player.rankHistory[1][3] = time();
            end

            player.rankName = newRank                                         -- rank name
            player.rankIndex = newRankIndex;                                           -- rank index!

            -- If player had it set to "unknown before"
            player.promoteDateUnknown = false;

            -- For SYNC
            player.verifiedPromoteDate[1] = timestamp;
            player.verifiedPromoteDate[2] = time();

            -- For history
            table.insert ( player.rankHistory , { player.rankName , timestamp , time() } );
            
            -- Let's update it on the fly!
            local simpleName = GRM.GetStringClassColorByName ( promotedName ) .. GRM.SlimName ( promotedName ) .. "|r";
            local playerSimpleName = GRM.GetStringClassColorByName ( promoterName ) .. GRM.SlimName ( promoterName ) .. "|r";
            local logReport , logReportWithTime = "" , "";
            local tempTimeStamp = select ( 2 , GRM.GetTimestamp() );

            -- Promotion Obtained
            if newRankIndex < formerRankIndex then
                if CanGuildPromote() then                   
                    logReportWithTime , logReport = GRM.GetPromotionLogString ( true , playerSimpleName , simpleName , formerRankName , newRank , tempTimeStamp );
                    -- Cleans up reporting
                    GRM.LiveChangesCheck ( 1 , logReportWithTime );
                    -- report the changes!
                    if not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].disableMacroToolLogSpam and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].promotion then
                        GRM.PrintLog ( { 1 , logReport } );
                    end
                    GRM.AddLog ( { 1 , logReportWithTime , true , playerSimpleName , simpleName , formerRankName , newRank , tempTimeStamp } );

                    -- And one more check in case of the 1ms chance this occurse.
                    GRM.LiveChangesCheck ( 1 , logReport );

                end

                if player[1] == GRM_G.addonUser then
                    GRM.AddonPlayerRankChange ( newRankIndex );
                end
            -- Demotion Obtained
            elseif newRankIndex > formerRankIndex and CanGuildDemote() then
                logReportWithTime , logReport = GRM.GetDemotionLogString  ( true , playerSimpleName , simpleName , formerRankName , newRank , tempTimeStamp );

                -- Live cleanup of the scan
                GRM.LiveChangesCheck ( 2 , logReportWithTime );
                -- reporting the changes!
                if not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].disableMacroToolLogSpam and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].demotion then
                    GRM.PrintLog ( { 2 , logReport } );                          
                end
                GRM.AddLog ( { 2 , logReportWithTime , true , playerSimpleName , simpleName , formerRankName , newRank , tempTimeStamp } );

                -- And one more check in case of the 1ms chance this occurse.
                GRM.LiveChangesCheck ( 2 , logReport );
            end
            if GRM_UI.GRM_MemberDetailMetaData:IsVisible() and promotedName == GRM_G.currentName then
                GRM.PopulateMemberDetails ( GRM_G.currentName );
            end
        end

        if GRM_G.BuildVersion >= 80000 and CommunitiesFrame ~= nil and CommunitiesFrame.GuildMemberDetailFrame:IsVisible() and promotedName == GRM_G.currentName then
            CommunitiesFrame.GuildMemberDetailFrame.RankDropdown.Text:SetText ( newRank );
        end

        -- Now, let's make the changes immediate for the button date.
        if GRM_UI.GRM_MemberDetailMetaData.GRM_SetPromoDateButton:IsVisible() then
            GRM_UI.GRM_MemberDetailMetaData.GRM_SetPromoDateButton:Hide();
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:SetText ( GRM.L ( "Promoted:" ) .. " " .. GRM.Trim ( string.sub ( timestamp , 1 , 10 ) ) );
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:Show();
        end

        -- Only want to refresh these things when NOT using the macro tool - the macro tool will do its own refresh.
        if promoterName ~= GRM_G.addonUser or ( promoterName == GRM_G.addonUser and GRM_UI.GRM_ToolCoreFrame ~= nil and not GRM_UI.GRM_ToolCoreFrame:IsVisible() ) then
            C_Timer.After ( 1 , function()
                GRM.RefreshSelectHybridFrames ( true , true , true , true );
            end);
        end
    end
end

-- Method:          GRM.RemoveOldRosterButtonHighlights()
-- What it Does:    Removes the button highlight from the click action
-- Purpose:         Purely aesthetics.
GRM.RemoveOldRosterButtonHighlights = function ()
    local rosterB;
    for i = 1 , GRM_UI.ContainerButtonCount do
        rosterB = GetClickFrame ( GRM_UI.OldRosterButtonName .. i );
        rosterB:UnlockHighlight();
    end

    if GRM_G.BuildVersion < 40000 then
        for i = 1 , GRM_UI.ContainerButtonCount do
            rosterB = _G[ "GuildFrameGuildStatusButton" .. i ];

            rosterB:UnlockHighlight();
        end
    end
end

-- Method:          GRM.PopulateOptionsRankDropDown ()
-- What it Does:    Adds all the guild ranks to the drop down menu
-- Purpose:         UI Feature
GRM.PopulateOptionsRankDropDown = function ()
    -- populating the frames!
    local buffer = 3;
    local height = 0;
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownMenu.Buttons = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownMenu.Buttons or {};

    -- Resetting the buttons!
    for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownMenu.Buttons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownMenu.Buttons[i][1]:Hide();
    end
    
    local i = 1;
    for count = 1 , GuildControlGetNumRanks() do
        if not GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownMenu.Buttons[i] then
            local tempButton = CreateFrame ( "Button" , "rankIndex" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownMenu );
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownMenu.Buttons[i] = { tempButton , tempButton:CreateFontString ( "rankIndexText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) }
        end

        local RankButtons = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownMenu.Buttons[i][1];
        local RankButtonsText = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownMenu.Buttons[i][2];
        RankButtons:SetWidth ( 110 );
        RankButtons:SetHeight ( 11 );
        RankButtons:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        RankButtonsText:SetText ( GuildControlGetRankName ( count ) );
        RankButtonsText:SetWidth ( 110 );
        RankButtonsText:SetWordWrap ( false );
        RankButtonsText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 12 );
        RankButtonsText:SetPoint ( "CENTER" , RankButtons );
        RankButtonsText:SetJustifyH ( "CENTER" );
        RankButtonsText:SetTextColor ( 0 , 0.8 , 1 , 1 );

        if i == 1 then
            RankButtons:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownMenu , 0 , -7 );
            height = height + RankButtons:GetHeight();
        else
            RankButtons:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownMenu.Buttons[i - 1][1] , "BOTTOM" , 0 , -buffer );
            height = height + RankButtons:GetHeight() + buffer;
        end

        RankButtons:SetScript ( "OnClick" , function( self , button ) 
            if button == "LeftButton" then
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownMenu:Hide();
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownSelected:Show();
                local selectedRank = GRM.GetRankIndex ( GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownSelectedText:GetText() , self );
                local isRestricted , rankIndex = GRM.IsSyncRankGuildLeaderRestricted ( 2 );

                -- Now, are they different? If they are different, that is a problem!
                if not isRestricted or CanEditGuildInfo() or ( isRestricted and rankIndex == selectedRank ) then
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownSelectedText:SetText ( RankButtonsText:GetText() );
                    GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank = selectedRank;

                    GRM.UpdateGuildInfoWithNewValue ( 2 , selectedRank );

                    -- ban list check
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank < GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankBanList then
                        GRM.Report ( GRM.L ( "Warning! Ban List rank threshold is below the overall sync rank. Changing from \"{name}\" to \"{name2}\"" , GuildControlGetRankName ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankBanList + 1 ) ,  RankButtonsText:GetText() ) );

                        -- Saving the data
                        GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankBanList = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank;
                        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownSelectedText:SetText ( RankButtonsText:GetText() );

                        GRM.UpdateGuildInfoWithNewValue ( 3 , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank );

                        GRM.SyncSettings();
                    end
                   
                    -- Retrigger active addon users... Very important to know permissions
                    if not GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame:IsVisible() then
                        GRM.RegisterGuildAddonUsersRefresh();
                    end

                    --Let's re-initiate syncing!
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled and not GRMsyncGlobals.currentlySyncing and GRM_G.HasAccessToGuildChat then
                        GRMsync.TriggerFullReset();
                        -- Now, let's add a brief delay, 3 seconds, to trigger sync again
                        C_Timer.After ( 3 , GRMsync.Initialize );
                    end
                    -- Determine if player has access to guild chat or is in restricted chat rank
                    GRM_G.HasAccessToOfficerChat = false;
                    GRM.RegisterGuildChatPermission();
                    GRM.SyncSettings();
                else
                    -- Report to player about Guild Leader restriction
                    GRM.ReportRankRestriction ( rankIndex );
                end                
            end
        end); 
        RankButtons:Show();
        i = i + 1;
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownMenu:SetHeight ( height + 15 );
end

-- Method:          GRM.PopulateBanListOptionsDropDown ()
-- What it Does:    Adds all the guild ranks to the drop down menu for ban changes
-- Purpose:         UI Feature in options - greater control to keep sync of ban list to officers only, whilst allowing great sync with all guildies.
GRM.PopulateBanListOptionsDropDown = function ()
    -- populating the frames!
    local buffer = 3;
    local height = 0;
    local color1 = { 1 , 0 , 0 };
    local color2 = { 0 , 0.8 , 1 };

    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownMenu.Buttons = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownMenu.Buttons or {};

    -- Resetting the buttons!
    for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownMenu.Buttons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownMenu.Buttons[i][1]:Hide();
    end
    
    local i = 1;
    for count = 1 , GuildControlGetNumRanks() do
        if not GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownMenu.Buttons[i] then
            local tempButton = CreateFrame ( "Button" , "rankIndex" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownMenu );
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownMenu.Buttons[i] = { tempButton , tempButton:CreateFontString ( "rankIndexText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) }
        end

        local RankButtons = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownMenu.Buttons[i][1];
        local RankButtonsText = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownMenu.Buttons[i][2];
        RankButtons:SetWidth ( 110 );
        RankButtons:SetHeight ( 11 );
        RankButtons:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        RankButtonsText:SetText ( GuildControlGetRankName ( count ) );
        RankButtonsText:SetWidth ( 110 );
        RankButtonsText:SetWordWrap ( false );
        RankButtonsText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 12 );
        RankButtonsText:SetPoint ( "CENTER" , RankButtons );
        RankButtonsText:SetJustifyH ( "CENTER" );
        if i - 1 <= GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank then
            RankButtonsText:SetTextColor ( color2[1] , color2[2] , color2[3] , 1 );
        else
            RankButtonsText:SetTextColor ( color1[1] , color1[2] , color1[3] , 1 );
        end

        if i == 1 then
            RankButtons:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownMenu , 0 , -7 );
            height = height + RankButtons:GetHeight();
        else
            RankButtons:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownMenu.Buttons[i - 1][1] , "BOTTOM" , 0 , -buffer );
            height = height + RankButtons:GetHeight() + buffer;
        end

        RankButtons:SetScript ( "OnClick" , function( self , button ) 
            if button == "LeftButton" then
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownMenu:Hide();
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownSelected:Show();
                local selectedRank = GRM.GetRankIndex ( GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownSelectedText:GetText() , self );
                local isRestricted , rankIndex = GRM.IsSyncRankGuildLeaderRestricted ( 3 );

                -- Now, are they different? If they are different, that is a problem!
                if not isRestricted or CanEditGuildInfo() or ( isRestricted and rankIndex == selectedRank ) then

                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank < selectedRank then

                        GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankBanList = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank;
                        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownSelectedText:SetText ( GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownSelectedText:GetText() );
                        GRM.Report ( GRM.L ( "Warning! Unable to select a Ban List rank below \"{name}\"" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownSelectedText:GetText() )  .. "\n" .. GRM.L ( "Setting to match core filter rank" ) );

                        GRM.UpdateGuildInfoWithNewValue ( 3 , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank );
                    else
                        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownSelectedText:SetText ( RankButtonsText:GetText() );
                        GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankBanList = selectedRank;

                        GRM.UpdateGuildInfoWithNewValue ( 3 , selectedRank );
                    end

                    -- Re-trigger addon users permissions
                    if not GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame:IsVisible() then
                        GRM.RegisterGuildAddonUsersRefresh();
                    end

                    GRM.SyncSettings();

                    --Let's re-initiate syncing!
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncBanList and not GRMsyncGlobals.currentlySyncing and GRM_G.HasAccessToGuildChat then
                        GRMsync.TriggerFullReset();
                        -- Now, let's add a brief delay, 3 seconds, to trigger sync again
                        C_Timer.After ( 3 , GRMsync.Initialize );
                    end
                else
                    -- Report to player about Guild Leader restriction
                    GRM.ReportRankRestriction ( rankIndex );
                end
            end
        end);
        RankButtons:Show();
        i = i + 1;
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownMenu:SetHeight ( height + 15 );
end

-- Method:          GRM.PopulateDefaultDropDownRankMenu()
-- What it Does:    Adds all the guild ranks to the drop down menu for custom default
-- Purpose:         UI Feature in options - greater control to keep custom note sync display on each character neat.
GRM.PopulateDefaultDropDownRankMenu = function ()
    -- populating the frames!
    local buffer = 3;
    local height = 0;
    local color2 = { 0 , 0.8 , 1 };
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomRankDropDownMenu.Buttons = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomRankDropDownMenu.Buttons or {};

    -- Resetting the buttons!
    for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomRankDropDownMenu.Buttons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomRankDropDownMenu.Buttons[i][1]:Hide();
    end
    
    local i = 1;
    for count = 1 , GuildControlGetNumRanks() do
        if not GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomRankDropDownMenu.Buttons[i] then
            local tempButton = CreateFrame ( "Button" , "rankIndex" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomRankDropDownMenu );
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomRankDropDownMenu.Buttons[i] = { tempButton , tempButton:CreateFontString ( "rankIndexText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) }
        end

        local RankButtons = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomRankDropDownMenu.Buttons[i][1];
        local RankButtonsText = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomRankDropDownMenu.Buttons[i][2];
        RankButtons:SetWidth ( 110 );
        RankButtons:SetHeight ( 11 );
        RankButtons:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        RankButtonsText:SetText ( GuildControlGetRankName ( count ) );
        RankButtonsText:SetWidth ( 110 );
        RankButtonsText:SetWordWrap ( false );
        RankButtonsText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 12 );
        RankButtonsText:SetPoint ( "CENTER" , RankButtons );
        RankButtonsText:SetJustifyH ( "CENTER" );
        RankButtonsText:SetTextColor ( color2[1] , color2[2] , color2[3] , 1 );

        if i == 1 then
            RankButtons:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomRankDropDownMenu , 0 , -7 );
            height = height + RankButtons:GetHeight();
        else
            RankButtons:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomRankDropDownMenu.Buttons[i - 1][1] , "BOTTOM" , 0 , -buffer );
            height = height + RankButtons:GetHeight() + buffer;
        end

        RankButtons:SetScript ( "OnClick" , function( self , button ) 
            if button == "LeftButton" then
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomRankDropDownMenu:Hide();
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomSelected:Show();
                local selectedRank = GRM.GetRankIndex ( GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomSelectedText:GetText() , self );
                local isRestricted , rankIndex = GRM.IsSyncRankGuildLeaderRestricted ( 4 );

                -- Now, are they different? If they are different, that is a problem!
                if not isRestricted or CanEditGuildInfo() or ( isRestricted and rankIndex == selectedRank ) then

                    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomSelectedText:SetText ( RankButtonsText:GetText() );
                    GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankCustom = selectedRank;
                    GRM.UpdateGuildInfoWithNewValue ( 4 , selectedRank );
                    GRM.ResetAllUnmodifiedDefaulCustomNoteFilters();

                    GRM.SyncSettings();
                else
                    -- Report to player about Guild Leader restriction
                    GRM.ReportRankRestriction ( rankIndex );
                end
            end
        end);
        RankButtons:Show();
        i = i + 1;
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomRankDropDownMenu:SetHeight ( height + 15 );
end

-- Method:          GRM.ResetAllUnmodifiedDefaulCustomNoteFilters()
-- What it Does:    Checks if the filter has ever been modified, and if it hasn't, then resets it to the default
-- Purpose:         Quality of life controls over the filters!
GRM.ResetAllUnmodifiedDefaulCustomNoteFilters = function ()
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            if not player.customNote[5] then
                player.customNote[4] = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankCustom;
            end
        end
    end
end

-- Method:          GRM.CreateRankDropDownMenu ( frame , fontstring , frame , int , int , function() )
-- What it Does:    It creates a dropdown menu that has all of the current guild ranks, with highest rank in guild on top
-- Purpose:         To create a generic, reusable dropdown menu for rank creation
GRM.CreateRankDropDownMenu = function ( SelectedFrame , Menu , fontSize , buttonHeight , logic )
    -- populating the frames!
    local buffer = 3;
    local height = 0;
    Menu.Buttons = Menu.Buttons or {};

    -- Resetting the buttons!
    for i = 1 , #Menu.Buttons do
        Menu.Buttons[i][1]:Hide();
    end
    
    local i = 1;
    for count = 1 , GuildControlGetNumRanks() do
        if not Menu.Buttons[i] then
            local tempButton = CreateFrame ( "Button" , Menu:GetName() .. "RankIndex_" .. i , Menu );
            Menu.Buttons[i] = { tempButton , tempButton:CreateFontString ( Menu:GetName() .. "RankIndexText_" .. i , "OVERLAY" , "GameFontWhiteTiny" ) }
        end

        local RankButtons = Menu.Buttons[i][1];
        local RankButtonsText = Menu.Buttons[i][2];
        RankButtons:SetWidth ( SelectedFrame:GetWidth() - 20 );
        RankButtons:SetHeight ( buttonHeight );
        RankButtons:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        RankButtonsText:SetText ( GuildControlGetRankName ( count ) );
        RankButtonsText:SetWidth ( SelectedFrame:GetWidth() - 20 );
        RankButtonsText:SetWordWrap ( false );
        RankButtonsText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + fontSize );
        RankButtonsText:SetPoint ( "CENTER" , RankButtons );
        RankButtonsText:SetJustifyH ( "CENTER" );

        if i == 1 then
            RankButtons:SetPoint ( "TOP" , Menu , 0 , -7 );
            height = height + RankButtons:GetHeight();
        else
            RankButtons:SetPoint ( "TOP" , Menu.Buttons[i - 1][1] , "BOTTOM" , 0 , -buffer );
            height = height + RankButtons:GetHeight() + buffer;
        end

        RankButtons:SetScript ( "OnClick" , function( self , button ) 
            if button == "LeftButton" then
                logic( self , RankButtonsText );
            end
        end);
        RankButtons:Show();
        i = i + 1;
    end
    Menu:SetHeight ( height + 15 );
end

-- Method:          GRM.CreateDropDownMenu ( frame , fontstring , frame , int , string  int ,  array , function() )
-- What it Does:    It creates a dropdown menu given the specific variables
-- Purpose:         To create a generic, reusable dropdown menus
GRM.CreateDropDownMenu = function ( SelectedFrame , Menu , fontSize , buttonHeight , fontModifier , textList , logic )
    -- populating the frames!
    local buffer = 3;
    local height = 0;
    Menu.Buttons = Menu.Buttons or {};

    -- Resetting the buttons!
    for i = 1 , #Menu.Buttons do
        Menu.Buttons[i][1]:Hide();
    end
    
    for i = 1 , #textList do
        if not Menu.Buttons[i] then
            local tempButton = CreateFrame ( "Button" , Menu:GetName() .. "_" .. i , Menu );
            Menu.Buttons[i] = { tempButton , tempButton:CreateFontString ( Menu:GetName() .. "Text_" .. i , "OVERLAY" , "GameFontWhiteTiny" ) }
        end

        local RankButtons = Menu.Buttons[i][1];
        local RankButtonsText = Menu.Buttons[i][2];
        RankButtons:SetWidth ( SelectedFrame:GetWidth() - 5 );
        RankButtons:SetHeight ( buttonHeight );
        RankButtons:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        RankButtonsText:SetText ( textList[i] );
        RankButtonsText:SetWidth ( SelectedFrame:GetWidth() - 20 );
        RankButtonsText:SetWordWrap ( false );
        RankButtonsText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + fontSize , fontModifier );
        RankButtonsText:SetPoint ( "CENTER" , RankButtons );
        RankButtonsText:SetJustifyH ( "CENTER" );

        if i == 1 then
            RankButtons:SetPoint ( "TOP" , Menu , 0 , -7 );
            height = height + RankButtons:GetHeight();
        else
            RankButtons:SetPoint ( "TOP" , Menu.Buttons[i - 1][1] , "BOTTOM" , 0 , -buffer );
            height = height + RankButtons:GetHeight() + buffer;
        end

        RankButtons:SetScript ( "OnClick" , function( self , button ) 
            if button == "LeftButton" then
                logic( self , i , RankButtonsText );
            end
        end);
        RankButtons:Show();
    end
    Menu:SetHeight ( height + 15 );
end

-- Method:          GRM.PopulateClassDropDownMenu ()
-- What it Does:    Adds all the player CLASSES to the drop down menu
-- Purpose:         This is useful for player selection of the class when manually adding a player's info to the metadata, like adding someone to a ban list.
GRM.PopulateClassDropDownMenu = function()
    -- populating the frames!
    local buffer = 3;
    local height = 0;
    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownMenu.Buttons = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownMenu.Buttons or {};

    -- Resetting the buttons!
    for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownMenu.Buttons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownMenu.Buttons[i][1]:Hide();
    end
    
    for i = 1 , #AllClasses do
        if not GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownMenu.Buttons[i] then
            local tempButton = CreateFrame ( "Button" , "ClassButton" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownMenu );
            GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownMenu.Buttons[i] = { tempButton , tempButton:CreateFontString ( "ClassButtonText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) }
            if i == 1 then
                GRM_G.DropDownHighlightLockIndex = 1;
                GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownMenu.Buttons[i][1]:LockHighlight();
            end
        end

        local ClassButtons = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownMenu.Buttons[i][1];
        local ClassButtonsText = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownMenu.Buttons[i][2];
        ClassButtons:SetWidth ( 110 );
        ClassButtons:SetHeight ( 11 );
        ClassButtons:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        ClassButtonsText:SetText ( GRM.L ( AllClasses[i] ) );
        local classCol = GRM.GetClassColorRGB ( string.upper ( AllClasses[i] ) );
        ClassButtonsText:SetTextColor ( classCol[1] , classCol[2] , classCol[3] , 1 );
        ClassButtonsText:SetWidth ( 110 );
        ClassButtonsText:SetWordWrap ( false );
        ClassButtonsText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 10 );
        ClassButtonsText:SetPoint ( "CENTER" , ClassButtons );
        ClassButtonsText:SetJustifyH ( "CENTER" );

        if i == 1 then
            ClassButtons:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownMenu , 0 , -7 );
            height = height + ClassButtons:GetHeight();
        else
            ClassButtons:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownMenu.Buttons[i - 1][1] , "BOTTOM" , 0 , -buffer );
            height = height + ClassButtons:GetHeight() + buffer;
        end

        ClassButtons:SetScript ( "OnClick" , function( self , button ) 
            if button == "LeftButton" then
                local parsedNumber = tonumber ( string.match ( self:GetName() , "(%d+)" ) );
                
                local classColors = GRM.GetClassColorRGB ( string.upper ( AllClasses[parsedNumber] ) );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownClassSelectedText:SetText ( ClassButtonsText:GetText() );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownClassSelectedText:SetTextColor ( classColors[1] , classColors[2] , classColors[3] , 1 );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownMenu:Hide();
                GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownClassSelected:Show();
                GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanReasonEditBox:SetFocus();
                GRM_G.tempAddBanClass = string.upper ( AllClasses[parsedNumber] );
            end
        end);
        ClassButtons:Show();
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_AddBanDropDownMenu:SetHeight ( height + 15 );
end

-- Method:          GRM.PopulateMainTagDropdown()
-- What it Does:    Creates a dropdown menu including options to choose from for main tag formatting in guild chat
-- Purpose:         Options, options, options! Customization to make it pleasing for all players in the formatting.
GRM.PopulateMainTagDropdown = function()
    local buffer = 3;
    local height = 0;
    local tagChoices = GRM.GetMainTags ( true );
    -- Initiate the buttons holder
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatMenu.Buttons = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatMenu.Buttons or {};

    for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatMenu.Buttons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatMenu.Buttons[i][1]:Hide();
    end

    for i = 1 , #tagChoices do
        if not GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatMenu.Buttons[i] then
            local tempButton = CreateFrame ( "Button" , "MainTagOption" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatMenu );
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatMenu.Buttons[i] = { tempButton , tempButton:CreateFontString ( "MainTagText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) }
        end

        local TagButton = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatMenu.Buttons[i][1];
        local TagButtonText = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatMenu.Buttons[i][2];
        TagButton:SetWidth ( 85 );
        TagButton:SetHeight ( 11 );
        TagButton:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        if i == 7 then
            TagButtonText:SetText ( GRM.L ( "No Tag" ) );
        else
            TagButtonText:SetText ( tagChoices[i] );
        end
        TagButtonText:SetTextColor ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["mainTagColor"].r , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["mainTagColor"].b , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["mainTagColor"].g , 1 );
        TagButtonText:SetWidth ( 85 );
        TagButtonText:SetWordWrap ( false );
        TagButtonText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 10 );
        TagButtonText:SetPoint ( "CENTER" , TagButton );
        TagButtonText:SetJustifyH ( "CENTER" );

        if i == 1 then
            TagButton:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatMenu , 0 , -7 );
            height = height + TagButton:GetHeight();
        else
            TagButton:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatMenu.Buttons[i - 1][1] , "BOTTOM" , 0 , -buffer );
            height = height + TagButton:GetHeight() + buffer;
        end

        TagButton:SetScript ( "OnClick" , function( self , button ) 
            if button == "LeftButton" then
                local parsedNumber = tonumber ( string.match ( self:GetName() , "(%d+)" ) );

                GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].mainTagIndex = parsedNumber;
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatSelected.GRM_TagText:SetText ( TagButtonText:GetText() );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatSelected.GRM_TagText:SetTextColor ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["mainTagColor"].r , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["mainTagColor"].b , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["mainTagColor"].g , 1 );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatMenu:Hide();
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatSelected:Show();
                GRM.SyncSettings();
            end
        end);
        TagButton:Show();
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatMenu:SetHeight ( height + 15 );
end

-- Method:          GRM.PopulateDefaultTabDropdown()
-- What it Does:    Creates a dropdown menu including options to choose from the GRM tabs as to which is default
-- Purpose:         Quality of Life preference control option
GRM.PopulateDefaultTabDropdown = function()
    local buffer = 3;
    local height = 0;
    local tabChoices = { GRM.L ( "LOG" ) , GRM.L ( "EVENTS" ) , GRM.L ( "BAN LIST" ) , GRM.L ( "SYNC USERS" ) , string.upper ( GRM.L ( "Options" ) ) , GRM.L ( "AUDIT" ) };

    -- Initiate the buttons holder
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabMenu.Buttons = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabMenu.Buttons or {};

    for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabMenu.Buttons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabMenu.Buttons[i][1]:Hide();
    end

    for i = 1 , #tabChoices do
        if not GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabMenu.Buttons[i] then
            local tempButton = CreateFrame ( "Button" , "DefaultTabButton" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabMenu );
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabMenu.Buttons[i] = { tempButton , tempButton:CreateFontString ( "DefaultTabButtonText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) }
        end

        local TabButton = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabMenu.Buttons[i][1];
        local TabButtonText = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabMenu.Buttons[i][2];
        TabButton:SetWidth ( 105 );
        TabButton:SetHeight ( 11 );
        TabButton:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        TabButtonText:SetText ( tabChoices[i] );
        TabButtonText:SetWidth ( 105 );
        TabButtonText:SetWordWrap ( false );
        TabButtonText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 9.5 );
        TabButtonText:SetPoint ( "CENTER" , TabButton );
        TabButtonText:SetJustifyH ( "CENTER" );

        if i == 1 then
            TabButton:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabMenu , 0 , -7 );
            height = height + TabButton:GetHeight();
        else
            TabButton:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabMenu.Buttons[i - 1][1] , "BOTTOM" , 0 , -buffer );
            height = height + TabButton:GetHeight() + buffer;
        end

        TabButton:SetScript ( "OnClick" , function( self , button ) 
            if button == "LeftButton" then
                GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].defaultTabSelection[2] = tonumber ( string.match ( self:GetName() , "%d+" ) );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabSelected.GRM_DefaultTabSelectedText:SetText ( TabButtonText:GetText() );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabMenu:Hide();
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabSelected:Show();
                GRM.SyncSettings();
            end
        end);
        TabButton:Show();
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabMenu:SetHeight ( height + 15 );
end

-- Method:          GRM.PopulateLanguageDropdown()
-- What it Does:    Populates a dropdown select menu with all of the available languages to choose from...
-- Purpose:         To give the player the option to manually select and change which language the addon is using.
GRM.PopulateLanguageDropdown = function()
    local buffer = 3;
    local height = 0;
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageDropDownMenu.Buttons = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageDropDownMenu.Buttons or {};

    for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageDropDownMenu.Buttons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageDropDownMenu.Buttons[i][1]:Hide();
    end

    for i = 1 , #GRML.Languages do
        if not GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageDropDownMenu.Buttons[i] then
            local tempButton = CreateFrame ( "Button" , "GRM_Language_" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageDropDownMenu );
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageDropDownMenu.Buttons[i] = { tempButton , tempButton:CreateFontString ( "GRM_LanguageButtonText_" .. i , "OVERLAY" , "GameFontWhiteTiny" ) }
        end

        local LangButton = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageDropDownMenu.Buttons[i][1];
        local LangButtonText = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageDropDownMenu.Buttons[i][2];
        LangButton:SetWidth ( 110 );
        LangButton:SetHeight ( 11 );
        LangButton:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        LangButtonText:SetText ( GRM.L ( GRML.Languages[i] ) );
        LangButtonText:SetWidth ( 105 );
        LangButtonText:SetWordWrap ( false );
        LangButtonText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 10 );
        LangButtonText:SetPoint ( "CENTER" , LangButton );
        LangButtonText:SetJustifyH ( "CENTER" );

        if i == 1 then
            LangButton:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageDropDownMenu , 0 , -7 );
            height = height + LangButton:GetHeight();
        else
            LangButton:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageDropDownMenu.Buttons[i - 1][1] , "BOTTOM" , 0 , -buffer );
            height = height + LangButton:GetHeight() + buffer;
        end

        LangButton:SetScript ( "OnClick" , function( self , button ) 
            if button == "LeftButton" then
                local parsedNumber = tonumber ( string.match ( self:GetName() , "(%d+)" ) );
                GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].selectedLang = parsedNumber;
                GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].selectedFont = GRML.GetFontChoiceIndex ( parsedNumber );
                GRML.SetNewLanguage( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].selectedLang , false , false );

                local HourFormat = { GRM.L ( "24 Hour" ) , GRM.L ( "12 Hour (am/pm)" ) };
                local tabChoices = { GRM.L ( "LOG" ) , GRM.L ( "EVENTS" ) , GRM.L ( "BAN LIST" ) , GRM.L ( "SYNC USERS" ) , string.upper ( GRM.L ( "Options" ) ) , GRM.L ( "AUDIT" ) };

                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageSelected.GRM_LanguageSelectedText:SetText ( GRM.L ( GRML.Languages[parsedNumber] ) );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontSelected.GRM_FontSelectedText:SetText ( GRML.FontNames[GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].selectedFont] );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontSelected.GRM_FontSelectedText:SetFont ( GRML.listOfFonts[GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].selectedFont] , GRM_G.FontModifier + 11 );
                local month , day , year = select ( 2 , GRM.CalendarGetDate() );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelected.GRM_TimestampSelectedText:SetText( GRM.FormatTimeStamp ( day .. " " .. monthEnum2 [ tostring ( month ) ] .. " '" .. ( year - 2000 ) , false ) );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabSelected.GRM_DefaultTabSelectedText:SetText ( tabChoices[GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].defaultTabSelection[2]] );
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].twentyFourHrScale then
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelected.GRM_24HrSelectedText:SetText ( HourFormat[1] );
                else
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelected.GRM_24HrSelectedText:SetText ( HourFormat[2] );
                end

                -- Macro Tool open
                if GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame:IsVisible() then
                    local matchString = GRM.L ( "Click to Set" );
                    if GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.rule.customLog and #GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.rule.customLogMsg > 0  then
                        matchString = GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.rule.customLogMsg;
                    end 
                    GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.GRM_CustomLogMessageEditBox:SetText ( matchString );
                    GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.GRM_CustomLogMessageEditBox.stringPattern = matchString;

                    if GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.rule.isMonths then
                        GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.GRM_TimeScaleSelected.GRM_TimeScaleSelectedText:SetText ( GRM.L ( "Months") );
                    else
                        GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.GRM_TimeScaleSelected.GRM_TimeScaleSelectedText:SetText ( GRM.L ( "Days") );
                    end
                    if GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.rule.rankSpecialIsMonths then
                        GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.GRM_KickEvenIfActiveTimeSelected.GRM_KickEvenIfActiveTimeSelectedText:SetText ( GRM.L ( "Months") );
                    else
                        GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.GRM_KickEvenIfActiveTimeSelected.GRM_KickEvenIfActiveTimeSelectedText:SetText ( GRM.L ( "Days") );
                    end
                    if GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.rule.repFilter then
                        GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.GRM_GuildRepRanksSelected.GRM_GuildRepRanksSelectedText:SetText ( GRM.GetReputationTextLevel ( GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.rule.rep , true ) );
                    else
                        GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.GRM_GuildRepRanksSelected.GRM_GuildRepRanksSelectedText:SetText ( GRM.GetReputationTextLevel ( GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.rule.rep , false ) );
                    end
                    if GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.rule.noteMatch then
                        if GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.rule.matchingString == "" then
                            matchString = GRM.L ( "Click to Set" );
                        else
                            matchString = GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.rule.matchingString;
                        end
        
                        GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.GRM_NoteSearchEditBox:SetText ( matchString );
                        GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame.GRM_NoteSearchEditBox.stringPattern = matchString;
                    end
                end

                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageDropDownMenu:Hide();
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageSelected:Show();
                GRM_UI.ElvUIReset = true;
                GRM_UI.ElvUIReset2 = true;
                -- Check the language count!
                local count = GRML.GetNumberUntranslatedLines ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].selectedLang );
                if GRM_G.Region == "enUS" then
                    count = count - 10;
                end
                if count > 0 and not GRML.TranslationStatusEnum[ GRML.Languages [ GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].selectedLang ] ] then
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageCountText:SetText ( GRM.L ( "{num} phrases still need translation to {name}" , GRM.L ( GRML.Languages[GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].selectedLang] ) , nil , count ) );
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageCountText:Show();
                else
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageCountText:Hide();
                end

                if IsAddOnLoaded ( "AddOnSkins" ) then
                    GRM_UI.GRM_RosterChangeLogFrame:Hide();
                    GRM_UI.GRM_RosterChangeLogFrame:Show();
                    if GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
                        GRM_UI.GRM_MemberDetailMetaData:Hide();
                        GRM_G.pause = true;
                        GRM_UI.GRM_MemberDetailMetaData:Show();
                    end
                end

                if parsedNumber == GRM_G.LocalizedIndex then
                    GRM.Report ( GRM.L ( "Font has been Reset to DEFAULT." ) );
                end
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ScanningOptionsFrame.GRM_LevelRange:SetText ( GRM.GetLevelRange() );

                -- Let's reprocess the language!
                GRM.ReprocessAllLogEntriesToCurrentLanguage();

                GRM.SyncSettings();

                if #GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_RosterChangeLogScrollChildFrame.AllButtons > 0 then
                    GRM.ResetLogStringPoints ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showLineNumbers );
                end
            end
        end);
        LangButton:Show();
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageDropDownMenu:SetHeight ( height + 15 );
end

-- Method:          GRM.PopulateFontDropdown()
-- What it Does:    Builds the font dropdown box
-- Purpose:         Give the user more customizability over the addon.
GRM.PopulateFontDropdown = function()
    local buffer = 3;
    local height = 0;
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontDropDownMenu.Buttons = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontDropDownMenu.Buttons or {};

    for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontDropDownMenu.Buttons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontDropDownMenu.Buttons[i][1]:Hide();
    end

    for i = 1 , #GRML.FontNames do
        if not GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontDropDownMenu.Buttons[i] then
            local tempButton = CreateFrame ( "Button" , "GRM_Font" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontDropDownMenu );
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontDropDownMenu.Buttons[i] = { tempButton , tempButton:CreateFontString ( "GRM_FontButtonText_" .. i , "OVERLAY" , "GameFontWhiteTiny" ) }
        end

        local FontButton = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontDropDownMenu.Buttons[i][1];
        local FontButtonText = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontDropDownMenu.Buttons[i][2];
        local additionalModifier = 0;
        if i == 4 then                             -- China
            additionalModifier = 0.5;
        elseif i == 5 then                         -- Taiwan
            additionalModifier = 2;
        elseif i == 6 then                         -- Action Man
            additionalModifier = 1;
        elseif i == 7 then                         -- Ancient
            additionalModifier = 2;
        elseif i == 9 then                         -- Cardinal
            additionalModifier = 2;
        elseif i == 10 then                        -- Continuum
            additionalModifier = 1;
        elseif i == 11 then                        -- Espressway
            additionalModifier = 1;
        elseif i == 13 then                        -- PT Sans
            additionalModifier = 2;
        elseif i == 14 then                        -- Roboto
            additionalModifier = 1;
        end
        FontButton:SetWidth ( 110 );
        FontButton:SetHeight ( 11 );
        FontButton:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        FontButtonText:SetText ( GRML.FontNames[i] );
        FontButtonText:SetWidth ( 105 );
        FontButtonText:SetWordWrap ( false );
        FontButtonText:SetFont ( GRML.listOfFonts[i] , GRM_G.FontModifier + additionalModifier + 10 );
        FontButtonText:SetPoint ( "CENTER" , FontButton );
        FontButtonText:SetJustifyH ( "CENTER" );

        if i == 1 then
            FontButton:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontDropDownMenu , 0 , -7 );
            height = height + FontButton:GetHeight();
        else
            FontButton:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontDropDownMenu.Buttons[i - 1][1] , "BOTTOM" , 0 , -buffer );
            height = height + FontButton:GetHeight() + buffer;
        end

        FontButton:SetScript ( "OnClick" , function( self , button ) 
            if button == "LeftButton" then
                local parsedNumber = tonumber ( string.match ( self:GetName() , "(%d+)" ) );

                GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].selectedFont = parsedNumber;
                GRML.SetNewFont ( parsedNumber );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontSelected.GRM_FontSelectedText:SetText ( FontButtonText:GetText() );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontSelected.GRM_FontSelectedText:SetFont ( GRML.listOfFonts[parsedNumber] , GRM_G.FontModifier + 11 );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontDropDownMenu:Hide();
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontSelected:Show();

                -- Additional frame check...
                GRM_UI.ElvUIReset = true;
                GRM_UI.ElvUIReset2 = true;
                if IsAddOnLoaded ( "AddOnSkins" ) then
                    GRM_UI.GRM_RosterChangeLogFrame:Hide();
                    GRM_UI.GRM_RosterChangeLogFrame:Show();
                    if GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
                        GRM_UI.GRM_MemberDetailMetaData:Hide();
                        GRM_G.pause = true;
                        GRM_UI.GRM_MemberDetailMetaData:Show();
                    end
                end
                GRM.SyncSettings();
            end
        end);
        FontButton:Show();
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontDropDownMenu:SetHeight ( height + 15 );
end

-- Method:          GRM.PopulateTimestampFormatDropDown()
-- What it Does:    Builds a dropdown menu displaying the various format options
-- Purpose:         To give the player the ability to adjust timestamp formats
GRM.PopulateTimestampFormatDropDown = function()
    local buffer = 4;
    local height = 0;
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelectedDropDownMenu.Buttons = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelectedDropDownMenu.Buttons or {};

    for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelectedDropDownMenu.Buttons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelectedDropDownMenu.Buttons[i][1]:Hide();
    end

    local month , day , year = select ( 2 , GRM.CalendarGetDate() );
    local timestamp = day .. " " .. monthEnum2 [ tostring ( month ) ] .. " '" .. ( year - 2000 );
    local tempTimestampHolder = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].dateFormat;

    for i = 1 , 15 do
        if not GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelectedDropDownMenu.Buttons[i] then
            local tempButton = CreateFrame ( "Button" , "GRM_timeStampButton" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelectedDropDownMenu );
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelectedDropDownMenu.Buttons[i] = { tempButton , tempButton:CreateFontString ( "GRM_GRM_timeStampButton_Text" .. i , "OVERLAY" , "GameFontWhiteTiny" ) }
        end
        GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].dateFormat = i;
        local timeStampButton = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelectedDropDownMenu.Buttons[i][1];
        local timeStampButtonText = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelectedDropDownMenu.Buttons[i][2];
        timeStampButton:SetWidth ( 110 );
        timeStampButton:SetHeight ( 11 );
        timeStampButton:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        timeStampButtonText:SetText ( GRM.FormatTimeStamp ( timestamp , false ) );
        timeStampButtonText:SetWidth ( 105 );
        timeStampButtonText:SetWordWrap ( false );
        timeStampButtonText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 11 );
        timeStampButtonText:SetPoint ( "CENTER" , timeStampButton , 5 , 0 );
        timeStampButtonText:SetJustifyH ( "LEFT" );

        if i == 1 then
            timeStampButton:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelectedDropDownMenu , 0 , -7 );
            height = height + timeStampButton:GetHeight();
        else
            timeStampButton:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelectedDropDownMenu.Buttons[i - 1][1] , "BOTTOM" , 0 , -buffer );
            height = height + timeStampButton:GetHeight() + buffer;
        end

        timeStampButton:SetScript ( "OnClick" , function( self , button ) 
            if button == "LeftButton" then
                local parsedNumber = tonumber ( string.match ( self:GetName() , "(%d+)" ) );
                
                GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].dateFormat = parsedNumber;
                GRM.UpdateGuildInfoWithNewValue ( 1 , parsedNumber );

                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelected.GRM_TimestampSelectedText:SetText ( timeStampButtonText:GetText() );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelectedDropDownMenu:Hide();
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelected:Show();

                GRM.ReprocessAllLogEntriesToCurrentLanguage();

            end
        end);
        timeStampButton:Show();
    end
    GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].dateFormat = tempTimestampHolder;
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelectedDropDownMenu:SetHeight ( height + 15 );
    GRM.SyncSettings();
end

-- Method:          GRM.CreateServerSelectionDropdown ( array )
-- What it Does:    Builds a dropdown menu of all the connected realms for ban player creation
-- Purpose:         To ensure players accurately add players.
GRM.CreateServerSelectionDropdown = function ( theList )
    local buffer = 4;
    local height = 0; 
    local name = "Realm";
    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerDropDownMenu.Buttons = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerDropDownMenu.Buttons or {};

    for i = 1 , #theList do
        if not GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerDropDownMenu.Buttons[i] then
            local tempButton = CreateFrame ( "Button" , name .. "_Button" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerDropDownMenu );
            GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerDropDownMenu.Buttons[i] = { tempButton , tempButton:CreateFontString ( name .. "_Button_Text" .. i , "OVERLAY" , "GameFontWhiteTiny" ) }
            if i == 1 then
                GRM_G.DropDownHighlightLockIndex = 1;
                GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerDropDownMenu.Buttons[i][1]:LockHighlight();
            end
        end
        local Button = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerDropDownMenu.Buttons[i][1];
        local ButtonText = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerDropDownMenu.Buttons[i][2];
        Button:SetWidth ( GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerSelected:GetWidth() );
        Button:SetHeight ( 11 );
        Button:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        ButtonText:SetText ( theList[i] );
        ButtonText:SetWidth ( GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerSelected:GetWidth() - 5 );
        ButtonText:SetWordWrap ( false );
        ButtonText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 10 );
        ButtonText:SetPoint ( "CENTER" , Button );
        ButtonText:SetJustifyH ( "CENTER" );

        if i == 1 then
            Button:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerDropDownMenu , 0 , -7 );
            height = height + Button:GetHeight();
        else
            Button:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerDropDownMenu.Buttons[i - 1][1] , "BOTTOM" , 0 , -buffer );
            height = height + Button:GetHeight() + buffer;
        end

        Button:SetScript ( "OnClick" , function()
            GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerSelected.GRM_BanServerSelectedText:SetText ( ButtonText:GetText() );
            GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerSelected:Show();

            GRM_UI.CheckForBanPlayerAutoSelect ( true , true );
        end);
        Button:Show();
    end
    
    for i = #theList + 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerDropDownMenu.Buttons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerDropDownMenu.Buttons[i][1]:Hide();
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame.GRM_BanServerDropDownMenu:SetHeight ( height + 15 );
end

-- Method:          GRM.Populate24HrDropDown()
-- What it Does:    Builds the 2 options in 24 hr timescale vs 12 hr
-- Purpose:         To give the player the option to set it to a 12hr scale or 24hr scale.
GRM.Populate24HrDropDown = function()
    local buffer = 4;
    local height = 0;
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelectedDropDownMenu.Buttons = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelectedDropDownMenu.Buttons or {};

    for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelectedDropDownMenu.Buttons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelectedDropDownMenu.Buttons[i][1]:Hide();
    end

    local HourFormat = { GRM.L ( "24 Hour" ) , GRM.L ( "12 Hour (am/pm)" ) };

    for i = 1 , 2 do
        if not GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelectedDropDownMenu.Buttons[i] then
            local tempButton = CreateFrame ( "Button" , "GRM_HrButton" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelectedDropDownMenu );
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelectedDropDownMenu.Buttons[i] = { tempButton , tempButton:CreateFontString ( "GRM_HrButton_Txt" .. i , "OVERLAY" , "GameFontWhiteTiny" ) }
        end
        local HrButton = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelectedDropDownMenu.Buttons[i][1];
        local HrButtonText = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelectedDropDownMenu.Buttons[i][2];
        HrButton:SetWidth ( 110 );
        HrButton:SetHeight ( 11 );
        HrButton:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        HrButtonText:SetText ( HourFormat[i] );
        HrButtonText:SetWidth ( 105 );
        HrButtonText:SetWordWrap ( false );
        HrButtonText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 11 );
        HrButtonText:SetPoint ( "CENTER" , HrButton );
        HrButtonText:SetJustifyH ( "CENTER" );

        if i == 1 then
            HrButton:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelectedDropDownMenu , 0 , -7 );
            height = height + HrButton:GetHeight();
        else
            HrButton:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelectedDropDownMenu.Buttons[i - 1][1] , "BOTTOM" , 0 , -buffer );
            height = height + HrButton:GetHeight() + buffer;
        end

        HrButton:SetScript ( "OnClick" , function( self , button ) 
            if button == "LeftButton" then
                local parsedNumber = tonumber ( string.match ( self:GetName() , "(%d+)" ) );

                if parsedNumber == 1 then
                    GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].twentyFourHrScale = true;
                else
                    GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].twentyFourHrScale = false;
                end
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelected.GRM_24HrSelectedText:SetText ( HrButtonText:GetText() );
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelectedDropDownMenu:Hide();
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelected:Show();
                GRM.ReprocessAllLogEntriesToCurrentLanguage();
                GRM.SyncSettings();
            end
        end);
        HrButton:Show();
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelectedDropDownMenu:SetHeight ( height + 15 );
end

-- Method:          GRM.SetGroupInviteButton ( string )
-- What it Does:    Invites a selected guildie to your group
-- Purpose:         Continuity on previous Blizz default frames to these
GRM.SetGroupInviteButton = function ( handle )

    local setButtonAction = function()
        -- Player is not in any group, thus inviting them will create new group.
        
        GRM_UI.GRM_MemberDetailMetaData.GRM_GroupInviteButton:SetScript ( "OnClick" , function ( _ , button )
            if button == "LeftButton" then
                C_PartyInfo.InviteUnit ( handle );
            end
        end);
    end

    if GetNumGroupMembers() > 0  then                                                               -- If > 0 then player is in either a raid or a party. (1 will show if in an instance by oneself)
        local isGroupLeader = UnitIsGroupLeader ( "PLAYER" );                                       -- Party or Group
        local isInRaidWithAssist = UnitIsGroupAssistant ( "PLAYER" , LE_PARTY_CATEGORY_HOME );      -- Player Has Assist in Raid group
            
        if isGroupLeader or isInRaidWithAssist then                                         -- Player has the ability to invite to group
            setButtonAction();
        else            -- Player is in a group but does not have invite privileges
            GRM_UI.GRM_MemberDetailMetaData.GRM_GroupInviteButton.GRM_GroupInviteButtonText:SetText ( GRM.L ( "No Invite" ) );
            GRM_UI.GRM_MemberDetailMetaData.GRM_GroupInviteButton:SetScript ( "OnClick" , function ( _ , button )
                if button == "LeftButton" then
                    GRM.Report ( GRM.L ( "Player should try to obtain group invite privileges." ) );
                end
            end);
        end
    else
        setButtonAction();
    end

    if GRM_UI.GRM_MemberDetailMetaData.GRM_GroupInviteButton.GRM_GroupInviteButtonText:GetText() ~= nil then
        GRM_UI.ScaleFontStringToObjectSize ( true , GRM_UI.GRM_MemberDetailMetaData.GRM_GroupInviteButton:GetWidth() , GRM_UI.GRM_MemberDetailMetaData.GRM_GroupInviteButton.GRM_GroupInviteButtonText , 4 );
    end
end

-- Method:          GRM.CreateOptionsRankDropDown()
-- What it Does:    Builds the final rank drop down product for options panel
-- Purpose:         UI Feature for options to be able to filter who you will accept shared data from.
GRM.CreateOptionsRankDropDown = function ()
    GRM.PopulateOptionsRankDropDown();
    GRM.PopulateBanListOptionsDropDown();
    GRM.PopulateDefaultDropDownRankMenu();

    local numRanks = GuildControlGetNumRanks() - 1;
    local HourFormat = { GRM.L ( "24 Hour" ) , GRM.L ( "12 Hour (am/pm)" ) };
    local tabChoices = { GRM.L ( "LOG" ) , GRM.L ( "EVENTS" ) , GRM.L ( "BAN LIST" ) , GRM.L ( "SYNC USERS" ) , string.upper ( GRM.L ( "Options" ) ) , GRM.L ( "AUDIT" ) };
    
    -- General sync restriction
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank > numRanks then       -- There's been a change since the player last logged in...
        GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank = numRanks;
        GRM.UpdateGuildInfoWithNewValue ( 2 , numRanks );
        GRM.SyncSettings();
    end
    -- Ban List Sync restriction
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankBanList > numRanks then       -- There's been a change since the player last logged in...
        GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankBanList = numRanks;
        GRM.UpdateGuildInfoWithNewValue ( 3 , numRanks );
        GRM.SyncSettings();
    end
    -- Custom Note Sync Restriction
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankCustom > numRanks then       -- There's been a change since the player last logged in...
        GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankCustom = numRanks;
        GRM.UpdateGuildInfoWithNewValue ( 4 , numRanks );
        GRM.SyncSettings();
    end

    local setRankName = GuildControlGetRankName ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank + 1 );
    local setRankNameBanList = GuildControlGetRankName ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankBanList + 1 );
    local setCustomDefaultName = GuildControlGetRankName ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankCustom + 1 );
    
    if setRankName == nil or setRankName == "" then
        setRankName = GuildControlGetRankName ( 1 )     -- Default it to guild leader. This scenario could happen if the rank was removed or you change guild but still have old settings.
    end
    if setRankNameBanList == nil or setRankNameBanList == "" then
        setRankNameBanList = GuildControlGetRankName ( 1 )     -- Default it to guild leader. This scenario could happen if the rank was removed or you change guild but still have old settings.
    end

    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownSelectedText:SetText( setRankName );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownSelectedText:SetText ( setRankNameBanList );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomSelectedText:SetText ( setCustomDefaultName );

    local TagText = GRM.GetMainTags ( false , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].mainTagIndex );
    if TagText == "" then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatSelected.GRM_TagText:SetText ( GRM.L ( "No Tag" ) );
    else
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatSelected.GRM_TagText:SetText ( TagText );
    end

    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatSelected.GRM_TagText:SetTextColor ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["mainTagColor"].r , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["mainTagColor"].b , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["mainTagColor"].g , 1 );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageSelected.GRM_LanguageSelectedText:SetText ( GRM.L ( GRML.Languages[GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].selectedLang] ) );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_ColorSelectOptionsFrame.GRM_OptionsTexture:SetColorTexture ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["mainTagColor"].r , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["mainTagColor"].b , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["mainTagColor"].g , 1 );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontSelected.GRM_FontSelectedText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 11 );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontSelected.GRM_FontSelectedText:SetText ( GRML.FontNames[ GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].selectedFont ] );
    local month , day , year = select ( 2 , GRM.CalendarGetDate() );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelected.GRM_TimestampSelectedText:SetText( GRM.FormatTimeStamp ( day .. " " .. monthEnum2 [ tostring ( month ) ] .. " '" .. ( year - 2000 ) , false ) );
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].twentyFourHrScale then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelected.GRM_24HrSelectedText:SetText ( HourFormat[1] );
    else
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelected.GRM_24HrSelectedText:SetText ( HourFormat[2] );
    end

    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabSelected.GRM_DefaultTabSelectedText:SetText ( tabChoices[GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].defaultTabSelection[2]] );
    

    -- Now that initial values set, let's display them!
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterSyncRankDropDownSelected:Show();
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_RosterBanListDropDownSelected:Show();
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.GRM_DefaultCustomSelected:Show();
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_MainTagFormatSelected:Show();
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_LanguageSelected:Show();
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_FontSelected:Show();
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelected:Show();
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_24HrSelected:Show();
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_DefaultTabSelected:Show();
end

-- Method:              GRM.ClearPromoDateHistory ( string , bool )
-- What it Does:        Purges history of promotions as if they had just joined the guild.
-- Purpose:             Editing ability in case of user error.
GRM.ClearPromoDateHistory = function ( name , isUnknown )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];

    if player ~= nil then
        -- Ok, let's clear the history now!
        if not isUnknown then
            GRM_G.rankDateSet = false;
        end
        player.rankHistory = nil;
        player.rankHistory = {};
        table.insert ( player.rankHistory , { "" , "" , 0 } );
        player.verifiedPromoteDate = { "" , 0 };
        player.promoteDateUnknown = false;
        if name == GRM_G.currentName and GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
            GRM_UI.GRM_altDropDownOptions:Hide();
            if not isUnknown then
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:Hide();
                GRM_UI.GRM_MemberDetailMetaData.GRM_SetPromoDateButton:Show();
            end
        end
    end
end

-- Method:              GRM.ClearJoinDateHistory ( string )
-- What it Does:        Clears the player's history on when they joined/left/rejoined the guild to be as if they were  a new member
-- Purpose:             Micromanagement of toons metadata.
GRM.ClearJoinDateHistory = function ( name , isUnknown )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];

    if player ~= nil then
        -- Ok, let's clear the history now!
        player.joinDate = nil;   -- oldJoinDate wiped!
        player.joinDate = {};
        player.joinDateEpoch = nil;
        player.joinDateEpoch = {};
        player.leftGuildDate = nil;
        player.leftGuildDate = {};
        player.leftGuildEpoch= nil;
        player.leftGuildEpoch= {};
        player.verifiedJoinDate = { "" , 0 };
        player.joinDateUnknown = false;
        if name == GRM_G.currentName and GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
            GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:Hide();
            GRM_UI.GRM_altDropDownOptions:Hide();
            if not isUnknown then
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailJoinDateButton:Show();
            end
        end
    end
end

-- Method:              GRM.ResetPlayerMetaData ( string , string )
-- What it Does:        Purges all metadata from an alt up to that point and resets them as if they were just added to the guild roster
-- Purpose:             Metadata player management. QoL feature if ever needed.
GRM.ResetPlayerMetaData = function ( playerName )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][playerName];

    if player ~= nil then
        local classedName = GRM.GetStringClassColorByName ( playerName ) .. GRM.SlimName ( playerName ) .. "|r";
        GRM.Report ( GRM.L ( "{name}'s saved data has been wiped!" , classedName ) );

        GRM.GuildRoster();

        local member = {};
        for i = 1 , GRM.GetNumGuildies() do
            local name , rank , rankInd , level , _ , zone , _ , _ , online , status , class , achievementPoints , _ , isMobile , _ , rep , GUID = GetGuildRosterInfo ( i );
            if name == playerName then

                member.name = name                                    -- 1
                member.rankName = rank;                               -- 2
                member.rankIndex = rankInd;                           -- 3
                member.level = level;                                 -- 4
                member.note = player.note;                                   -- 5
                member.officerNote = player.officerNote;                 -- 6

                member.class = class;                                 -- 7
                member.lastOnline = GRM.GetHoursSinceLastOnline ( i , online ); -- 8 Time since they last logged in in hours.
                member.zone = zone;                                   -- 9
                member.achievementPoints = achievementPoints;         -- 10
                member.isMobile = isMobile;                           -- 11
                member.rep = rep;                                     -- 12
                member.isOnline = online;                             -- 13
                member.status = status;                               -- 14
                member.GUID = GUID;                                   -- 15

                local race , sex = select ( 4 , GetPlayerInfoByGUID ( GUID ) );
                if race == nil or sex == nil then
                    race , sex = select ( 4 , GetPlayerInfoByGUID ( GUID ) );   -- Call a second time... sometimes the server is weird and the first call produces nil, but the immediate 2nd does respond.
                    if race == nil or sex == nil then
                        race = "";
                        sex = 1;
                    end
                end

                member.race = race;                                   -- 16
                member.sex = sex;                                     -- 17
                member.rosterSelection = i;                           -- 18
                
                break;
            end
        end

        if #player.alts > 0 then
            GRM.RemoveAlt ( player.alts[1][1] , playerName , false , 0 , false );      -- Removing oneself from his alts list on clearing info so it clears him from them too.
        end

        GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][playerName] = nil;         -- Remove the player!

        if member.name ~= nil then
            GRM.AddMemberRecord( member , false , nil )     -- Re-Add the player!
            --Let's re-initiate syncing!
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled and not GRMsyncGlobals.currentlySyncing and GRM_G.HasAccessToGuildChat then
                if GRMsync.IsPlayerDataSyncCompatibleWithAnyOnline() then
                    GRM.Report ( GRM.L ( "Re-Syncing {name}'s Guild Data..." , classedName ) );
                    GRMsync.Initialize();
                else
                    GRM.Report ( GRM.L ( "No player's currently available to sync {name}'s Guild Data..." , classedName ) );
                end
            end
        end

        GRM_UI.GRM_MemberDetailMetaData:Hide();        
    end
end

-- Method:          GRM.ResetAllSavedData()
-- What it Does:    Purges literally ALL saved data, then rebuilds it from scratch as if addon was just installed.
-- Purpose:         Clear data for any purpose needed.
GRM.ResetAllSavedData = function()
    GRM.Report ( GRM.L ( "Wiping all Saved Roster Data Account Wide! Rebuilding from Scratch..." ) );

    GRM_GuildMemberHistory_Save = nil;
    GRM_GuildMemberHistory_Save = {};
    GRM_GuildMemberHistory_Save["H"] = {};
    GRM_GuildMemberHistory_Save["A"] = {};

    GRM_PlayersThatLeftHistory_Save = nil;
    GRM_PlayersThatLeftHistory_Save = {};
    GRM_PlayersThatLeftHistory_Save["H"] = {};
    GRM_PlayersThatLeftHistory_Save["A"] = {};

    GRM_LogReport_Save = nil;
    GRM_LogReport_Save = {};
    GRM_LogReport_Save["H"] = {};
    GRM_LogReport_Save["A"] = {};

    GRM_PlayerListOfAlts_Save = nil;
    GRM_PlayerListOfAlts_Save = {};
    GRM_PlayerListOfAlts_Save["H"] = {};
    GRM_PlayerListOfAlts_Save["A"] = {};

    GRM_GuildDataBackup_Save = nil;
    GRM_GuildDataBackup_Save = {};
    GRM_GuildDataBackup_Save["H"] = {};
    GRM_GuildDataBackup_Save["A"] = {};

    GRM_CalendarAddQue_Save = nil;
    GRM_CalendarAddQue_Save = {};
    GRM_CalendarAddQue_Save["H"] = {};
    GRM_CalendarAddQue_Save["A"] = {};

    GRM_DebugLog_Save = nil;
    GRM_DebugLog_Save = {};

    GRM_Misc = nil;
    GRM_Misc = {};

    -- Hide the window frame so it can quickly be reloaded.
    GRM_UI.GRM_MemberDetailMetaData:Hide();

    -- Now, let's rebuild...
    if IsInGuild() then
        GRM.BuildNewRoster();
    end
    -- Update the logFrame if it was open at the time too
    if GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogEditBox:SetText ( GRM.L ( "Search Filter" ) );
        GRM.BuildLog();
    end

    -- Update the ban list too!
    if GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame:IsVisible() then
        GRM.RefreshBanListFrames();
    end

    if GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame:IsVisible() then
        GRM.RefreshAuditFrames ( true , true );
    end

    -- Trigger Sync
    --Let's re-initiate syncing!
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled and not GRMsyncGlobals.currentlySyncing and GRM_G.HasAccessToGuildChat then
        GRMsync.TriggerFullReset();
        -- Now, let's add a brief delay, 3 seconds, to trigger sync again
        C_Timer.After ( 3 , GRMsync.Initialize );
    end
end

-- Method:          GRM.ResetGuildSavedData( string )
-- What it Does:    Purges all saved data from the guild and only the guild...
-- Purpose:         Sometimes you don't want to reset everything... just the guild.
GRM.ResetGuildSavedData = function ( guildName )
    GRM.Report ( GRM.L ( "Wiping all saved Guild data! Rebuilding from scratch..." ) );

    -- Removing Players that left saved metadaa
    GRM_PlayersThatLeftHistory_Save[GRM_G.F][guildName] = nil;

    -- removing Player Saved metadata of the guild
    GRM_GuildMemberHistory_Save[GRM_G.F][guildName] = nil;
    
    -- Clearing the Guild Log...Resetting the add to calendar que
    GRM_CalendarAddQue_Save[GRM_G.F][guildName] = nil;

    -- Clearing the Guild Log...
    GRM_LogReport_Save[GRM_G.F][guildName] = nil;

    -- Clear the backups... they gotta go too!
    GRM_GuildDataBackup_Save[GRM_G.F][guildName] = nil;

    -- Hide the window frame so it can quickly be reloaded.
    GRM_UI.GRM_MemberDetailMetaData:Hide();

    -- Now, let's rebuild...
    if IsInGuild() then
        GRM.BuildNewRoster();
    end

    C_Timer.After ( 3 , function()
        -- Update the logFrame if it was open at the time too
        if GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogEditBox:SetText ( GRM.L ( "Search Filter" ) );
            GRM.BuildLog();
        end

        -- Update the ban list too!
        if GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame:IsVisible() then
            GRM.RefreshBanListFrames();
        end

        if GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame:IsVisible() then
            GRM.RefreshAuditFrames ( true , true );
        end
        
        --Let's re-initiate syncing!
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled and not GRMsyncGlobals.currentlySyncing and GRM_G.HasAccessToGuildChat and not GRM_G.OnFirstLoad then
            GRMsync.TriggerFullReset();
            -- Now, let's add a brief delay, 3 seconds, to trigger sync again
            C_Timer.After ( 3 , GRMsync.Initialize );
        end
    end);
end

-- Method:          GRM.LiveChangesCheck ( int , string )
-- What it DoeS:    It removes repeat log instances of changes that happen live
-- Purpose:         To prevent double log reporting of live changes. A single scan may take several seconds, especially in large guilds as it splits the scan up to prevent any momentary stutter.
--                  Well, what if a change is live detected during this scan? This updates the logs to remove the changes if so.
GRM.LiveChangesCheck = function ( indexOfAction , logEntry )
    -- 1 = Promotions
    if indexOfAction == 1 then
        for i = 1 , #GRM_G.TempLogPromotion do
            if GRM_G.TempLogPromotion[i][2] == logEntry then
                table.remove ( GRM_G.TempLogPromotion , i );
                break;
            end
        end
    -- 2 = Demotion
    elseif indexOfAction == 2 then
        for i = 1 , #GRM_G.TempLogDemotion do
            if GRM_G.TempLogDemotion[i][2] == logEntry then
                table.remove ( GRM_G.TempLogDemotion , i );
                break;
            end
        end
    -- 3 = Left Guild
    elseif indexOfAction == 3 then
        for i = 1 , #GRM_G.TempLeftGuild do
            if GRM_G.TempLeftGuild[i][2] == logEntry then
                table.remove ( GRM_G.TempLeftGuild , i );
                break;
            end
        end
    -- 4 = joined/rejoined guild
    elseif indexOfAction == 4 then
        for i = 1 , #GRM_G.TempNewMember do
            if GRM_G.TempNewMember[i][2] == logEntry then
                table.remove ( GRM_G.TempNewMember , i );
                break;
            end
        end
        for i = 1 , #GRM_G.TempRejoin do
            if GRM_G.TempRejoin[i][2] == logEntry then
                table.remove ( GRM_G.TempRejoin , i );
                break;
            end
        end
        for i = 1 , #GRM_G.TempBannedRejoin do
            if GRM_G.TempBannedRejoin[i][2] == logEntry then
                table.remove ( GRM_G.TempBannedRejoin , i );
                break;
            end
        end
    end
end

-- Method:          GRM.CheckForNewPlayer ( int , int )
-- What it Does:    First parses the system message, then quickly determines if there is a new player that just joined the guild and then builds their profile
-- Purpose:         For instant join data for the log, rather than having to wait up to 10 seconds.
GRM.CheckForNewPlayer = function( clubID , memberID )

    if GRM_G.gClubID == 0 then
        GRM_G.gClubID = C_Club.GetGuildClubId();
    end

    if GRM_G.gClubID == nil or GRM_G.gClubID == 0 then
        C_Timer.After ( 3 , function()
            GRM_G.gClubID = C_Club.GetGuildClubId();
            GRM.CheckForNewPlayer ( clubID , memberID );
        end);
        return;
    end

    if C_Club.GetMemberInfo ( clubID , memberID ).name ~= nil then           -- This means it shows successfully 1 found player...
        
        local memberInfo = C_Club.GetMemberInfo ( clubID , memberID );        
        local rosterName = GRM.FormatNameWithPlayerServer ( memberInfo.name );

        -- This means a system message fired erroneously. This is an edge case that can happen when servers merge.
        if GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][rosterName] ~= nil then
            GRM_G.RejoinControlCheck = 0;
            return;
        end

        local note = memberInfo.memberNote or "";
        local guildRank = memberInfo.guildRank or GuildControlGetRankName ( GuildControlGetNumRanks() );
        local rankOrder = memberInfo.guildRankOrder or GuildControlGetNumRanks();
        local level = memberInfo.level or 1;
        local pts = memberInfo.achievementPoints or 100;
        local guid = memberInfo.guid or "";
        local oNote = memberInfo.officerNote or "";
        local classFile = select ( 2 , GetClassInfo ( memberInfo.classID ) );
        local zone = memberInfo.zone or "";

        local race , sex;
        if guid ~= "" then
            race , sex = select ( 4 , GetPlayerInfoByGUID ( guid ) );
        end
        if race == nil or sex == nil then
            race , sex = select ( 4 , GetPlayerInfoByGUID ( guid ) );   -- Call a second time... sometimes the server is weird and the first call produces nil, but the immediate 2nd does respond.
            if race == nil or sex == nil then
                race = "";
                sex = 1;
            end
        end

        local player = {};

        player.name = rosterName                                    -- 1
        player.rankName = guildRank;                                -- 2
        player.rankIndex = ( rankOrder - 1 );                       -- 3 (It needs to be 1 less to match when compared to the guildRosterInfo call )
        player.level = level;                                       -- 4
        player.note = note;                                         -- 5
        if GRM.CanViewOfficerNote() then -- Officer Note permission to view.
            player.officerNote = oNote;                             -- 6
        else
            player.officerNote = nil; -- Set Officer note to nil if needed due to player not being able to view. - If it is set to "" then player will think it is changing.
        end
        player.class = classFile;                                   -- 7
        player.lastOnline = 0;                                      -- 8 Time since they last logged in in hours.
        player.zone = zone;                                         -- 9
        player.achievementPoints = pts;                             -- 10
        player.isMobile = false;                                    -- 11
        player.rep = 4;                                             -- 12
        player.isOnline = true;                                     -- 13
        player.status = 0;                                          -- 14
        player.GUID = guid;                                         -- 15
        player.race = race;                                         -- 16
        player.sex = sex;                                           -- 17
        player.rosterSelection = 0;                                 -- 18

        for i = 1 , GRM.GetNumGuildies() do
            local name = GetGuildRosterInfo ( i );
            if name == rosterName then
                player.rosterSelection = i;
                break;
            end
        end

        GRM_G.changeHappenedExitScan = true;
        GRM.RecordJoinChanges ( player , GRM.GetClassColorRGB ( classFile , true ) .. GRM.SlimName ( rosterName ) .. "|r" , true , select ( 2 , GRM.GetTimestamp() ) , true );

        -- Check Main Auto tagging...
        if not GRM_G.OnFirstLoad then
            GRM.SetGuildInfoDetails();
        end
        -- -- Delay for time to check "Unique Accounts" change...
        C_Timer.After ( 10 , function()               
            if GRM_G.DesignateMain then
                GRM.SetMain ( rosterName , rosterName , false , 0 );
                GRM.Report ( GRM.L ( "GRM Auto-Detect! {name} has joined the guild and will be set as Main" , GRM.GetClassifiedName ( rosterName , true ) ) );
                if GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame:IsVisible() then
                    GRM.RefreshAuditFrames ( true , true );
                end
            end
        end);

    elseif GRM_G.RejoinControlCheck <= 50 then
        GRM_G.RejoinControlCheck = GRM_G.RejoinControlCheck + 1;
        C_Timer.After ( 0.1 , function()
            -- Re-Check 1 time.
            GRM.CheckForNewPlayer( clubID , memberID );
        end);
        return;
    end

    GRM_G.RejoinControlCheck = 0;
end

-- Method:          GRM.CheckForLeftOrKickedPlayer ( int , int )
-- What it Does:    Checks for Left Player Info and registers a player if they leave the guild.
-- Purpose:         Find them in the eventLog to be removed.
GRM.CheckForLeftOrKickedPlayer = function ( clubID , memberID )
    if GRM_G.gClubID == 0 then
        GRM_G.gClubID = C_Club.GetGuildClubId();
    end

    if GRM_G.gClubID == nil or GRM_G.gClubID == 0 then
        C_Timer.After ( 3 , function()
            GRM_G.gClubID = C_Club.GetGuildClubId();
            GRM.CheckForLeftOrKickedPlayer ( clubID , memberID );
        end);
        return;
    end

    if C_Club.GetMemberInfo ( clubID , memberID ).name ~= nil then           -- This means it shows successfully 1 found player...
        local memberInfo = C_Club.GetMemberInfo ( clubID , memberID );
        local rosterName = GRM.FormatNameWithPlayerServer ( memberInfo.name )

        local note = memberInfo.memberNote or "";
        local zone = memberInfo.zone or "";
        local guildRank = memberInfo.guildRank or GuildControlGetRankName ( GuildControlGetNumRanks() );
        local rankOrder = memberInfo.guildRankOrder or GuildControlGetNumRanks();
        local level = memberInfo.level or 1;
        local pts = memberInfo.achievementPoints or 0;
        local guid = memberInfo.guid or "";
        local oNote = memberInfo.officerNote or "";
        local classFile = select ( 2 , GetClassInfo ( memberInfo.classID ) );

        local race , sex;
        if guid ~= "" then
            race , sex = select ( 4 , GetPlayerInfoByGUID ( guid ) );
        end
        if race == nil or sex == nil then
            race , sex = select ( 4 , GetPlayerInfoByGUID ( guid ) );   -- Call a second time... sometimes the server is weird and the first call produces nil, but the immediate 2nd does respond.
            if race == nil or sex == nil then
                race = "";
                sex = 1;
            end
        end

        local player = {};

        player.name = rosterName                                    -- 1
        player.rankName = guildRank;                                -- 2
        player.rankIndex = ( rankOrder - 1 );                       -- 3 (It needs to be 1 less to match when compared to the guildRosterInfo call )
        player.level = level;                                       -- 4
        player.note = note;                                         -- 5
        if GRM.CanViewOfficerNote() then -- Officer Note permission to view.
            player.officerNote = oNote;                             -- 6
        else
            player.officerNote = nil; -- Set Officer note to nil if needed due to player not being able to view. - If it is set to "" then player will think it is changing.
        end
        player.class = classFile;                                   -- 7
        player.lastOnline = 0;                                      -- 8 Time since they last logged in in hours.
        player.zone = zone;                                         -- 9
        player.achievementPoints = pts;                             -- 10
        player.isMobile = false;                                    -- 11
        player.rep = 4;                                             -- 12
        player.isOnline = true;                                     -- 13
        player.status = 0;                                          -- 14
        player.GUID = guid;                                         -- 15
        player.race = race;                                         -- 16
        player.sex = sex;                                           -- 17
        player.rosterSelection = 0;                                 -- 18

        GRM_G.changeHappenedExitScan = true;
        GRM.RecordJoinChanges ( player , GRM.GetClassColorRGB ( classFile , true ) .. GRM.SlimName ( rosterName ) .. "|r" , true , select ( 2 , GRM.GetTimestamp() ) , true );

        -- Check Main Auto tagging...
        GRM.SetGuildInfoDetails();
        -- -- Delay for time to check "Unique Accounts" change...
        C_Timer.After ( 10 , function()               
            if GRM_G.DesignateMain then
                GRM.SetMain ( rosterName , rosterName , false , 0 );
                GRM.Report ( GRM.L ( "GRM Auto-Detect! {name} has joined the guild and will be set as Main" , GRM.GetClassifiedName ( rosterName , true ) ) );
                if GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame:IsVisible() then
                    GRM.RefreshAuditFrames ( true , true );
                end
            end
        end);

    elseif GRM_G.RejoinControlCheck <= 90 then
        GRM_G.RejoinControlCheck = GRM_G.RejoinControlCheck + 1;
        C_Timer.After ( 0.1 , function()
            -- Re-Check 1 time.
            GRM.CheckForLeftOrKickedPlayer( clubID , memberID );
        end);
        return;
    end

    GRM_G.RejoinControlCheck = 0;
end


-- Method:          GRM.GetParsedNameFromInviteAnnouncmenet ( string )
-- What it Does:    Parses out the player name-server from the system message about a player joining the guild
-- Purpose:         Useful for players joining the guild.
GRM.GetParsedNameFromInviteAnnouncmenet = function( text )
    local ind = string.find ( text , "-" );
    if string.find ( string.sub ( text , ind ) , " " ) ~= nil then  -- We know that there is a space at the end of it... Let's parse that off.
        for i = ind , #text do
            if string.sub ( text , i , i ) == " " then
                text = string.sub ( text , 1 , i - 1 );
                break;
            end
        end
        -- text = string.sub ( text , 1 , ind + string.find ( string.sub ( text , ind ) , " " ) - 1 );
    end
    if string.find ( text , " " ) ~= nil then
        for i = ind , 1 , -1 do
            if string.sub ( text , i , i ) == " " then
                text = string.sub ( text , i + 1 );
                break;
            end
        end
    end
    return text;
end

-- Method:          GRM.LiveKickDetection( string )
-- What it Does:    Reports the a player was kicked from the guild and checks if ban was done as well
-- Purpose:         To update the database on-the-fly for changes.
--                  Limited in Classic to only detect your own changes, not others, due to some server limitations and lack of desire to parse text for 11 different languages.
GRM.LiveKickDetection = function( text )
    GRM_G.changeHappenedExitScan = true;
    -- In a function to be called immediately or upon a delay
    local playerThatWasKicked , playerThatKicked = GRM.GetParsedKickPlayerNames ( text );

    if playerThatWasKicked ~= "" and GRM_G.currentName ~= playerThatWasKicked then      -- We don't want to enable the kicking of the player that was kicked as of yet as we will handle that from the secure hook on the kicking
        GRM_G.KickAction ( playerThatWasKicked , playerThatKicked );
    end
end

-- Method:          GRM.BanAndKickingAltsByPlayer ( string )
-- What it Does:    Removes the player, but all ques up alts for kicking, and bans all the alts as well if setting is so checked.
GRM.BanAndKickingAltsByPlayer = function ( playerThatWasKicked )

    if GRM_G.isChecked2 then
        GRM.KickAllAlts ( playerThatWasKicked );
    end
    
    if GRM_G.isChecked then          -- Box is checked, so YES player should be banned. -This boolean is useful because this is a reused Blizz default frame, since protected function.
        -- Popup edit box - BAN logic...
        local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][playerThatWasKicked];
        if player ~= nil then
            player.bannedInfo[1] = true;      -- This officially tags the player as BANNED!
            player.bannedInfo[2] = time();
            player.bannedInfo[3] = false;
            player.bannedInfo[4] = GRM_G.addonUser;
            local result = GRM_UI.GRM_MemberDetailPopupEditBox:GetText();
            if result ~= GRM.L ( "Reason Banned?" ) .. "\n" .. GRM.L ( "Click \"YES\" When Done" ) and result ~= "" and result ~= nil then
                player.reasonBanned = result;
            else
                player.reasonBanned = "";
                result = "";
            end

            -- Add a log message too if it is a ban!
            local logEntryWithTime , logEntry = GRM.GetBanLogString ( GRM_G.isChecked2 , GRM.GetClassifiedName ( GRM_G.addonUser , true ) , GRM.GetClassifiedName ( player.name , true ) , player.reasonBanned , select ( 2 , GRM.GetTimestamp() ) );
            
            GRM.AddLog ( { 17 , logEntryWithTime , GRM_G.isChecked2 , GRM.GetClassifiedName ( GRM_G.addonUser , true ) , GRM.GetClassifiedName ( player.name , true ) , player.reasonBanned , select ( 2 , GRM.GetTimestamp() ) } );

            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].banned then
                GRM.PrintLog ( { 17 , logEntry } );
            end

            -- Send the message out!
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncBanList then
                if result == "" then
                    result = GRM.L ( "None Given" );
                end
                GRMsync.SendMessage ( "GRM_SYNC" , GRM_G.PatchDayString .. "?GRM_BAN?" .. GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank .. "?" .. tostring ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankBanList ) .. "?" .. playerThatWasKicked .. "?" .. tostring ( GRM_G.isChecked2 ) .. "?" .. result .. "?" .. player.class , "GUILD" );
            end
        end
    end
    GRM_G.KickAction ( playerThatWasKicked , GRM_G.addonUser );

    GRM_G.isChecked = false;
    GRM_G.isChecked2 = false;
end

-- Method:          GRM.GetPlayerKickedFromButton ( string )
-- What it Does:    Used for the secure hook and runs when the guild uninvite action is implemented from removing the player and returns the player removed's name
-- Purpose:         Since the guild uninvite is a protected action now, it is still useful to know the name of the player being kicked, especially for perfect matching
--                  when trying to set a ban or kicking all of their alts as well.
-- Extra NoteS:     TY to @Roadblock on Discord for suggesting this and giving a code example!
GRM.GetPlayerKickedFromButton = function( nameOrGUID )
    local playerThatWasKicked , realm = "" , "";

    if string.match ( nameOrGUID , "Player\-.+" ) then -- guid
        playerThatWasKicked, realm = select ( 6 , GetPlayerInfoByGUID ( nameOrGUID ) );

        if realm == "" then
            playerThatWasKicked = GRM.AppendSameServerName ( playerThatWasKicked );
        else
            playerThatWasKicked = playerThatWasKicked .. "-" .. realm;
        end
        
    else -- name
        playerThatWasKicked = GRM.AppendSameServerName ( nameOrGUID );
        
    end

    GRM.BanAndKickingAltsByPlayer ( playerThatWasKicked );
end

-- Method:          GRM.SetName ( string  )
-- What it Does:    Formats the name based on the player settings
-- Purpose:         Reusable function for all future name formatting.
GRM.SetName = function ( name )
    if name ~= nil and name ~= "" then

        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].useFullName then
            name = GRM.AppendSameServerName ( name );
        else
            name = GRM.SlimName ( name );
        end
    end
    return name;
end

-- The leave/kick buttons that are protected
hooksecurefunc ( "GuildUninvite" , GRM.GetPlayerKickedFromButton )
hooksecurefunc ( C_GuildInfo , "RemoveFromGuild" , GRM.GetPlayerKickedFromButton );

-- Method:          GRM_G.KickAction ( string , string )
-- What it Does:    Records the data of who was kicked, who did the kicking, and for their alts if necessary
-- Purpose:         Management of kicking logic.
GRM_G.KickAction = function( kickedToon , kickerOfficer )
    if kickerOfficer ~= nil and #kickerOfficer > 0 and GRM_G.kickSafetyRedundancyCheck ~= kickedToon then
        GRM_G.kickSafetyRedundancyCheck = kickedToon;

        -- Calculate the changesGRM.RecordKickChanges
        local unitName , playerKicked , timePassed , logEntryMetaData , listOfAlts , mainName , publicNote , officerNote , date , isFoundInEventLog , _ , playerLevel , customNote = GRM.RecordKickChanges ( kickedToon , true , select ( 2 , GRM.GetTimestamp() ) , kickerOfficer );

        local logReportWithTime , logReport = GRM.GetLeftOrKickString ( unitName , playerKicked , timePassed , logEntryMetaData , listOfAlts , mainName , publicNote , officerNote , date , isFoundInEventLog , nil , nil , playerLevel , customNote );
        
        -- report the changes!
        if not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].disableMacroToolLogSpam and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].left and not GRM_G.isChecked then
            GRM.PrintLog ( { 10 , logReport } );
        end
        GRM.AddLog ( { 10 , logReportWithTime , unitName , playerKicked , timePassed , logEntryMetaData , listOfAlts , mainName , publicNote , officerNote , date , playerLevel} );

        GRM_G.pause = false;

        -- Referesh frames if visible
        GRM_UI.GRM_MemberDetailMetaData:Hide();
        if kickerOfficer ~= GRM_G.addonUser or ( kickerOfficer == GRM_G.addonUser and GRM_UI.GRM_ToolCoreFrame ~= nil and not GRM_UI.GRM_ToolCoreFrame:IsVisible() and not GRM.isChecked2 ) then
            GRM.RefreshSelectHybridFrames ( true , true , true , true );
        end
        
    end
end

-- Method:          GRM.GetPromotionText ( string )
-- What it Does:    Returns the parsed names of the person who promotes a player, who was promoted, and the rank they were promoted to.
-- Purpose:         Live parsing of the system message so the player does not have to wait for the log to update in retail or the roster to refresh in Classic
--                  Also in Classic there is no log so this is the only way to actually log *who* did what.
GRM.GetPromotionText = function ( text )
    local officer , playerPromoted , newRank = "" , "" , "";
    local pattern = "";

    if GRM_G.Region == "deDE" then
        
        pattern = string.gsub ( ERR_GUILD_PROMOTE_SSS  , "%%1$s" , "(.+)" );
        for i = 2 , 3 do
            pattern = string.gsub ( pattern  , "%%" .. i .. "$s" , "(.+)" );
        end
        officer , playerPromoted , newRank = string.match ( text , pattern );

    elseif GRM_G.Region == "ruRU" then
        pattern = string.gsub ( "%s производит %s в звание \"%s\"."  , "%%s" , "(.+)" );
        officer , playerPromoted , newRank = string.match ( text , pattern );
        playerPromoted = string.sub ( playerPromoted , 6 , #playerPromoted - 1 );
        
    else
        pattern = string.gsub ( ERR_GUILD_PROMOTE_SSS  , "%%s" , "(.+)" );
        officer , playerPromoted , newRank = string.match ( text , pattern );

    end
        
    return GRM.AppendSameServerName ( officer ) , GRM.AppendSameServerName ( playerPromoted ) , newRank;
end

-- Method:          GRM.GetDeomotionText ( string )
-- What it Does:    Returns the parsed names of the person who promotes a player, who was demoted, and the rank they were demoted to.
-- Purpose:         Live parsing of the system message so the player does not have to wait for the log to update in retail or the roster to refresh in Classic
--                  Also in Classic there is no log so this is the only way to actually log *who* did what.
GRM.GetDeomotionText = function ( text )

    local officer , playerDemoted , newRank = "" , "" , "";
    local pattern = "";

    if GRM_G.Region == "deDE" then

        pattern = string.gsub ( ERR_GUILD_DEMOTE_SSS  , "%%1$s" , "(.+)" );
        for i = 2 , 3 do
            pattern = string.gsub ( pattern  , "%%" .. i .. "$s" , "(.+)" );
        end
        officer , playerDemoted , newRank = string.match ( text , pattern );

    elseif GRM_G.Region == "ruRU" then
        pattern = string.gsub ( "%s разжалует %s в звание \"%s\"."  , "%%s" , "(.+)" );
        officer , playerDemoted , newRank = string.match ( text , pattern );
        playerDemoted = string.sub ( playerDemoted , 6 , #playerDemoted - 1 );
        
    else
        pattern = string.gsub ( ERR_GUILD_DEMOTE_SSS  , "%%s" , "(.+)" );
        officer , playerDemoted , newRank = string.match ( text , pattern );

    end

    return GRM.AppendSameServerName ( officer ) , GRM.AppendSameServerName ( playerDemoted ) , newRank;
end

-- Method:          GRM.LivePromoteOrDemoteDetection ( string , bool )
-- What it Does:    Checks live event on promotions and demotions and reports on them and updates the database
-- Purpose:         To instantly log these rank event changes to the database.
--                  Note, due to the lack of a server event log in Classic it will be more limited and only function to instant report if you do the action yourself.
GRM.LivePromoteOrDemoteDetection = function( text , isPromotion )
    GRM_G.changeHappenedExitScan = true;
    -- Controlling the flow of information to either be instant or on delay
    local rankChangeAction = function ( promotedToon , promotingOfficer , newRank )

        if #newRank > 0 then
            local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][promotedToon];

            if player ~= nil then
                GRM.OnRankChange ( player.rankName , newRank , promotedToon , promotingOfficer )
            end
        end

    end

    local officer , playerWhoseRankChanged , newRank = "" , "" , "";
    
    if isPromotion then
        officer , playerWhoseRankChanged , newRank = GRM.GetPromotionText ( text );
    else
        officer , playerWhoseRankChanged , newRank = GRM.GetDeomotionText ( text );
    end

    if officer ~= nil and playerWhoseRankChanged ~= nil and newRank ~= nil then
        rankChangeAction( playerWhoseRankChanged , officer , newRank );
    end

end

-- Method:          GRM.LiveLeaveDetection( string )
-- What it Does:    Catches the system message that the player just left and then updates their details appropriately
-- purpose:         Instantly report when a player is no longer in the guild. Also to use for your own self-detection.
GRM.LiveLeaveDetection = function( text )

    local unitName , playerKicked , timePassed , logEntryMetaData , listOfAlts , mainName , publicNote , officerNote , date , isFoundInEventLog , isNoLongerOnServer , isLiveDetection , playerLevel , customNote = GRM.RecordLeftGuildChanges ( GRM.GetParsedplayerName ( text ) , select ( 2 , GRM.GetTimestamp() ) , true );
    local logReportWithTime , logReport = GRM.GetLeftOrKickString ( unitName , playerKicked , timePassed , logEntryMetaData , listOfAlts , mainName , publicNote , officerNote , date , isFoundInEventLog , isNoLongerOnServer , isLiveDetection , playerLevel , customNote );

    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].left then
        GRM.PrintLog ( { 10 , logReport } );
    end
    GRM.AddLog ( { 10 , logReportWithTime , unitName , playerKicked , timePassed , logEntryMetaData , listOfAlts , mainName , publicNote , officerNote , date } );

    GRM.RefreshSelectHybridFrames ( true , true , true , true );
    
end

-- Method:          GRM.KickPromoteOrJoinPlayer ( object , string , string )
-- What it Does:    Acts as an active event listener and handler for when a player is kicked, left , joined, demoted or promoted in the guild
-- Purpose:         For instantaneous log reporting rather than waiting for the next scan to update the data.
GRM.KickPromoteOrJoinPlayer = function ( _ , msg , text , clubMemberID )
    if msg == "CHAT_MSG_SYSTEM" or msg == "CLUB_MEMBER_ADDED" then

        if GRM_G.gClubID == 0 and GRM_G.BuildVersion >= 80000 then
            GRM_G.gClubID = C_Club.GetGuildClubId();
        end

        if msg == "CLUB_MEMBER_ADDED" then

            GRM_G.changeHappenedExitScan = true;
            QueryGuildEventLog();

            if text == GRM_G.gClubID then
                GRM_G.TempBanSystemMessage = true;
                GRM_G.MainNameSystemMsgControl = true;
                C_Timer.After ( 1 , function()            -- Half a second delay to wait for update (further 3.5 second recursive check loop before given up buried in the next function)
                    GRM_G.TempBanSystemMessage = false;
                    GRM_G.MainNameSystemMsgControl = false;
                    GRM_G.changeHappenedExitScan = true;
                    GRM.CheckForNewPlayer ( text , clubMemberID );
                end);
            end

        else
            -- player rank change
            if string.find ( text , GRM.L ( "has promoted" ) ) ~= nil then
                GRM_G.changeHappenedExitScan = true;
                GRM.GuildRoster();
                GRM.LivePromoteOrDemoteDetection ( text , true );
                
            elseif string.find ( text , GRM.L ( "has demoted" ) ) ~= nil then
                GRM_G.changeHappenedExitScan = true;
                GRM.GuildRoster();
                GRM.LivePromoteOrDemoteDetection ( text , false );

            -- Player joins the guild
            elseif string.find ( text , GRM.L ( "joined the guild." ) ) ~= nil and IsInGuild() then
                if GRM_G.BuildVersion < 80000 then
                    if GRM_G.BuildVersion >= 40000 then -- Cata to Legion
                        QueryGuildEventLog();
                    end
                    GRM_G.changeHappenedExitScan = true;
                    GRM.GuildRoster();

                    if not GRM_G.inCombat and FriendsFrame and not FriendsFrame:IsVisible() then
                        FriendsFrame:Show();
                        FriendsFrameCloseButton:Click();
                    end
                end
        
                GRM_G.TempBanSystemMessage = true;
                GRM_G.MainNameSystemMsgControl = true;
                C_Timer.After ( 1 , function()            -- Half a second delay to wait for update (further 3.5 second recursive check loop before given up buried in the next function)
                    GRM_G.TempBanSystemMessage = false;
                    GRM_G.MainNameSystemMsgControl = false;
                    if GRM_G.BuildVersion < 80000 then
                        local name = GRM.GetParsedJoinPlayerName ( text );
                        if name ~= nil then
                            GRM_G.changeHappenedExitScan = true;
                            GRM.ClassicCheckForNewMember( name );
                        end
                    end
                end);

            -- player left on their own and was not kicked
            elseif string.find ( text , GRM.L ( "left the guild." ) ) ~= nil then
                GRM_G.changeHappenedExitScan = true;
                GRM.GuildRoster();
                GRM.LiveLeaveDetection( text );

                -- Removal from the guild by officer, either yourself or another
            elseif string.find ( text , GRM.L ( "has been kicked" ) ) ~= nil and not GRM_UI.GRM_ToolCoreFrame:IsVisible() then      -- you do not want to trigger this live action when macro tool is being used.
                GRM_G.changeHappenedExitScan = true;
                GRM.GuildRoster();
                GRM.LiveKickDetection ( text );
                               
            end
        end
    end
end

-- Method:          GRM.GetParsedplayerName ( string )
-- What it Does:    Parses out the Left Player message for all 11 clients
-- Purpose:         The Taiwan and Chinese Mandarin need to be parsed differently.
GRM.GetParsedplayerName = function ( text )
    local result = "";

    if GRM_G.Region == "zhCN" then
        result = string.sub ( text , 1 , string.find ( text , "离" ) - 1 );
    elseif GRM_G.Region == "zhTW" then
        result = string.sub ( text , 1 , string.find ( text , "離" ) - 1 );
    else
        result = string.sub ( text , 1 , string.find ( text , " " ) - 1 );
    end

    return GRM.AppendSameServerName ( result );
end

-- Method:          GRM.GetParsedJoinPlayerName ( string )
-- What it Does:    Parses out the joined Player message for all 11 clients
-- Purpose:         To live determine the name of the player that joined the guild
GRM.GetParsedJoinPlayerName = function ( text )
    local pattern = string.gsub ( ERR_GUILD_JOIN_S , "%%s" , "([%%S]+)" );
    local playerJoined = string.match ( text , pattern );

    return GRM.AppendSameServerName ( playerJoined );
end

-- Method:          GRM.GetParsedKickPlayerNames ( string )
-- What it Does:    Returns both the kicked and the kicker names from the parsed system message 
-- Purpose:         To cleanup and efficiently handle LIVE chat reporting when a player is kicked.
GRM.GetParsedKickPlayerNames = function ( text )
    local playerThatWasKicked = "";
    local playerThatKicked = "";

    if GRM_G.Region == "deDE" then
        playerThatWasKicked = string.sub ( text , 1 , string.find ( text , " " ) - 1 );
        local start = select ( 2 , string.find ( text , "wurde von " , 1 , true ) ) + 1;
        playerThatKicked = string.sub ( text , start , start + ( string.find ( string.sub ( text , start ) , " " ) - 1 ) );

    elseif GRM_G.Region == "ptBR" then

        local start = select ( 2 , string.find ( text , "personagem " , 1 , true ) ) + 1;
        playerThatWasKicked = string.sub ( text , start , string.find ( text , " foi" , 1 , true ) - 1 ); 
        start = 0;
        for i = #text , 1 , -1 do
            if string.sub ( text , i , i ) == " " then
                start = i + 1;      -- add 1 because we want the letter immediately after.
                break;
            end
        end
        playerThatKicked = string.sub ( text , start , #text - 1 );

    elseif GRM_G.Region == "koKR" then
        if GRM_G.BuildVersion < 80000 then
            playerThatKicked = string.sub ( text , 1 , string.find ( text , "|1이" , 1 , true ) - 1 );
            local start = string.find ( text , " " ) + 1;
            playerThatWasKicked = string.sub ( text , start , string.find ( text , "|1을" , 1 , true ) - 1 );

        else
            playerThatKicked = string.sub ( text , 1 , string.find ( text , " 님이" , 1 , true ) - 1 );
            local start = select ( 2 , string.find ( text , "님이 " ) ) + 1;
            playerThatWasKicked = string.sub ( text , start , string.find ( text , " 님을" , 1 , true ) - 1 );
        end
        
    elseif GRM_G.Region == "ruRU" then
        playerThatKicked = string.sub ( text , 1 , string.find ( text , " " ) - 1 );
        playerThatWasKicked = string.sub ( text , string.find ( text , "(" , 1 , true ) + 1 , string.find ( text , ")" , 1 ,  true ) - 1 );

    else
        
        local pattern = string.gsub ( ERR_GUILD_REMOVE_SS , "%%s" , "([%%S]+)" );
        playerThatWasKicked, playerThatKicked = string.match ( text , pattern );

    end

    return GRM.AppendSameServerName ( playerThatWasKicked ) , GRM.AppendSameServerName ( playerThatKicked );
end

-- Method:          GRM.RemoveBan( int , boolean , boolean , int )
-- What it Does:    Just what it says... it removes the ban from the player and wipes the data clean. No history of ban is stored
-- Purpose:         Necessary for forgiveness or accidental banning.
GRM.RemoveBan = function ( name , onPopulate , personWhoRemovedIt )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];

    if player ~= nil then
        player.bannedInfo = nil;
        player.bannedInfo = { false , time() , true , personWhoRemovedIt }
        player.reasonBanned = "";

        GRM_UI.GRM_MemberDetailBannedText1:Hide();
        GRM_UI.GRM_MemberDetailBannedIgnoreButton:Hide();

        -- On populate is referring to the check for when it is on mouseover... no need to check this if not.
        if onPopulate and GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame:IsVisible() then
            -- Refresh the frames:
            GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListFrameSelectedNameText:Hide();
            GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListFrameText:SetText ( GRM.L ( "Select a Player" ) );
            if GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons ~= nil then
                for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons do
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[i][1]:UnlockHighlight();
                end
            end
            GRM.RefreshBanListFrames();
        end
    end
end

-- Method:          GRM.UnBanplayer ( string , string )
-- What it Does:    Unbans a listed player in the ban list
-- Purpose:         To be able to control who is banned and not banned in the guild.
GRM.UnBanplayer = function ( name , personWhoRemovedIt )
    local isFound = false;
    local player = GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];

    if player ~= nil then
        isFound = true;
        player.bannedInfo = nil;
        player.bannedInfo = { false , time() , true , personWhoRemovedIt }
        player.reasonBanned = "";
    end
    return isFound;
end

-- Method:          GRM.BanListUnban ( string , string )
-- What it Does:    Unbans the player selected on the core ban list frame upon button push
-- Purpose:         Good to control who is and isn't on the ban list...
GRM.BanListUnban = function ( name , personWhoRemovedIt )
    -- Check the players that left first!
    -- if the player was not found in the left player's list, then we know he is currently in the guild but on ban list!!!
    if not GRM.UnBanplayer ( name , personWhoRemovedIt ) then
        GRM.RemoveBan ( name , false , personWhoRemovedIt );
    end

    -- Refresh the frames:
    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListFrameSelectedNameText:Hide();
    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListFrameText:SetText ( GRM.L ( "Select a Player" ) );
    if GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons ~= nil then
        for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons do
            GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[i][1]:UnlockHighlight();
        end
    end
    GRM.RefreshBanListFrames();
end

-------------------------------
----- UI SCRIPTING LOGIC ------
----- ALL THINGS UX ARE HERE --
-------------------------------

-- Method:          GRM.GRM.PopulateMemberDetails ( string )
-- What it Does:    Builds the details for the core MemberInfoFrame
-- Purpose:         Iterate on each mouseover... Furthermore, this is being kept in "Local" for even the most infinitesimal cost-saving on resources
--                  by not indexing it in a table. Buried in it will be mostly non-compartmentalized logic, few function calls.
GRM.PopulateMemberDetails = function( handle , memberInfo )
    if handle ~= "" and handle ~= nil and GRM_G.guildName ~= "" and IsInGuild() then              -- If the handle is failed to be returned, it is returned as an empty string. Just logic to not populate if on failure.
        GRM_G.rankDateSet = false;        -- resetting tracker

        local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][handle];
        if player ~= nil then
            local fullName , zone , isOnline , _ ;

            if memberInfo then
                if memberInfo.guid ~= nil then
                    zone = memberInfo.zone;
                    if memberInfo.presence ~= 3 then
                        isOnline = true;
                    else
                        isOnline = false;
                    end
                else
                    local found = false;
                    for i = 1 , GRM.GetNumGuildies() do
                        fullName, _, _, _, _, zone, _, _, isOnline = GetGuildRosterInfo ( i );
                        if fullName == handle then
                            found = true;
                            break;
                        end
                    end
                    if not found then
                        zone = nil;
                    end

                end
            elseif GRM_G.RosterSelection ~= 0 then
                zone, _, _, isOnline = select ( 6 , GetGuildRosterInfo ( GRM_G.RosterSelection ) );
            else
                local found = false;
                for i = 1 , GRM.GetNumGuildies() do
                    fullName, _, _, _, _, zone, _, _, isOnline = GetGuildRosterInfo ( i );
                    if fullName == handle then
                        found = true;
                        break;
                    end
                end
                if not found then
                    zone = nil;
                end
            end

            if zone ~= nil and zone ~= "" then
                player.isOnline = isOnline;
                if player.zone ~= zone then
                    player.timeEnteredZone = time();    -- Resets the time
                end
                player.zone = zone;
            end

            --- CLASS
            local classColors = GRM.GetClassColorRGB ( player.class );
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailNameText:SetTextColor ( classColors[1] , classColors[2] , classColors[3] , 1.0 );
            
            -- PLAYER NAME
            -- Let's scale the name too!
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailNameText:SetText ( GRM.SlimName ( handle ) );
            local nameHeight = 16;
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailNameText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + nameHeight );        -- Reset size back to 16 just in case previous fontstring was altered 
            while ( GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailNameText:GetWidth() > 120 ) do
                nameHeight = nameHeight - 0.1;
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailNameText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + nameHeight );
            end

            -- IS MAIN
            if player.isMain then
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMainText:Show();
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailAltText:Hide();
            else
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMainText:Hide();

                if GRM.PlayerIsAnAlt ( player ) then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailAltText:Show();
                else
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailAltText:Hide();
                end
            end

            --- LEVEL
            if GRML.Languages[GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].selectedLang] == "Russian" or GRML.Languages[GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].selectedLang] == "Korean" then
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailLevel:SetText (  tostring ( player.level ) .. GRM.L ( "Level: " ) );
            else
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailLevel:SetText ( GRM.L ( "Level: " ) .. player.level );
            end

            -- RANK
            GRM_G.rankIndex = player.rankIndex;

            -- Rank Text Info...
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankTxt:SetText ( "\"" .. player.rankName .. "\"");
            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankTxt:Show();

            -- ZONE INFORMATION
            if not GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButton:IsVisible() then
                if player.isOnline then
                    if player.zone ~= nil then
                        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoZoneText:SetText ( player.zone );                                     -- Zone
                        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText2:SetText ( GRM.GetTimePassedInZone ( player.timeEnteredZone ) );              -- Time Passed
                    end
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoText:Show();
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoZoneText:Show();
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText1:Show();
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText2:Show();
                else
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoText:Hide();
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoZoneText:Hide();
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText1:Hide();
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailMetaZoneInfoTimeText2:Hide();
                end


                --RANK PROMO DATE
                if player.promoteDateUnknown then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:SetText ( GRM.L ( "Promoted:" ) .. " " .. GRM.L ( "Unknown" ) );
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:Show();
                    GRM_UI.GRM_MemberDetailMetaData.GRM_SetPromoDateButton:Hide();
                    GRM_G.rankDateSet = true;
                else
                    if player.rankHistory[1][3] == 0 then      --- Promotion has never been recorded!
                        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:Hide();
                        GRM_UI.GRM_MemberDetailMetaData.GRM_SetPromoDateButton:Show();
                    else
                        GRM_UI.GRM_MemberDetailMetaData.GRM_SetPromoDateButton:Hide();
                        GRM_G.rankDateSet = true;
                        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:SetText ( GRM.DateUntrustedTag ( player.verifiedPromoteDate ) .. GRM.L ( "Promoted:" ) .. " " .. GRM.FormatTimeStamp ( player.rankHistory[#player.rankHistory][2] , false ) );
                        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailRankDateTxt:Show();
                    end
                end

                -- JOIN DATE
                if player.joinDateUnknown then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailJoinDateButton:Hide();
                    GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:SetText ( GRM.L ( "Unknown" ) );
                    GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:Show();
                else
                    if #player.joinDate == 0 then
                        GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:Hide();
                        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailJoinDateButton:Show();
                    else
                        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailJoinDateButton:Hide();

                        GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:SetText ( GRM.DateUntrustedTag ( player.verifiedJoinDate ) .. GRM.FormatTimeStamp ( player.joinDate[#player.joinDate] , false ) );
                        GRM_UI.GRM_MemberDetailMetaData.GRM_JoinDateText:Show();
                    end
                end

                -- Birthday
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].showBDay then
                    -- Title should always be here
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayTitleText:Show();
                    if player.birthdayUnknown then
                        GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:Hide();
                        GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:SetText ( GRM.L ( "Unknown" ) );
                        GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:Show();
                    else
                        -- Checking button vs text
                        if player.events[2][1][1] ~= 0 then
                            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:Hide();
                            GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:SetText ( GRM.FormatTimeStamp ( player.events[2][3] , false , true ) );
                            GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:Show();
                        else
                            GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:Show();
                            GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:Hide();
                        end
                    end
                else
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayButton:Hide();
                    GRM_UI.GRM_MemberDetailMetaData.GRM_BirthdayText:Hide();
                    GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailBirthdayTitleText:Hide();
                end
            end

            -- PLAYER NOTE AND OFFICER NOTE EDIT BOXES
            if not GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerNoteEditBox:HasFocus() and not GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerOfficerNoteEditBox:HasFocus() and not GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:HasFocus() then
                local finalNote = GRM.L ( "Click here to set a Public Note" );
                local finalONote = GRM.L ( "Click here to set an Officer's Note" );
                GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerNoteEditBox:Hide();
                GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerOfficerNoteEditBox:Hide();

                -- Set Public Note if is One
                if player.note ~= nil and player.note ~= "" then
                    finalNote = player.note;
                end
                GRM_UI.GRM_MemberDetailMetaData.GRM_noteFontString1:SetText ( finalNote );
                if ( CanEditPublicNote() or ( GRM_G.BuildVersion >= 80000 and handle == GRM_G.addonUser ) ) then
                    if finalNote ~= GRM.L ( "Click here to set a Public Note" ) then
                        GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerNoteEditBox:SetText( finalNote );
                    else
                        GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerNoteEditBox:SetText( "" );
                    end
                elseif finalNote == GRM.L ( "Click here to set a Public Note" ) then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_noteFontString1:SetText ( GRM.L ( "Unable to Edit Public Note at Rank" ) );
                end

                -- Set O Note
                if GRM.CanViewOfficerNote() == true then
                    if player.officerNote ~= nil and player.officerNote ~= "" then
                        finalONote = player.officerNote;
                    end
                    if finalONote == GRM.L ( "Click here to set an Officer's Note" ) and GRM.CanEditOfficerNote() ~= true then
                        finalONote = GRM.L ( "Unable to Edit Officer Note at Rank" );
                    end
                    GRM_UI.GRM_MemberDetailMetaData.GRM_noteFontString2:SetText ( finalONote );
                    if finalONote ~= GRM.L ( "Click here to set an Officer's Note" ) then
                        GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerOfficerNoteEditBox:SetText( finalONote );
                    else
                        GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerOfficerNoteEditBox:SetText( "" );
                    end
                else
                    GRM_UI.GRM_MemberDetailMetaData.GRM_noteFontString2:SetText ( GRM.L ( "Unable to View Officer Note at Rank" ) );
                end

                -- Custom Note CheckBox
                if player.customNote[1] then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteSyncMetaCheckBox:SetChecked( true );
                else
                    GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteSyncMetaCheckBox:SetChecked( false );
                end
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncCustomNote then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteSyncMetaCheckBox:Enable();
                    GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteSyncMetaCheckBox.GRM_CustomNoteMetaCheckBoxText:SetTextColor ( 1.0 , 0.82 , 0.0 , 1.0 );
                else
                    -- sync is disabled, so disable the button...
                    GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteSyncMetaCheckBox.GRM_CustomNoteMetaCheckBoxText:SetTextColor ( 0.5 , 0.5 , 0.5 , 1 );
                    GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteSyncMetaCheckBox:Disable();
                end
                -- Set Custom Note details
                GRM.BuildCustomNoteScrollFrame ( player.customNote[6] );
                
                GRM_UI.GRM_MemberDetailMetaData.GRM_noteFontString2:Show();
                GRM_UI.GRM_MemberDetailMetaData.GRM_noteFontString1:Show();

            end

            -- Last Online
            if player.isOnline then
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailLastOnlineTxt:SetText ( GRM.L ( "Online" ) );
            else
                GRM_UI.GRM_MemberDetailMetaData.GRM_MemberDetailLastOnlineTxt:SetText ( GRM.HoursReport ( player.lastOnline ) );
            end

            -- Reputation
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].viewGuildRep and GRM_G.BuildVersion >= 40000 then  -- Cataclysm feature added
                GRM_UI.GRM_MemberDetailMetaData.GRM_ReputationLevelText:SetText ( GRM.GetReputationTextLevel ( player.guildRep , true ) );
                GRM_UI.GRM_MemberDetailMetaData.GRM_ReputationLevelText:Show();
            else
                GRM_UI.GRM_MemberDetailMetaData.GRM_ReputationLevelText:Hide();
            end

            -- Group Invite Button -- Setting script here
            if player.isOnline and handle ~= GRM_G.addonUser and not GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:HasFocus() then
                GRM.SetGroupInviteButton ( handle );
                GRM_UI.GRM_MemberDetailMetaData.GRM_GroupInviteButton:Show();
                GRM_UI.GRM_MemberDetailMetaData.GRM_SafeFromRulesButton:ClearAllPoints();
                if #player.alts == 12 then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_SafeFromRulesButton:SetPoint ( "LEFT" , GRM_UI.GRM_MemberDetailMetaData.GRM_GroupInviteButton , "RIGHT" , 5 , -6 );
                else
                    GRM_UI.GRM_MemberDetailMetaData.GRM_SafeFromRulesButton:SetPoint ( "LEFT" , GRM_UI.GRM_MemberDetailMetaData.GRM_GroupInviteButton , "RIGHT" , 5 , 0 );
                end
                GRM_UI.GRM_MemberDetailMetaData.GRM_SafeFromRulesButton:Show();
            elseif not player.isOnline or handle == GRM_G.addonUser then
                GRM_UI.GRM_MemberDetailMetaData.GRM_GroupInviteButton:Hide();

                if handle ~= GRM_G.addonUser and not GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:HasFocus() then
                    GRM_UI.GRM_MemberDetailMetaData.GRM_SafeFromRulesButton:ClearAllPoints();
                    GRM_UI.GRM_MemberDetailMetaData.GRM_SafeFromRulesButton:SetPoint ( "BOTTOMLEFT" , GRM_UI.GRM_MemberDetailMetaData , "BOTTOMLEFT" , 15 , 11 );
                    GRM_UI.GRM_MemberDetailMetaData.GRM_SafeFromRulesButton:Show();
                else
                    GRM_UI.GRM_MemberDetailMetaData.GRM_SafeFromRulesButton:Hide();
                end
            end

            -- IF PLAYER WAS PREVIOUSLY BANNED AND REJOINED
            -- Player was previous banned and rejoined logic! This will unban the player.
            local isGuildieBanned = player.bannedInfo[1];
            if isGuildieBanned and handle ~= GRM_G.addonUser then
                GRM_UI.GRM_MemberDetailBannedIgnoreButton:SetScript ( "OnClick" , function ( _ , button ) 
                    if button == "LeftButton" then
                        GRM.RemoveBan ( handle , true , GRM_G.addonUser );

                        -- Send the unban out for sync'd players
                        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncBanList then
                            GRMsync.SendMessage ( "GRM_SYNC" , GRM_G.PatchDayString .. "?GRM_UNBAN?" .. GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank .. "?" .. tostring ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankBanList ) .. "?" .. player.name .. "?" , "GUILD");
                        end
                        -- Message
                        local classColorHex = GRM.GetClassColorRGB ( player.class , true )
                        GRM.Report ( GRM.L ( "{name} has been Removed from the Ban List." ,  classColorHex .. player.name .. "|r" ) );
                    end
                end);
                
                GRM_UI.GRM_MemberDetailBannedText1:Show();
                GRM_UI.GRM_MemberDetailBannedIgnoreButton:Show();
            else
                GRM_UI.GRM_MemberDetailBannedText1:Hide();
                GRM_UI.GRM_MemberDetailBannedIgnoreButton:Hide();
            end

            -- ALTS 
            GRM.PopulateAltFrames ( handle );

            -- Font note handling
            GRM_UI.ReconfigureNoteFonts();

        end
    end
end

-- Method:          GRM.ClearAllFrames( boolean )
-- What it Does:    Ensures frames are properly reset upon frame reload...
-- Purpose:         Logic time-saver for minimal costs... why check status of them all when you can just disable and build anew on each reload?
GRM.ClearAllFrames = function( includingMeta )
    if includingMeta then
        if GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
            GRM_UI.GRM_MemberDetailMetaData:Hide();
        end
    end
    GRM_UI.GRM_MemberDetailMetaData.GRM_MonthDropDownMenuSelected:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenuSelected:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenuSelected:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitButton:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitCancelButton:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_NoteCount:Hide();
    GRM_UI.GRM_CoreAltFrame:Hide();
    GRM_UI.GRM_altDropDownOptions:Hide();
    GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_SyncJoinDateSideFrame:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_ConfirmCustomNoteButton:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_CancelCustomNoteButton:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:ClearFocus();
    GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerNoteEditBox:ClearFocus();
    GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerOfficerNoteEditBox:ClearFocus();
    GRM_UI.GRM_OfficerNoteTooltip:Hide();
    GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrameSlider:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteScrollFrameSlider:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_MacroToolIgnoreListSettingsFrame:Hide();
end

-- Method:          GRM.ClearResetFramesOnTabChange()
-- What it Does:    Resets the frames when you are tabbing back and forth
-- Purpose:         Cleaner UI transition experience. Also, no need to keep player name up when the roster screen is gone.
GRM.ClearResetFramesOnTabChange = function()
    if GRM_UI.GRM_MemberDetailMetaData:IsVisible() then
        GRM_UI.GRM_MemberDetailMetaData:Hide() -- this will also trigger to clear all frames GRM.ClearAllFrames()
    end

    if GRM_UI.MemberDetailFrame and GRM_UI.MemberDetailFrame:IsVisible() then
        GRM_UI.MemberDetailFrame:Hide();
    end
end

-- Method:          GRM.SubFrameCheck()
-- What it Does:    Checks the core main frames, if they are open... and hides them
-- Purpose:         Questionable at this time... I might rewrite it with just 4 lines... It serves its purpose now
GRM.SubFrameCheck = function()
    -- wipe the frames...
    if GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitCancelButton:IsVisible() then
        GRM_UI.GRM_MemberDetailMetaData.GRM_DateSubmitCancelButton:Click();
    end
    if GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame:IsVisible() then
        GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame:Hide();
    end
    if GRM_UI.GRM_MemberDetailMetaData.GRM_NoteCount:IsVisible() then
        GRM_UI.GRM_MemberDetailMetaData.GRM_NoteCount:Hide();
    end
    if GRM_UI.GRM_MemberDetailMetaData.GRM_MacroToolIgnoreListSettingsFrame:IsVisible() then
        GRM_UI.GRM_MemberDetailMetaData.GRM_MacroToolIgnoreListSettingsFrame:Hide();
    end
    GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerNoteEditBox:ClearFocus();
    GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerOfficerNoteEditBox:ClearFocus();
    GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame:Hide();
    GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:ClearFocus();
    GRM_UI.GRM_altDropDownOptions:Hide();
end

-- Method:          GRM.SelectPlayerOnRoster ( string )
-- What it Does:    If the guild roster window is open, this will jump to the player anywhere in the roster, online or offline, and bring up their metadata window
-- Purpose:         Useful for when a player wants to click and alt rather than have to scan through the roster for them.
GRM.SelectPlayerOnRoster = function ( playerName )
    if GRM_UI.MemberDetailFrame:IsVisible() then
        GRM_UI.MemberDetailFrame:Hide();
    end

    if GuildFrame and GRM_UI.MemberDetailFrameClassic:IsVisible() then
        GRM_UI.MemberDetailFrameClassic:Hide();
    end
    GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerNoteEditBox:ClearFocus();
    GRM_UI.GRM_MemberDetailMetaData.GRM_PlayerOfficerNoteEditBox:ClearFocus();
    GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:ClearFocus();
    GRM_G.currentName = playerName;
    GRM_G.RosterSelection = GRM.GetRosterSelectionID ( playerName );
    GRM_G.pause = false;
    GRM.ClearAllFrames( false );
    GRM.PopulateMemberDetails ( playerName );
    GRM_G.pause = true;
end

-- Method:          GRM.DateUntrustedTag ( array )
-- What it Does:    Adds a tag in front of a date indicating it is not valid. Returns also the boolean of if it is trusted or not.
-- Purpose:         Notify you that you may need to manually update the date.
GRM.DateUntrustedTag = function( SyncData )
    if SyncData[1] ~= "" and SyncData[1] ~= "1 Jan '01 12:01am" and SyncData[1] ~= ( "1 Jan '01 00:01" .. GRM.L ( "24HR_Notation" ) ) then
        return "";
    else
        return "|cffff0000!!|r";
    end
end

-------------------------------
-- BANNING LOGIC AND METHODS --
-------------------------------

-- Method:          GRM.GetPlayerWhoBanned ( string , boolean )
-- What it Does:    Returns the name of the player who banned the given toon, if they are banned at least.
-- Purpose:         Mostly for use in the tooltip on the mouseover of the ban buttons to say who banned the player.
GRM.GetPlayerWhoBanned = function ( name , isInGuild )
    local result = "";
    local player;

    if isInGuild then
        player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];
    else
        player = GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];
    end

    if player ~= nil then
        if player.bannedInfo[1] and player.bannedInfo[4] ~= "" then  -- No point in stating who banned them if they don't currently have a ban
            result = player.bannedInfo[4];
        end
    end

    return result;
end

-- Method:          GRM.GetBannedPlayersWithGUIDs()
-- What it Does:    Compiles a list of all players that are banned with GUIDs into a 2d array
-- Purpose:         For use on easy re-checking of players' GUID to see if the banned player is no longer on the server
GRM.GetBannedPlayersWithGUIDs = function()
    local result = {};
    local databases = { GRM_GuildMemberHistory_Save , GRM_PlayersThatLeftHistory_Save };

    -- Add bans of people still in the guild
    local guildData;
    for i = 1 , #databases do
        guildData = databases[i][ GRM_G.F ][ GRM_G.guildName ];
        for _ , player in pairs ( guildData ) do
            if type ( player ) == "table" then

                if player.bannedInfo[1] and player.GUID ~= nil and player.GUID ~= "" then
                    table.insert ( result , { player.name , player.GUID } );
                end
            end
        end
    end

    return result;
end

-- Method:          GRM.GetBannedPlayersStillInGuild()
-- What it Does:    Returns a list of players currently in the guild that are banned, with their accompanying metadata.
-- Purpose:         Collect more than just the count, but get the player metadata too
GRM.GetBannedPlayersStillInGuild = function()
    local playerDetails = {};
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];

    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            if player.bannedInfo[1] then
                table.insert ( playerDetails  , { player.name , player.class , player.bannedInfo[2] , player.rankName , player.rankIndex , player.reasonBanned , true , player.isUnknown , player.GUID } );
            end
        end
    end
    return playerDetails;
end

-- Method:          GRM.SyncRemoveCurrentPlayerBan ( string , int )
-- What it Does:    Removes a current ban during sync, thus avoids the chat spam.
-- Purpose:         Ban mechanism for during retroactive sync.
GRM.SyncRemoveCurrentPlayerBan = function ( name , timestamp , unbanner )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];

    if player ~= nil then
        if timestamp > player.bannedInfo[2] then
            player.bannedInfo = nil;
            player.bannedInfo = { false , timestamp , true , unbanner }
            player.reasonBanned = "";
            
            GRM_UI.GRM_MemberDetailBannedText1:Hide();
            GRM_UI.GRM_MemberDetailBannedIgnoreButton:Hide();
        end
    end
end

-- Method:          GRM.SyncAddCurrentPlayerBan ( string , int , string , string )
-- What it Does:    Adds a ban during sync with the reason
-- Purpose:         Ban mechanism for during retroactive sync.
GRM.SyncAddCurrentPlayerBan = function ( name , timestamp , reason , banner )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];

    if player ~= nil then
        if timestamp > player.bannedInfo[2] then
            player.bannedInfo = nil;
            player.bannedInfo = { true , timestamp , false , banner }
            player.reasonBanned = reason;
        end
    end
end

-- Method:          GRM.ChangeCurrentPlayerBanReason ( string , string , string )
-- What it Does:    Edits the current ban reason with a new one
-- Purpose:         Mechanism for editing the reason of a ban
GRM.ChangeCurrentPlayerBanReason = function ( name , reason , banner )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];

    if player ~= nil then
        if not banner then
            banner = "";
        end
        player.bannedInfo[4] = banner;
        player.reasonBanned = reason;
    end
end

-- Method:          GRM.GetNumberOfPlayerSBannedCurrentlyInGuild ( int )
-- What it Does:    Returns the number of players that are currently banned but are still in the guild
-- Purpose:         Useful on the OnUpdate Ban List tab to inform the player better if there are currently players in the guild that need to be banned.
GRM.GetNumberOfPlayerSBannedCurrentlyInGuild = function()
    local result = 0;
    local names = {};
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];

    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            if player.bannedInfo[1] then
                result = result + 1;
                table.insert ( names , { player.name , GRM.GetClassColorRGB ( player.class ) , player.lastOnline } );
            end
        end
    end

    if result == 0 then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_BanListRemoveAllCurrentButton:Hide();
    elseif GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame:IsVisible() and not GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_BanListRemoveAllCurrentButton:IsVisible() then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_BanListRemoveAllCurrentButton:Show();
    end
        
    return result , names;
end

-- Method:          GRM.GetSortedBanListNamesWithDetails ()
-- What it Does:    Returns the list of all banned players based on the given sorting settings
-- Purpose:         Give the player to sort the ban list by the headers.
GRM.GetSortedBanListNamesWithDetails = function ()
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    local finalList = {};
    local format = GRM_G.banDetailsControl[1];
    local descending = GRM_G.banDetailsControl[2];
    local playerDetails = {};
    local isAdded = false
    local count = 0;

    -- Add bans of people still in the guild
    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then
            if player.bannedInfo[1] then
                
                table.insert ( playerDetails  , { player.name , player.class , player.bannedInfo[2] , player.rankName , player.rankIndex , player.reasonBanned , true , player.isUnknown , player.GUID } );
                count = count + 1;
            end
        end
    end

    guildData = GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    
    local start = #playerDetails + 1;
    local insertIndex = 1;

    -- Add bans of people not in the guild - adds them in order of when the most recent ban was.
    for _ , player in pairs ( guildData ) do
        if type ( player ) == "table" then

            if player.bannedInfo[1] then
                insertIndex = 1;

                local isUnknown = false;
                if player.GUID == "" then
                    isUnknown = true;
                end

                if #playerDetails == 0 then
                    insertIndex = 1;
                else
                    for i = start , #playerDetails do
                        if player.bannedInfo[2] >= playerDetails[i][3] then
                            insertIndex = i;
                            break;
                        end

                        if i == #playerDetails and insertIndex == 1 then
                            insertIndex = i + 1;
                        end
                    end
                end

                table.insert ( playerDetails  , insertIndex , { player.name , player.class , player.bannedInfo[2] , player.rankName , player.rankIndex , player.reasonBanned , false , isUnknown , player.GUID } );
                count = count + 1;
            end
        end
    end
        
    -- By Name
    if format == 2 then

        if descending then
            sort ( playerDetails , function ( a , b ) return a[1] < b[1] end );     -- Sorts complex table with first index alphabetically with score ranking
        else
            sort ( playerDetails , function ( a , b ) return b[1] < a[1] end );     -- Sorts complex table with first index alphabetically with score ranking
        end
        finalList = playerDetails;
    
    -- By Rank
    elseif format == 3 then

        for i = 1 , #playerDetails do
            if #finalList == 0 then
                table.insert ( finalList , playerDetails[i] );
            else
                -- Now, need to scan through the list to see what we have
                isAdded = false;
                for j = 1 , #finalList do
                    if descending then
                        if playerDetails[i][5] < finalList[j][5] then                  -- if rank index is lower, it is higher in the guild as index 0 = Guild Leader
                            table.insert ( finalList , j , playerDetails[i] );
                            isAdded = true;
                            break;
                        end
                    else
                        if playerDetails[i][5] > finalList[j][5] then
                            table.insert ( finalList , j , playerDetails[i] );
                            isAdded = true;
                            break;
                        end
                    end
                end
                if not isAdded then
                    table.insert ( finalList , playerDetails[i] ); -- Just add it to the end
                end
            end
        end

    -- By Date
    elseif format == 4 then

        for i = 1 , #playerDetails do

            if #finalList == 0 then
                table.insert ( finalList , playerDetails[i] );
            else
                -- Now, need to scan through the list to see what we have
                
                isAdded = false;
                for j = 1 , #finalList do
                    if descending then
                        if playerDetails[i][3] >= finalList[j][3] then                   -- Larger epoch stamps means more recent...
                            table.insert ( finalList , j , playerDetails[i] );
                            isAdded = true;
                            break;
                        end
                    else
                        if playerDetails[i][3] < finalList[j][3] then
                            table.insert ( finalList , j , playerDetails[i] );
                            isAdded = true;
                            break;
                        end
                    end
                end
                if not isAdded then
                    table.insert ( finalList , playerDetails[i] ); -- Just add it to the end
                end
            end
        end
    end
    
    return finalList , count;
end

-- Method:          GRM.IsBanNameOnList ( name , array )
-- What it Does:    Returns true if the given name is on the list
-- Purpose:         To identify a quick lookup if the name is on the list for purposes of updating the ban list to show player has left the server
GRM.IsBanNameOnList = function ( name , list )
    local result = false;

    for i = 1 , #list do
        if name == list[i] then
            result = true;
            break;
        end
    end

    return result;
end

-- Method:          GRM.RefreshBanListFrames()
-- What it Does:    On loading the Ban List frames, it populates and prepares them for a scrollable window if necessary
-- purpose:         Quality of Life. Whilst the ban list is managed automatically behind the scenes, it is useful to have common information that syncs between users
--                  with the guild.
GRM.RefreshBanListFrames = function( listNeedingUpdate )
    -- SCRIPT LOGIC ON ADD EVENT SCROLLING FRAME
    local scrollHeight = 0;
    local scrollWidth = 561;
    local buffer = 20;

    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons or {};  -- Create a table for the Buttons.

    -- populating the window correctly.
    local tempHeight = 0;
    -- { tempGuild[i][1] , tempGuild[i][9] , tempGuild[i][17][2] , tempGuild[i][4] , tempGuild[i][5] , tempGuild[i][18] , isInGuild } );
    local banList , count = GRM.GetSortedBanListNamesWithDetails();

    -- Populating the window based on the Current Players PLayers
    for i = 1 , #banList do
        -- if font string is not created, do so.
        if not GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[i] then
            local tempButton = CreateFrame ( "Button" , "BannedPlayer" .. i , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame ); -- Names each Button 1 increment up
            table.insert ( GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons , { tempButton , tempButton:CreateFontString ( "BannedPlayerNameText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) , tempButton:CreateFontString ( "BannedPlayerRankText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) , tempButton:CreateFontString ( "BannedPlayerDateText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) , tempButton:CreateFontString ( "BannedPlayerReasonText" .. i , "OVERLAY" , "GameFontWhiteTiny" ) } );
        end

        local BanButtons = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[i][1];
        local BanNameText = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[i][2];
        local BanRankText = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[i][3];
        local BanDateText = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[i][4];
        local BanReasonText = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[i][5];
        local classColor = GRM.GetClassColorRGB ( banList[i][2] );
        local nameDetails = banList[i][1];
        if banList[i][8] then
            -- Player was unable to be identified on the server
            nameDetails = nameDetails .. " |CFFFF0000" .. GRM.L ( "(Unable to Identify)" );
        elseif listNeedingUpdate and GRM.IsBanNameOnList ( nameDetails , listNeedingUpdate ) then
            nameDetails = nameDetails .. " |CFFFF0000(" .. GRM.L ( "Player no longer on Server" ) .. ")";
        end

        BanButtons:SetWidth ( 555 );
        BanButtons:SetHeight ( 19 );
        BanButtons:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
        if banList[i][7] then
            BanNameText:SetText ( GRM.L ( "{name}(Still in Guild)" , banList[i][1] .. "  |cff7fff00" ) );
        else
            BanNameText:SetText ( nameDetails );
        end
        BanButtons:RegisterForDrag ( "LeftButton" );
        BanButtons:SetScript ( "OnDragStart" , function()
            GRM_UI.GRM_RosterChangeLogFrame:StartMoving();
        end);
        BanButtons:SetScript ( "OnDragStop" , function()
            GRM_UI.GRM_RosterChangeLogFrame:StopMovingOrSizing();
            GRM_UI.SaveCorePosition();
        end);

        BanNameText:SetTextColor ( classColor[1] , classColor[2] , classColor[3] , 1 );
        BanNameText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 12 );
        BanNameText:SetJustifyH ( "LEFT" );
        BanRankText:SetText ( banList[i][4] );
        BanRankText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 12 );
        BanRankText:SetJustifyH ( "CENTER" );
        BanRankText:SetWidth ( 100 );
        BanRankText:SetTextColor ( 0.90 , 0.80 , 0.50 , 1.0 );
        BanDateText:SetText ( GRM.FormatTimeStamp ( GRM.EpochToDateFormat ( banList[i][3] ) , false ) );
        BanDateText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 12 );
        BanDateText:SetJustifyH ( "CENTER" );
        BanDateText:SetWidth ( 100 );
        -- Determine it's not an empty ban reason!
        local reason = "";
        if banList[i][6] == "" or banList[i][6] == nil then
            reason = GRM.L ( "No Ban Reason Given" );
        else
            reason = banList[i][6];
        end
        BanReasonText:SetText ( "|CFFFF0000" .. GRM.L ( "Reason:" ) .. " |CFFFFFFFF" .. reason );
        BanReasonText:SetWidth ( 245 );
        BanReasonText:SetWordWrap ( true );
        BanReasonText:SetSpacing ( 1 );
        BanReasonText:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 12 );
        BanReasonText:SetPoint ( "TOPLEFT" , BanButtons , "BOTTOMLEFT" , 0 , -1);
        BanReasonText:SetJustifyH ( "LEFT" );

        -- Logic
        BanButtons:SetScript ( "OnClick" , function ( self , button )
            if button == "LeftButton" then

                local fullName = BanNameText:GetText();
                local R,G,B = BanNameText:GetTextColor();
                local stillInGuild = false;
                
                if string.find ( fullName , GRM.L ( "(Unable to Identify)" ) , 1 , true ) ~= nil then
                    fullName = string.sub ( fullName , 1 , string.find ( fullName , " " ) - 1 );
                elseif string.find ( fullName , GRM.L ( "(Still in Guild)" ) , 1 , true ) ~= nil then
                    fullName = string.sub ( fullName , 1 , string.find ( fullName , " " ) - 1 );
                    stillInGuild = true;
                end
                
                if IsShiftKeyDown() and IsControlKeyDown() then
                    GRM.RestoreTooltip();
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogTab:Click();
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogEditBox:SetText( GRM.SlimName ( fullName ) );
                elseif IsControlKeyDown() then
                    if stillInGuild then
                        GRM.OpenPlayerWindow( fullName );
                    end
                else

                    -- For highlighting purposes
                    for j = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons do
                        if self ~= GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[j][1] then
                            GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[j][1]:UnlockHighlight();
                        else
                            GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[j][1]:LockHighlight();
                        end
                    end
                    
                    GRM_G.TempBanTarget = { fullName , { GRM.ConvertRGBScale ( R , true ) , GRM.ConvertRGBScale ( G , true ) , GRM.ConvertRGBScale ( B , true ) } }; -- Need to parse out the "(Still in Guild)"
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListFrameSelectedNameText:SetText ( GRM.SlimName ( fullName ) );
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListFrameText:SetText ( GRM.L ( "Player Selected" ) );
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListFrameSelectedNameText:Show();
                end
            end
        end);
        
        -- Now let's pin it!
        
        if i == 1 then
            BanButtons:SetPoint( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame , "TOPLEFT" , 5 , -12 );
            BanNameText:SetPoint ( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame , "TOPLEFT" , 5 , -12 );
            BanRankText:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame , "TOP" , 64 , -12 );
            BanDateText:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame , "TOP" , 211 , -12 );
            scrollHeight = scrollHeight + BanButtons:GetHeight() + BanReasonText:GetHeight();
        else
            BanButtons:SetPoint( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[i - 1][5] , "BOTTOMLEFT" , 0 , - buffer );
            BanNameText:SetPoint ( "TOPLEFT" , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[i - 1][5] , "BOTTOMLEFT" , 0 , - buffer );
            BanRankText:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[i - 1][3] , "BOTTOM" , 0 , - ( tempHeight + buffer ) );
            BanDateText:SetPoint ( "TOP" , GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[i - 1][4] , "BOTTOM" , 0 , - ( tempHeight + buffer ) );
            scrollHeight = scrollHeight + BanButtons:GetHeight() + BanReasonText:GetHeight() + buffer;
        end
        BanButtons:Show();
        tempHeight = BanReasonText:GetHeight() + ( BanButtons:GetHeight() - BanNameText:GetHeight() ) + 1;
    end

    -- Ok, let's add a count to how many banned
    if count > 0 then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListFrameNumBannedText:SetText( "(" .. GRM.L ( "Total Banned:" ) .. " " .. count .. ")" );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListFrameNumBannedText:Show();
        GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListFrameText:Show();
        GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListFrameAllOfflineText:Hide();
    else
        GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListFrameNumBannedText:Hide();
        GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListFrameText:Hide();
        GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListFrameAllOfflineText:Show();
    end

    -- Hides all the additional buttons... if necessary ( necessary because once initialized, the buttons are there. This avoids bloated code and too much purging and rebuilding and purging. Just hide for future use.
    for i = count + 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[i][1]:Hide();
    end

    --Set Button Logic
    for i = 1 , #GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons do
        GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[i][1]:SetScript ( "OnEnter" , function ( self )
            local parsedNumber = tonumber ( string.match ( self:GetName() , "(%d+)" ) );
            
            local playerName = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[parsedNumber][2]:GetText();
            local playerWhoBanned = "";
            local stillInGuild = false;

            if string.find ( playerName , " " ) ~= nil then

                local inGuild = true;
                if string.find ( playerName , GRM.L ( "(Unable to Identify)" ) , 1 , true ) ~= nil then
                    inGuild = false; 
                end

                playerName = GRM.Trim ( string.sub ( playerName , 1 , string.find ( playerName , " " ) - 1 ) );
                playerWhoBanned = GRM.GetPlayerWhoBanned ( playerName , inGuild );

                if inGuild then
                    stillInGuild = true;
                end
            else
                playerWhoBanned = GRM.GetPlayerWhoBanned ( playerName , false );
            end

            if playerWhoBanned == "" then
                playerWhoBanned = GRM.L ( "Unknown" );
            else
                playerWhoBanned = GRM.GetClassifiedName ( playerWhoBanned , true );
            end
            
            GRM_UI.SetTooltipScale();
            GameTooltip:SetOwner ( self , "ANCHOR_CURSOR" );
            GameTooltip:AddLine ( GRM.L ( "Player Was Banned By: {name}" , playerWhoBanned ) );
            if stillInGuild then
                GameTooltip:AddLine ( GRM.L ( "|CFFE6CC7FCtrl-Click|r to open Player Window" ) );
            end
            GameTooltip:AddLine ( GRM.L ( "|CFFE6CC7FCtrl-Shift-Click|r to Search the Log for Player" ) );
            GameTooltip:Show();
        end);

        GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame.allFrameButtons[i][1]:SetScript ( "OnLeave" , function ()
            GRM.RestoreTooltip();
        end);

    end

    GRM_UI.UpdateBanTabCurrentlyInGuild();
    
    -- Update the size -- it either grows or it shrinks!
    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollChildFrame:SetSize ( scrollWidth , scrollHeight );

    --Set Slider Parameters ( has to be done after the above details are placed )
    local scrollMax = ( scrollHeight - 353 ) + ( buffer * .5 ) + tempHeight;
    if scrollMax < 0 then
        scrollMax = 0;
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollFrameSlider:SetMinMaxValues ( 0 , scrollMax );
    -- Mousewheel Scrolling Logic
    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollFrame:EnableMouseWheel( true );
    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollFrame:SetScript( "OnMouseWheel" , function( _ , delta )
        local current = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollFrameSlider:GetValue();
        
        if IsShiftKeyDown() and delta > 0 then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollFrameSlider:SetValue ( 0 );
        elseif IsShiftKeyDown() and delta < 0 then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollFrameSlider:SetValue ( scrollMax );
        elseif delta < 0 and current < scrollMax then
            if IsControlKeyDown() then
                GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollFrameSlider:SetValue ( current + 60 );
            else
                GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollFrameSlider:SetValue ( current + 20 );
            end
        elseif delta > 0 and current > 1 then
            if IsControlKeyDown() then
                GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollFrameSlider:SetValue ( current - 60 );
            else
                GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_CoreBanListScrollFrameSlider:SetValue ( current - 20 );
            end
        end
    end);

    -- GUID check to see if any of them are no longer on the server...
    if not listNeedingUpdate then
        local names = GRM.GetBannedPlayersWithGUIDs();
        GRM.SetplayersStillOnServer ( names , true );

        C_Timer.After ( 1.5 , function()
            local listNeedingUpdate = GRM.ValidateBanGUIDs();

            if #listNeedingUpdate > 0 then
                GRM.RefreshBanListFrames( listNeedingUpdate );
            end
        end);
    end
end

-- Method:          GRM.RefreshSelectHybridFrames ( bool , bool , bool , bool )
-- What it Does:    Checks if these frames are visible and if so, refreshes their details
-- Purpose:         If a value changes in the roster, like a player leaves or joins the guild, or even is promoted, demoted, you want these values to be instantly visible and reset
--                  Be warned, calling this can be extremely computationally expensive. Never place it in a loop to update on each value changed. Only place it at the end of the 
--                  sequence of actions to update everything all at once.
GRM.RefreshSelectHybridFrames = function ( log , audit , ban , macroTool )
    if log and GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame:IsVisible() then
        GRM.BuildLogComplete( true , true );
    end

    --Refresh Audit Frames
    if audit and GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame:IsVisible() then
        GRM.RefreshAuditFrames ( true , true );
    end

    -- Refresh Ban List Frames
    if ban and GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame:IsVisible() then
        GRM.RefreshBanListFrames();
    end

    --Refresh Macro Tool Frames
    if macroTool and GRM_UI.GRM_ToolCoreFrame ~= nil and GRM_UI.GRM_ToolCoreFrame:IsVisible() then
        GRM_UI.RefreshManagementTool();
    end
end


--- FINALLY!!!!!
--- TOOLTIPS ---
----------------

-- Method:          GRM.RestoreTooltip()
-- What it Does:    Restores the previous settings of the tooltip so GRM's own settings do not overwrite elsewhere
-- Purpose:         Maintain compatibility, and also add this all into a single function call.
GRM.RestoreTooltip = function()
    GRM_UI.RestoreTooltipScale();
    GameTooltip:Hide();
end

-- Method:          GRM.GetRankHistory ( string , string )
-- What it Does:    Returns the give players promotion date history
-- Purpose:         So export can include a promotion date history
GRM.GetRankHistory = function ( player , delimiter )
    local history = "";
    local separator = ",";
    local tFormat = "";

    if delimiter == "," then
        separator = ";";
    end

    if player.rankHistory[1][3] > 0 then

        local count = 0;
        for k = #player.rankHistory , 1 , -1 do

            tFormat = player.rankHistory[k][2];
            
            -- If on the last set...
            if k == #player.rankHistory then
                if player.verifiedPromoteDate[1] ~= "" then
                    tFormat = player.verifiedPromoteDate[1];                                     -- Change it to different format if necessary.
                end
            end

            if #history > 0 then
                history = history .. separator;
            end
            history = history .. ( string.gsub ( player.rankHistory[k][1] , "Left Guild" , GRM.L ( "Left Guild" ) ) .. ": " ..  GRM.FormatTimeStamp ( tFormat , false ) );

            count = count + 1;
            if count > 10 then
                break;
            end
        end

    end

    return history;
end

-- Method:          GRM.PromoRankTooltip ( frame , table )
-- What it Does:    Processes the logic for this specific tooltip in the player rank window and populates player rank promotion history
-- Purpose:         Keep code clean and management and compartmentalized on the mouseover tooltip window.
GRM.PromoRankTooltip = function( self , guildData )
    if GetMouseFocus() == self and self.GRM_MemberDetailRankToolTip:IsVisible() ~= true and not StaticPopup1:IsVisible() and not DropDownList1:IsVisible() and self.GRM_MemberDetailRankDateTxt:IsVisible() == true and GRM_UI.GRM_altDropDownOptions:IsVisible() ~= true and self.GRM_MemberDetailRankDateTxt:IsMouseOver(1,-1,-1,1) == true then
            
        self.GRM_MemberDetailRankToolTip:SetOwner( self.GRM_MemberDetailRankDateTxt , "ANCHOR_BOTTOMRIGHT" );
        self.GRM_MemberDetailRankToolTip:AddLine( "|cFFFFFFFF" .. GRM.L ( "Rank History" ) );
        
        local player = guildData[ GRM_G.currentName ];
        if player ~= nil then
            -- Now, let's build the tooltip
            if self.GRM_MemberDetailRankDateTxt:GetText() == GRM.L ( "Promoted:" ) .. " " .. GRM.L ( "Unknown" ) then
                self.GRM_MemberDetailRankToolTip:AddDoubleLine ( "|cFFFF0000" .. GRM.L ( "Time at Rank:" ) , GRM.L ( "Unknown" ) );
                self.GRM_MemberDetailRankToolTip:AddDoubleLine ( " " , " " );
            else
                if player.rankHistory[1][3] > 0 then
                    for k = #player.rankHistory , 1 , -1 do
                        local tFormat = player.rankHistory[k][2];
                        -- If on the last set...
                        if k == #player.rankHistory then
                            if player.verifiedPromoteDate[1] ~= "" then
                                tFormat = player.verifiedPromoteDate[1];                                     -- Change it to different format if necessary.
                            end
                            local timeAtRank = GRM.GetTimePassedUsingStringStamp ( player.rankHistory[#player.rankHistory][2] );
                            self.GRM_MemberDetailRankToolTip:AddDoubleLine ( "|cFFFF0000" .. GRM.L ( "Time at Rank:" ) , timeAtRank[4] );
                            self.GRM_MemberDetailRankToolTip:AddDoubleLine ( " " , " " );

                            if string.find ( self.GRM_MemberDetailRankDateTxt:GetText() , "!!" , 1 , true ) ~= nil then
                                self.GRM_MemberDetailRankToolTip:AddLine ( GRM.L ( "The {name} tag indicates a date must be verified to sync" , "|CFFFF0000!!|r" ) );
                                self.GRM_MemberDetailRankToolTip:AddLine ( GRM.L ( "To confirm or edit the date, right click the date, edit, and submit" ) , 1 , 0.84 , 0 , true );
                                self.GRM_MemberDetailRankToolTip:AddLine ( " " );
                                self.GRM_MemberDetailRankToolTip:AddLine ( GRM.L ( "If the date is accurate, right click and select 'Confirm Date'" ) , 1 , 0.84 , 0 , true );
                                self.GRM_MemberDetailRankToolTip:AddDoubleLine ( " " , " " );
                            end

                        end
                        self.GRM_MemberDetailRankToolTip:AddDoubleLine(  string.gsub ( player.rankHistory[k][1] , "Left Guild" , GRM.L ( "Left Guild" ) ) .. ":" , GRM.FormatTimeStamp ( tFormat , false ) , 0.38 , 0.67 , 1.0 );
                    end
                end
            end
            self.GRM_MemberDetailRankToolTip:AddLine ( " " );
            self.GRM_MemberDetailRankToolTip:AddLine ( GRM.L ( "Right-Click to Edit" ) );
        end

        self.GRM_MemberDetailRankToolTip:Show();
    elseif GetMouseFocus() ~= self or ( self.GRM_MemberDetailRankToolTip:IsVisible() == true and self.GRM_MemberDetailRankDateTxt:IsMouseOver(1,-1,-1,1) ~= true ) then
        self.GRM_MemberDetailRankToolTip:Hide();
        self.GRM_MemberDetailServerNameToolTip:Hide();
    end
end

-- Method:          GRM.JoinDateTooltip ( frame , table )
-- What it Does:    Builds the tooltip for when mousing over the guild join date history
-- Purpose:         Keep code clean and compartmentalized
GRM.JoinDateTooltip = function ( self , guildData )
    if GetMouseFocus() == self and self.GRM_MemberDetailJoinDateToolTip:IsVisible() ~= true and not StaticPopup1:IsVisible() and self.GRM_JoinDateText:IsVisible() == true and GRM_UI.GRM_altDropDownOptions:IsVisible() ~= true and self.GRM_JoinDateText:IsMouseOver(1,-1,-1,1) == true then
           
        self.GRM_MemberDetailJoinDateToolTip:SetOwner( self.GRM_JoinDateText , "ANCHOR_BOTTOMRIGHT" );
        self.GRM_MemberDetailJoinDateToolTip:AddLine( "|cFFFFFFFF" .. GRM.L ( "Membership History" ) );
        local joinedHeader;

        local player = guildData[GRM_G.currentName];
        if player ~= nil then
            -- Ok, let's build the tooltip now.
            if self.GRM_JoinDateText:GetText() == GRM.L ( "Unknown" ) then
                self.GRM_MemberDetailJoinDateToolTip:AddDoubleLine ( GRM.L ( "Joined:" ) , GRM.L ( "Unknown" ) );
                self.GRM_MemberDetailJoinDateToolTip:AddDoubleLine ( " " , " " );
            else
                for r = #player.joinDate , 1 , -1 do                                       -- Starting with most recent join which will be at end of array.
                    if r == #player.joinDate then
                        self.GRM_MemberDetailJoinDateToolTip:AddDoubleLine ( "|cFFFF0000" .. GRM.L ( "Time as Member:" ) , GRM.GetTimePlayerHasBeenMember ( player.name ) );

                        if #player.joinDate > 0 then
                            local total = GRM.L ( "Times in Guild: {num}" , nil , nil , #player.joinDate );
            
                            if GRM.IsGuildEE ( GRM_G.guildName ) then
                                if GRM.IsPlayerEE ( GRM_G.currentName ) then
                                    total = GRM_G.listOfPlayers[GRM_G.currentName] .. ": " .. #player.joinDate;
                                else
                                    total = GRM_G.listOfGuilds[GRM_G.guildName] .. ": " .. #player.joinDate;
                                end
                            end
            
                            self.GRM_MemberDetailJoinDateToolTip:AddLine( total , 1 , 0 , 0 );
                        end

                        self.GRM_MemberDetailJoinDateToolTip:AddDoubleLine ( " " , " " );

                        if string.find ( self.GRM_JoinDateText:GetText() , "!!" , 1 , true ) ~= nil then
                            self.GRM_MemberDetailJoinDateToolTip:AddLine ( GRM.L ( "The {name} tag indicates a date must be verified to sync" , "|CFFFF0000!!|r" ) );
                            self.GRM_MemberDetailJoinDateToolTip:AddLine ( GRM.L ( "To confirm or edit the date, right click the date, edit, and submit" ) , 1 , 0.84 , 0 , true );
                            self.GRM_MemberDetailJoinDateToolTip:AddLine ( " " );
                            self.GRM_MemberDetailJoinDateToolTip:AddLine ( GRM.L ( "If the date is accurate, right click and select 'Confirm Date'" ) , 1 , 0.84 , 0 , true );
                            self.GRM_MemberDetailJoinDateToolTip:AddDoubleLine ( " " , " " );
                        end
                    end
                    if r > 1 then
                        joinedHeader = GRM.L ( "Rejoined:" );
                    else
                        joinedHeader = GRM.L ( "Joined:" );
                    end
                    if player.leftGuildDate[r] ~= nil then
                        self.GRM_MemberDetailJoinDateToolTip:AddDoubleLine( "|CFFC41F3B" .. GRM.L ( "Left:" ) ,  GRM.FormatTimeStamp ( player.leftGuildDate[r] , false ) , 1 , 0 , 0 );
                    end
                    self.GRM_MemberDetailJoinDateToolTip:AddDoubleLine( joinedHeader , GRM.FormatTimeStamp ( player.joinDate[r] , false ) , 0.38 , 0.67 , 1.0 );
                    -- If player once left, then this will add the line for it.
                end
            end
            self.GRM_MemberDetailJoinDateToolTip:AddLine ( " " );
            self.GRM_MemberDetailJoinDateToolTip:AddLine ( GRM.L ( "Right-Click to Edit" ) );
        end
        self.GRM_MemberDetailJoinDateToolTip:Show();
    elseif GetMouseFocus() ~= self or ( self.GRM_JoinDateText:IsMouseOver(1,-1,-1,1) ~= true and ( self.GRM_MemberDetailJoinDateToolTip:IsVisible() or self.GRM_MemberDetailServerNameToolTip:IsVisible() ) ) then
        self.GRM_MemberDetailJoinDateToolTip:Hide();
        self.GRM_MemberDetailServerNameToolTip:Hide();
    end
end

-- Method:          GRM.PlayerNameTooltip( frame )
-- What it Does:    Builds tooltip when mousing over the player name
-- Purpose:         Compartmentalizing the code for cleanliness, like previous tooltip logic
GRM.PlayerNameTooltip = function( self )
    if not GRM_UI.GRM_altDropDownOptions:IsVisible() and not StaticPopup1:IsVisible() and self.GRM_MemberDetailNameText:IsMouseOver ( 1 , -1 , -1 , 1 ) and GetMouseFocus() == self then
        -- Get Class Color
        local textR, textG, textB = self.GRM_MemberDetailNameText:GetTextColor();

        -- Build the tooltip
        self.GRM_MemberDetailServerNameToolTip:SetOwner ( self.GRM_JoinDateText , "ANCHOR_CURSOR" );
        self.GRM_MemberDetailServerNameToolTip:AddLine ( GRM_G.currentName , textR , textG , textB );

        local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName];
        
        if player.race ~= "" and player.sex ~= 0 then
            self.GRM_MemberDetailServerNameToolTip:AddLine ( GRM.L ( "{name} {name2}" , C_CreatureInfo.GetRaceInfo ( raceIDEnum[player.race] ).raceName , GRM.GetSex ( player.sex ) ) , 1 , 1 , 1 );
        end

        self.GRM_MemberDetailServerNameToolTip:AddLine ( "|CFFE6CC7FRight-Click|r for Additional Options" );
        self.GRM_MemberDetailServerNameToolTip:Show();
    else
        self.GRM_MemberDetailServerNameToolTip:Hide();
    end
end

-- Method:          GRM.GetSex ( int )
-- What it Does:    Returns the localized text string of the given gender
-- Purpose:         Useful to report the player's sex.
GRM.GetSex = function ( index )
    local result = "";
    if index > 1 then
        if index == 2 then
            result = GRM.L ( "Male" );
        elseif index == 3 then
            result = GRM.L ( "Female" );
        end
    end
    return result;
end

-- Method:          GRM.JoinDateSyncAllAltsTooltip( frame )
-- What it Does:    Builds tooltip for players to know they can right click to sync all alts to same join date
-- Purpose:         Same as previous tooltips. Keep code clean.
GRM.JoinDateSyncAllAltsTooltip = function( self )
    if not self.GRM_DateSubmitButton:IsVisible() and not self.GRM_MemberDetailNJDSyncTooltip:IsVisible() and not self.GRM_SyncJoinDateSideFrame:IsVisible() and GetMouseFocus() == self and self.GRM_MemberDetailDateJoinedTitleTxt:IsMouseOver ( 1 , -1 , -1 , 1 ) and GRM.PlayerOrAltHasJD ( GRM_G.currentName ) then
        self.GRM_MemberDetailNJDSyncTooltip:SetOwner ( self.GRM_MemberDetailDateJoinedTitleTxt , "ANCHOR_CURSOR" );
        if GRM.IsAltJoinDatesSynced ( GRM_G.currentName ) then
            self.GRM_MemberDetailNJDSyncTooltip:AddLine( GRM.L ( "Join Date of All Alts is Currently Synced" ) );
        else
            self.GRM_MemberDetailNJDSyncTooltip:AddLine( GRM.L ( "|CFFE6CC7FRight-Click|r to Sync Join Date with Alts" ) );
        end
        self.GRM_MemberDetailNJDSyncTooltip:Show();
    elseif GetMouseFocus() ~= self or ( not self.GRM_MemberDetailDateJoinedTitleTxt:IsMouseOver ( 1 , -1 , -1 , 1 ) and self.GRM_MemberDetailNJDSyncTooltip:IsVisible() ) then
        self.GRM_MemberDetailNJDSyncTooltip:Hide();
    end
end

-- Method:          GRM.AltNameTooltip ( frame , table )
-- What it Does:    Populates the alt info tooltip
-- Purpose:         Useful info for player, keeps code clean.
GRM.AltNameTooltip = function ( self , guildData )
    if ( GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons ~= nil and #GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons > 0 and GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons[1][1]:IsVisible() ) and not StaticPopup1:IsVisible() and not GRM_UI.GRM_altDropDownOptions:IsVisible() and not ( GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenu:IsVisible() and GRM_UI.GRM_MemberDetailMetaData.GRM_YearDropDownMenu:IsMouseOver() ) and not ( GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenu:IsVisible() and GRM_UI.GRM_MemberDetailMetaData.GRM_DayDropDownMenu:IsMouseOver() ) then
            
        if GRM_UI.GRM_altFrameTitleText:IsMouseOver( 1 , -1 , -1 , 1 ) then

            if not IsShiftKeyDown() and not GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame:IsVisible() then
                -- Build the tooltip
                self.GRM_AltGroupingTooltip:SetOwner ( GRM_UI.GRM_altFrameTitleText , "ANCHOR_CURSOR" );
                self.GRM_AltGroupingTooltip:AddLine ( GRM.L ( "|CFFE6CC7FHold Shift|r to view more alt details." ) );
                self.GRM_AltGroupingTooltip:AddLine( GRM.L ( "|CFFE6CC7FShift-Click|r to keep alt details open." ) );
                self.GRM_AltGroupingTooltip:Show();
            elseif IsShiftKeyDown() and not GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame:IsVisible() then
                GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame:Show();        -- The OnShow will have the trigger action...
                GRM_G.pause = true;
            end
        else
            GRM_G.tempAltName = "";
            local player = guildData[GRM_G.currentName];
            if player ~= nil then
                local listOfAlts = player.alts;
                local AltTT = self.GRM_MemberDetailServerNameToolTip;
                local isOver = false;
                if GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons ~= nil then
                    for i = 1 , #GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons do
                        if GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons[i][1]:IsVisible() and GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons[i][1]:IsMouseOver ( 1 , -1 , -1 , 1 ) and GetMouseFocus() == GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons[i][1] and listOfAlts[i] and listOfAlts[i][1] then
                            GRM_G.tempAltName = listOfAlts[i][1];
                            AltTT:SetOwner ( GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrame.GRM_CoreAltScrollChildFrame.allFrameButtons[i][1] , "ANCHOR_CURSOR" );
                            AltTT:AddLine ( listOfAlts[i][1] , listOfAlts[i][2] , listOfAlts[i][3] , listOfAlts[i][4] );
                            AltTT:AddLine ( "|CFFE6CC7FRight-Click|r for Additional Options" );
                            isOver = true;
                            break;
                        end
                    end
                end

                if isOver and not GRM_UI.GRM_altDropDownOptions:IsVisible() then
                    AltTT:Show();
                elseif GRM_UI.GRM_altDropDownOptions:IsVisible() and not self.GRM_MemberDetailNameText:IsMouseOver ( 1 , -1 , -1 , 1 ) then
                    AltTT:Hide();
                end
            end
        end

        if GRM_UI.GRM_CoreAltFrame:IsMouseOver( 5 , 5 , 5 , 35 ) and GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName] ~= nil and #GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][GRM_G.currentName].alts >= 12 then
            GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrameSlider:Show();
            GRM_CoreAltScrollFrameSliderThumbTexture:Show();
        else
            GRM_UI.GRM_CoreAltFrame.GRM_CoreAltScrollFrameSlider:Hide();
        end

        if GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteScrollFrame:IsMouseOver( 1 , 1 , 1 , 13 ) and select ( 2 , GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteScrollFrameSlider:GetMinMaxValues() ) > 0 then
            GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteScrollFrameSlider:Show();
            GRM_CustomNoteScrollFrameSliderThumbTexture:Show()
        else
            GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteScrollFrameSlider:Hide();
        end

    elseif not self.GRM_MemberDetailNameText:IsMouseOver ( 1 , -1 , -1 , 1 ) then
        self.GRM_MemberDetailServerNameToolTip:Hide();
    end
end

-- Method:          GRM.PlayerStatusNotificationTooltip ( Frame )
-- What it Does:    Builds notification tooltip to let player know they can view player status updates
-- Purpose:         Useful info for the player
GRM.PlayerStatusNotificationTooltip = function ( self )
    if GetMouseFocus() == self and self.GRM_MemberDetailPlayerStatus:IsMouseOver ( 1 , -1 , -1 , 1 ) and not GRM_UI.GRM_altDropDownOptions:IsVisible() then
        self.GRM_MemberDetailNotifyStatusChangeTooltip:SetOwner ( self.GRM_MemberDetailPlayerStatus , "ANCHOR_CURSOR" );
        self.GRM_MemberDetailNotifyStatusChangeTooltip:AddLine ( "|cFFFFFFFF" .. GRM.L ( "|CFFE6CC7FRight-Click|r to Set Notification of Status Change" ) );

        self.GRM_MemberDetailNotifyStatusChangeTooltip:Show();
    else
        self.GRM_MemberDetailNotifyStatusChangeTooltip:Hide();
    end
end

-- Method:          GRM.AltGroupingTooltip ( frame )
-- What it Does:    Handles the side window for alt grouping window logic
-- Purpose:         Cleaner code in the overall tooltip window...
GRM.AltGroupingTooltip = function ( self )
    if GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame:IsVisible() and not GRM_G.AltSideWindowFreeze then
        if ( not GRM_UI.GRM_altFrameTitleText:IsMouseOver( 1 , -1 , -1 , 1 ) and not GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame:IsMouseOver ( 10 , -2 , -135 , 20 ) ) or ( ( GRM_UI.GRM_altFrameTitleText:IsMouseOver( 1 , -1 , -1 , 1 ) or GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame:IsMouseOver( 10 , -2 , -135 , 20 ) ) and not IsShiftKeyDown() ) then
            GRM_UI.GRM_MemberDetailMetaData.GRM_AltGroupingScrollBorderFrame:Hide();
            GRM_G.pause = false;
        end
    end
    -- Cleanup of this alt grouping tooltip
    if self.GRM_AltGroupingTooltip:IsVisible() and not GRM_UI.GRM_altFrameTitleText:IsMouseOver( 1 , -1 , -1 , 1 ) then
        self.GRM_AltGroupingTooltip:Hide();
    end
end

-- Method:          GRM.EditBdayTooltip ( self )
-- What it Does:    Provides a tooltip on the text so player knows they can edit it.
-- Purpose:         Clean code!
GRM.EditBdayTooltip = function ( self )
    if not self.GRM_BirthdayTooltip:IsVisible() and GetMouseFocus() == self and self.GRM_BirthdayText:IsVisible() and self.GRM_BirthdayText:IsMouseOver ( 1 , -1 , -1 , 1 ) then
        self.GRM_BirthdayTooltip:SetOwner ( self.GRM_BirthdayText , "ANCHOR_CURSOR" );
        self.GRM_BirthdayTooltip:AddLine (  GRM.L ( "Right-Click to Edit" ) );

        self.GRM_BirthdayTooltip:Show();
    elseif GetMouseFocus() ~= self or ( self.GRM_BirthdayTooltip:IsVisible() and not self.GRM_BirthdayText:IsMouseOver ( 1 , -1 , -1 , 1 ) ) then
        self.GRM_BirthdayTooltip:Hide();
    end
end

----------------------
-- CONFIG CHECK ------
----------------------

-- Method:          GRM.SystemMessageEnabledCheck()
-- What it does:    Does a check if system messages are still enabled.
-- Purpose:         To let the player know if they disable system messages the addon will not work.
GRM.SystemMessageEnabledCheck = function()
    GRM_G.SystemMessageTest = false;
    if not GRM_G.SystemMessageTestAnnounced then

        C_Timer.After ( 3 , function()
            if not GRM_G.SystemMessageTest then
                GRM_G.SystemMessageTestAnnounced = true;
                GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "Warning! System messages are disabled! GRM cannot function fully without them. You must re-enable them in the chat settings." ) );
            end
        end);
    end
end

----------------------
-- GLOBAL CONTROLS ---
----------------------

-- Method:          GRM.MemberDetailToolTips ( self , float )
-- What it Does:    Populates the tooltips on the "OnUpdate" check for the core Member Detail frame
-- Purpose:         UI Feature  
-- Note:            self = GRM_UI.GRM_MemberDetailMetaData
GRM.MemberDetailToolTips = function ( self , elapsed )
    GRM_G.timer2 = GRM_G.timer2 + elapsed;
    if GRM_G.timer2 >= 0.075 then
        if IsInGuild() then
            local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];

            -- PROMO DATE TEXT
            -- Only populate and show tooltip if mouse is over text frame and it is not already visible.
            GRM.PromoRankTooltip ( self , guildData );

            -- JOIN DATE TEXT
            GRM.JoinDateTooltip ( self , guildData );

            -- PLAYER NAME TEXT
            GRM.PlayerNameTooltip( self );

            -- JOIN DATE SYNC TEXT
            GRM.JoinDateSyncAllAltsTooltip( self );

            -- ALT NAMES
            GRM.AltNameTooltip ( self , guildData );

            -- PLAYER STATUS
            GRM.PlayerStatusNotificationTooltip ( self );

            -- ALT GROUPING
            GRM.AltGroupingTooltip ( self );

            -- EDIT TOOLTIPS
            GRM.EditBdayTooltip ( self );
            
            if GRM_G.pause then
                GRM_UI.GRM_MemberDetailMetaData.GRM_WindowIsLockedText:Show();
            else
                GRM_UI.GRM_MemberDetailMetaData.GRM_WindowIsLockedText:Hide();
            end
        end
        GRM_G.timer2 = 0;
    end
end

-- Method:          GRM.ModifySpecificGuildControlValue ( int , string )
-- What it Does:    Modifies the position of the guild Control and returns the string
-- Purpose:         Easy modification of the string.
GRM.ModifySpecificGuildControlValue = function ( position , newValue )
    local rules = GRM.GetRulesString();
    local result = "";
    local pos1;


    if position == 1 then
        local pos = string.find ( rules , ";" , 1 , true );
        result = newValue .. string.sub ( rules , pos );
    elseif position == 10 then
        result = string.sub ( rules , 1 , #rules - 1 ) .. newValue;
    else
        local c = 1;    -- start it at 1 so it is always incremented up by 1 ahead. 

        for i = 1 , #rules do
            if string.sub ( rules , i , i ) == ";" then
                c = c + 1;

                -- Joined and rejoined dates.
                if pos1 ~= nil then
                    result = string.sub ( rules , 1 , pos1 ) .. newValue .. string.sub ( rules , i );
                    break;
                end

                if c == position then
                    if c ~= 7 and c ~= 8 then
                        result = string.sub ( rules , 1 , i ) .. newValue .. string.sub ( rules , i + 2 );
                        break;
                    else
                        pos1 = i;
                    end
                end
            end
        end
    end

    return result;                                              
end

-- Method:          GRM.UpdateGuildInfoWithNewValue ( int , string )
-- What it Does:    Modifies the control values then inserts them into the guildInfo over the original GRM controls, effectively replacing them
-- Purpose:         Ease of controls of global GRM controls for the addon user.
GRM.UpdateGuildInfoWithNewValue = function ( controlIndex , newValue )
    if CanEditGuildInfo() then
        local guildInfoText = GetGuildInfoText();
        local rulesString = GRM.GetRulesString();

        if rulesString ~= nil then
            local first , last = string.find ( guildInfoText , GRM.GetRulesString() , 1 , true );

            if first ~= nil and last ~= nil and CanEditGuildInfo() then
                SetGuildInfoText ( string.sub ( guildInfoText , 1 , first - 1 ) .. GRM.ModifySpecificGuildControlValue ( controlIndex , tostring ( newValue ) ) .. string.sub ( guildInfoText , last + 1 ) );
            end
        end
    end
end

-- Method:          GRM.GetRulesString()
-- What it Does:    Pulls the rules from the guild Info note
-- Purpose:         To clear the guildInfo and clean it up for expanded  global controls.
GRM.GetRulesString = function()
    return string.match ( GetGuildInfoText() , "grm^(.+)^g" );
end

-- Method:          GRM.GetEachGlobalControl()
-- What it Does:    Returns the parsed string based on the pattern match
-- Purpose:         Faster and cleaer code for parsing the global controls
GRM.GetEachGlobalControl = function( rulesString )
    if rulesString then
        return string.match ( rulesString , GRM_G.GlobalControlPermissionPattern );
    else
        return nil;
    end
end

-- Method:          GRM.GetGlobalControlValue ( int )
-- What it Does:    Returns the Global control value from the guildInfo global string
-- Purpose:         Easily pull values from the parsed note.
GRM.GetGlobalControlValue = function ( index )
    local rulesString = GRM.GetRulesString();
    local result;

    if rulesString ~= nil then
        result = select ( index , GRM.GetEachGlobalControl( rulesString ) );
    end

    return result;
end


-- Method:          GRM.UpdateGuildLeaderPermissions( bool , bool )
-- What it Does:    Scans the guild leader note for special tags and controls, pushes them to addon player setting - Rechecks every 60 seconds...
-- Purpose:         So the guild leader can mass enable/disable certain features in the addon.
GRM.UpdateGuildLeaderPermissions = function( isMyEdit , forced )
    local notes = GetGuildInfoText();
    -- This extra check is because often in the first few seconds they do not load.
    if #notes == 0 and GRM_G.OnFirstLoad then
        C_Timer.After ( 1 , function()
            GRM.UpdateGuildLeaderPermissions ( isMyEdit , false );
        end);
        return;
    end

    if GRM_G.GuildInfo ~= notes then
        if notes ~= nil and #notes > 0 then

            if not GRM_G.tagFormatChangeNotice then
                GRM_G.tagFormatChangeNotice = true;

                if CanEditGuildInfo() and string.find ( notes , "grm^" , 1 , true ) == nil and ( string.find ( notes , "g1^" , 1 , true ) ~= nil or string.find ( notes , "g2^" , 1 , true ) ~= nil ) then
                    GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "A new format exists for global settings controls." ) .. "\n" .. GRM.L ( "Go to GRM window > Options > Officer Tab > \"Set Global Controls\"" ) .. "\n" .. GRM.L ( "Remove the old format when all guildies have updated properly. You may wish to leave it in Guild Info until then." ) );
                end

            end

            GRM_G.GlobalControlPermissionPattern = GRM_G.GlobalControlPermissionPattern or GRM.BuildComPattern ( 10 , ";" , false )
            local rulesString = GRM.GetRulesString();

            if rulesString ~= nil then
                local timeFormat , generalSync , banSync , customSync , joinDateLocation , enableUsingTags , joinTag , rejoinTag , noteTrigger = GRM.GetEachGlobalControl( rulesString );
                
                -- Timestamp format
                GRM.SetTimestampRestriction ( timeFormat , isMyEdit );

                -- Rank restriction control!
                GRM.SetLeaderRankRestrictionSetting ( generalSync , isMyEdit );

                -- Ban Restriction control
                GRM.SetLeaderBanRestrictionSetting ( banSync , isMyEdit );

                -- Custom Note Restriction Control
                GRM.SetLeaderCustomNoteRestrictionSetting ( customSync , isMyEdit );

                -- -- Join Date destiation Control
                GRM.SetLeaderJoinDateRestrictionSetting ( joinDateLocation , isMyEdit );

                -- -- Use join/Rejoin headers
                GRM.SetLeaderUsingJoinTagHeaders ( enableUsingTags );

                -- Tag Formatting
                GRM.SetJoinTagCustomFormat ( joinTag , rejoinTag , isMyEdit );

                -- -- !Note enabled controls
                GRM.SetNoteTriggerRestrictions ( noteTrigger );

            end
        end
        GRM_G.GuildInfo = GetGuildInfoText();

        if GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then
            GRM_UI.BuildLogFrames()
        end
    end
    if not forced then
        C_Timer.After ( 60 , function()
            if IsInGuild() then
                GRM.UpdateGuildLeaderPermissions ( false , false );
            end
        end);    -- Recheck once per minute indefinitely...
    end
end

-- Method:          GRM.SetJoinTagCustomFormat ( string , string , bool )
-- What it Does:    Checks for the number after the given match, and forces the player to adhere to those setting...
-- Purpose:         Conformity within the guild in the settings.
GRM.SetJoinTagCustomFormat = function ( customJoin , customRejoin , isMyEdit )
    -- first, very it is not nil
    local needsRefresh = false;

    -- DEFAULTS
    if customJoin == "XX" then
        customJoin = GRM.L ( "Joined:" );
    end
    if customRejoin == "XX" then
        customRejoin = GRM.L ( "Rejoined:" );
    end

    -- Custom JOIN
    if customJoin ~= "X" and customJoin ~= GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[1] and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeTag then

        -- Cannot be greater than Max Characters (16) characters
        if #customJoin > GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_OfficerOptionsFrame.GRM_CustomTagJoinEditBox:GetMaxLetters() then
            if isMyEdit then
                GRM.Report ( GRM.L ( "Custom Join Date Tag \"{name}\" cannot be set as it is {num} characters. The max is {custom1}." , customJoin , nil , #customJoin , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_OfficerOptionsFrame.GRM_CustomTagJoinEditBox:GetMaxLetters() ) );
            end
        else
            local finalReport = "";
            if isMyEdit or CanEditGuildInfo() then
                if #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[1] > 0 then
                    finalReport = GRM.L ( "Custom Join Tag has been changed from \"{name}\" to \"{name2}\"" , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[1] , customJoin );
                else
                    finalReport = GRM.L ( "Custom Join Tag has been set to \"{name}\"" , customJoin );
                end
            else
                if #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[1] > 0 then
                    finalReport = GRM.L ( "Your Guild Leader has changed the Custom Join Tag from \"{name}\" to \"{name2}\"" , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[1] , customJoin );
                else
                    finalReport = GRM.L ( "Your Guild Leader has set the Custom Join Tag to \"{name}\"" , customJoin );
                end
            end
            GRM.Report ( GRM.L ( "GRM:" ) .. " " .. finalReport );
            GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[1] = customJoin;

            if GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_OfficerOptionsFrame.GRM_CustomTagJoinEditBox:IsVisible() then
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_OfficerOptionsFrame.GRM_CustomTagJoinEditBox:SetText ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[1] );
            end
            needsRefresh = true;
        end
    end

    -- Custom REJOIN
    if customRejoin ~= "X" and customRejoin ~= GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[2] then

        -- Cannot be greater than Max Characters (16) characters
        if #customRejoin > GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_OfficerOptionsFrame.GRM_CustomTagREJoinEditBox:GetMaxLetters() then
            if isMyEdit then
                GRM.Report ( GRM.L ( "Custom Rejoin Date Tag \"{name}\" cannot be set as it is {num} characters. The max is {custom1}." , customRejoin , nil , #customRejoin , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_OfficerOptionsFrame.GRM_CustomTagREJoinEditBox:GetMaxLetters() ) );
            end
        else
            local finalReport = "";
            if isMyEdit or CanEditGuildInfo() then
                if #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[2] > 0 then
                    finalReport = GRM.L ( "Custom Rejoin Tag has been changed from \"{name}\" to \"{name2}\"" , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[2] , customRejoin );
                else
                    finalReport = GRM.L ( "Custom Rejoin Tag has been set to \"{name}\"" , customRejoin );
                end
            else
                if #GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[2] > 0 then
                    finalReport = GRM.L ( "Your Guild Leader has changed the Custom Rejoin Tag from \"{name}\" to \"{name2}\"" , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[2] , customRejoin );
                else
                    finalReport = GRM.L ( "Your Guild Leader has set the Custom Rejoin Tag to \"{name}\"" , customRejoin );
                end
            end
            GRM.Report ( GRM.L ( "GRM:" ) .. " " .. finalReport );

            GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[2] = customRejoin;
            if GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_OfficerOptionsFrame.GRM_CustomTagREJoinEditBox:IsVisible() then
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_OfficerOptionsFrame.GRM_CustomTagREJoinEditBox:SetText ( customRejoin );
            end
            needsRefresh = true;
        end
    end

    if needsRefresh then
        GRM.SetJoinAndRejoinTags();
        GRM.SyncSettings();
    end
end

-- Method:          GRM.SetNoteTriggerRestrictions ( string )
-- What it Does:    Checks the Guildinfo tag if settings need to be adjusted and modify the settings if they do.
-- Purpose:         Allow global control in enforcing all officers use this function.
GRM.SetNoteTriggerRestrictions = function ( noteTrigger )
    noteTrigger = tonumber ( noteTrigger );
    -- first, very it is not nil
    if noteTrigger ~= nil and noteTrigger < 3 then
        
        if not CanEditGuildInfo() then
            GRM_G.GlobalControl7 = true;
        end

        local needsRefresh = false;

        if noteTrigger == 1 and not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].noteSetEnabled then
            GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].noteSetEnabled = true;
            GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "'!note' trigger has been globally ENABLED" ) );
            needsRefresh = true;

        elseif noteTrigger == 2 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].noteSetEnabled then
            GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].noteSetEnabled = false;
            GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "'!note' trigger has been globally DISABLED" ) );
            needsRefresh = true
        end

        if needsRefresh then
            -- Update the frames if necessary
            if GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].noteSetEnabled then
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_OfficerOptionsFrame.GRM_NoteTagFeatureCheckButton:SetChecked ( true );
                else
                    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_OfficerOptionsFrame.GRM_NoteTagFeatureCheckButton:SetChecked ( false );
                end
            end
            GRM.SyncSettings();
        end

    else
        -- Not valid, disable it.
        GRM_G.GlobalControl7 = false;
    end
end

-- Method:          GRM.IsCustomJoinRestricted()
-- What it Does:    Returns true if the guild Info has the custom join tag note set properly to a custom
-- Purpose:         Prevent players from making editing changes
GRM.IsCustomJoinRestricted = function()
    local result = false;
    local customJoin = GRM.GetGlobalControlValue ( 7 );

    if customJoin ~= nil then        
        if customJoin == "XX" or #customJoin <= GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_OfficerOptionsFrame.GRM_CustomTagJoinEditBox:GetMaxLetters() then
            result = true;
        end
    end

    return result , customJoin;
end

-- Method:          GRM.IsCustomReJoinRestricted()
-- What it Does:    Returns true if the custom Rejoin is restricted globally using the guild info note
-- Purpose:         Prevent players from changing their settings if it is already restricted in the guild info note
GRM.IsCustomReJoinRestricted = function()
    local result = false;
    local customReJoin = GRM.GetGlobalControlValue ( 8 );

    if customReJoin ~= nil then     
        
        if customReJoin == "XX" or #customReJoin <= GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_OfficerOptionsFrame.GRM_CustomTagREJoinEditBox:GetMaxLetters() then
            result = true;
        end
    end

    return result , customReJoin;
end

-- Method:          GRM.SetTimestampRestriction ( string , bool )
-- What it Does:    Checks for the number after the given match, and forces the player to adhere to those setting...
-- Purpose:         Conformity within the guild in the settings.
GRM.SetTimestampRestriction = function ( timeFormatIndex , isMyEdit )
    
    -- Let's first verify it is not nil as any player could put anything there, no need to throw errors out there constantly due to bad formatting.
    if tonumber ( timeFormatIndex ) ~= nil then
        timeFormatIndex = tonumber ( timeFormatIndex );
    else
        GRM_G.GlobalControl5 = false;
        return;
    end

    if not CanEditGuildInfo() then
        GRM_G.GlobalControl5 = true;
    end

    if timeFormatIndex > 15 then
        timeFormatIndex = 15
    end

    -- Now, we check to see if yours matches it or not. If it doesn't, you need to change it.
    if timeFormatIndex ~= GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].dateFormat then

        GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].dateFormat = timeFormatIndex;
        GRM.UpdateGuildInfoWithNewValue ( 1 , timeFormatIndex );

        local finalReport = "";
        local month , day , year = select ( 2 , GRM.CalendarGetDate() );
        local timestamp = GRM.FormatTimeStamp ( ( day .. " " .. monthEnum2 [ tostring ( month ) ] .. " '" .. ( year - 2000 ) ) , false );

        if isMyEdit or CanEditGuildInfo() then
            finalReport = GRM.L ( "Timestamp Formatting has been Globally Set to: < {name} >" , timestamp );
        else
            finalReport = GRM.L ( "Your Guild Leader Has Globally Set the Timestamp Formatting to: < {name} >" , timestamp );
        end
        GRM.Report ( GRM.L ( "GRM:" ) .. " " .. finalReport );

        if GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame:IsVisible() then
            GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame.GRM_TimestampSelected.GRM_TimestampSelectedText:SetText ( timestamp );
        end
        GRM.SyncSettings();
    end
end

-- Method:          GRM.SetLeaderRankRestrictionSetting ( string , bool )
-- What it Does:    Checks for the number after the given match, and forces the player to adhere to those setting...
-- Purpose:         Conformity within the guild in the settings.
GRM.SetLeaderRankRestrictionSetting = function ( generalSyncIndex , isMyEdit )

    generalSyncIndex = tonumber ( generalSyncIndex );
    if generalSyncIndex ~= nil then

        if not CanEditGuildInfo() then
            GRM_G.GlobalControl1 = true;
        end

        -- Now, we need to verify it is a valid rank number. Note, indexes start at zero, but a general count starts at 1. So number of ranks is 10, while their index is 0-9.
        if generalSyncIndex > GuildControlGetNumRanks() - 1 then
            generalSyncIndex = GuildControlGetNumRanks() - 1;
        end
        -- Now, we check to see if yours matches it or not. If it doesn't, you need to change it.
        if generalSyncIndex ~= GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank then

            local finalReport = "";
            if isMyEdit or CanEditGuildInfo() then
                finalReport = GRM.L ( "Sync Restrictions Globally Changed to {name} or Higher" , GuildControlGetRankName( generalSyncIndex + 1 ) );
            else
                finalReport = GRM.L ( "Your Guild Leader Has Set Sync Restrictions to {name} or Higher" , GuildControlGetRankName( generalSyncIndex + 1 ) );
            end
            GRM.Report ( GRM.L ( "GRM:" ) .. " " .. finalReport );

            GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank = generalSyncIndex;
            if GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame:IsVisible() then
                GRM.CreateOptionsRankDropDown();
            end
            GRM.SyncSettings();

        end
    else
        GRM_G.GlobalControl1 = false;
    end
end

-- Method:          GRM.SetLeaderBanRestrictionSetting ( string , bool )
-- What it Does:    Checks for the number after the given match, and forces the player to adhere to those Ban setting...
-- Purpose:         Conformity within the guild in the settings.
GRM.SetLeaderBanRestrictionSetting = function ( banSync , isMyEdit )
    banSync = tonumber ( banSync );

    -- first, very it is not nil
    if banSync ~= nil then

        if not CanEditGuildInfo() then
            GRM_G.GlobalControl2 = true;
        end
        
        -- Now, we need to verify it is a valid rank number. Note, indexes start at zero, but a general count starts at 1. So number of ranks is 10, while their index is 0-9.
        if banSync > GuildControlGetNumRanks() - 1 then
            banSync = GuildControlGetNumRanks() - 1;
        end
        -- Now, we check to see if yours matches it or not. If it doesn't, you need to change it.
        if banSync ~= GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankBanList then

            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank < banSync then
                GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankBanList = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank;
                GRM.UpdateGuildInfoWithNewValue ( 3 , GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank )

                GRM.Report ( GRM.L ( "Warning! Unable to select a Ban List rank below \"{name}\"" , GuildControlGetRankName( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankBanList + 1 ) )  .. "\n" .. GRM.L ( "Setting to match core filter rank" ) );

            else
                GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankBanList = banSync;
                
                GRM.UpdateGuildInfoWithNewValue ( 3 , banSync )

                local finalReport = "";
                if isMyEdit or CanEditGuildInfo() then
                    finalReport = GRM.L ( "BAN Sync Restrictions Globally Changed to {name} or Higher" , GuildControlGetRankName( banSync + 1 ) );
                else
                    finalReport = GRM.L ( "Your Guild Leader Has Set BAN Sync Restrictions to {name} or Higher" , GuildControlGetRankName( banSync + 1 ) );
                end
                GRM.Report ( GRM.L ( "GRM:" ) .. " " .. finalReport );

                
                if GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame:IsVisible() then
                    GRM.CreateOptionsRankDropDown();
                end
            end

            GRM.SyncSettings();
        end
    else
        GRM_G.GlobalControl2 = false;
    end
end

-- Method:          GRM.SetLeaderCustomNoteRestrictionSetting ( string , bool )
-- What it Does:    Checks for the number after the given match, and forces the player to adhere to those Ban setting...
-- Purpose:         Conformity within the guild in the settings.
GRM.SetLeaderCustomNoteRestrictionSetting = function ( customSyncIndex , isMyEdit )
    customSyncIndex = tonumber ( customSyncIndex );

    -- first, very it is not nil
    if customSyncIndex ~= nil then

        if not CanEditGuildInfo() then
            GRM_G.GlobalControl3 = true;
        end

        -- Now, we need to verify it is a valid rank number. Note, indexes start at zero, but a general count starts at 1. So number of ranks is 10, while their index is 0-9.
        if customSyncIndex > GuildControlGetNumRanks() - 1 then
            customSyncIndex = GuildControlGetNumRanks() - 1;
        end

        -- Now, we check to see if yours matches it or not. If it doesn't, you need to change it.
        if customSyncIndex ~= GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankCustom then
            GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankCustom = customSyncIndex;

            local finalReport = "";
            if isMyEdit or CanEditGuildInfo() then
                finalReport = GRM.L ( "CUSTOM NOTE Sync Restrictions Globally Changed to {name} or Higher" , GuildControlGetRankName( customSyncIndex + 1 ) );
            else
                finalReport = GRM.L ( "Your Guild Leader Has Set CUSTOM NOTE Sync Restrictions to {name} or Higher" , GuildControlGetRankName( customSyncIndex + 1 ) );
            end
            GRM.Report ( GRM.L ( "GRM:" ) .. " " .. finalReport );

            
            local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
            for _ , player in pairs ( guildData ) do
                if type ( player ) == "table" then
                    player.customNote[4] = customSyncIndex;
                    player.customNote[5] = false;
                end
            end

            if GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame:IsVisible() then
                GRM.CreateOptionsRankDropDown();
            end
            GRM.SyncSettings();
        end
    else
        GRM_G.GlobalControl3 = false;
    end
end

-- Method:          GRM.SetLeaderJoinDateRestrictionSetting ( int , bool )
-- What it Does:    Checks for the number after the given match and forces all players to adhere to that setting.
-- Purpose:         Conformity and unification of addon settings and to prevent multiple people from adding join date to multiple locations.
GRM.SetLeaderJoinDateRestrictionSetting = function ( joinDateLocationIndex , isMyEdit )
    joinDateLocationIndex = tonumber ( joinDateLocationIndex );

    -- first, very it is not nil
    if joinDateLocationIndex ~= nil then

        if not CanEditGuildInfo() then
            GRM_G.GlobalControl4 = true;
        end

        -- Confirming it is GL controlled
        local location = { "Officer Note" , "Public Note" , "Custom Note" };
        local needsRefresh = false;

        if joinDateLocationIndex > 3 or joinDateLocationIndex < 0 then

            GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination = 1;
            GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].addTimestampToNote = true;
            joinDateLocationIndex = 1;
            GRM.UpdateGuildInfoWithNewValue ( 5 , joinDateLocationIndex );

        -- Update guildinfo...
            GRM.Report ( GRM.L ( "Setting join date to destination index {num} is not valid. Value must be between 0 and 3. Setting to default of Officer Note." , nil , nil , joinDateLocationIndex ) );

            needsRefresh = true;

        end      
        
        -- Disabled = 0, Officer Note = 1, Public Note = 2 , custom = 3
        if joinDateLocationIndex ~= GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination then
            GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination = joinDateLocationIndex;
            GRM.UpdateGuildInfoWithNewValue ( 5 , joinDateLocationIndex );

            if joinDateLocationIndex == 0 then
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].addTimestampToNote then
                    GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].addTimestampToNote = false
                end
            else
                GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].addTimestampToNote = true;
            end
            needsRefresh = true;
        end

        if needsRefresh then
            local finalReport = "";
            if isMyEdit or CanEditGuildInfo() then
                if joinDateLocationIndex == 0 then
                    finalReport = GRM.L ( "Join Dates Globally Changed to no longer be added to the note when a player joins the guild" );
                else
                    finalReport = GRM.L ( "Join Dates Globally Changed to only be added to the {name}" , location[joinDateLocationIndex] );
                end
            else
                if joinDateLocationIndex == 0 then
                    finalReport = GRM.L ( "Your Guild Leader has disabled adding join dates to player notes" );
                else
                    finalReport = GRM.L ( "Your Guild Leader Has Set Join Dates to only be added to the {name}" , location[joinDateLocationIndex] );
                end
            end
            GRM.Report ( GRM.L ( "GRM:" ) .. " " .. finalReport );
        end

        if needsRefresh then
            if GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then
                GRM_UI.ConfigureJoinDateLocation();
            end
            GRM.SyncSettings();
        end

    else
        GRM_G.GlobalControl4 = false;
    end
end

-- Method:          GRM.SetLeaderUsingJoinTagHeaders ( string )
-- What it Does:    Checks if including the tag headers should be enabled or disabled
-- Purpose:         To help conformity in the guild on the destination of the notes.
GRM.SetLeaderUsingJoinTagHeaders = function( headerControl )
    if headerControl == "+" or headerControl == "-" then
        local needsRefresh = false;

        if not CanEditGuildInfo() then
            GRM_G.GlobalControl4_5 = true;
        end

        -- + = on, - = off
        if headerControl == "+" and not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeTag then
            GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeTag = true;
            GRM.Report ( GRM.L ( "The \"{name}\" and \"{name2}\" tags have globally been set to be added to join date note entries." , GRM_G.customHeaderJoin , GRM_G.customHeaderRejoin ) );
            needsRefresh = true;
        elseif headerControl == "-" and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeTag then
            GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeTag = false;
            GRM.Report ( GRM.L ( "The \"{name}\" and \"{name2}\" tags have globally been disabled from adding to join date note entries." , GRM_G.customHeaderJoin , GRM_G.customHeaderRejoin ) );
            needsRefresh = true;
        end

        if needsRefresh then
            if GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then
                GRM_UI.ConfigureJoinDateLocation();
            end
            GRM.UpdateGuildInfoWithNewValue ( 6 , headerControl );
            GRM.SyncSettings();
        end

    else
        GRM_G.GlobalControl4_5 = false;
    end
end



-- Method:          GRM.IsSyncRankGuildLeaderRestricted ( int )
-- What it Does:    Returns a boolean if the player's guild leader does have restriction standardization
--                  and returns the rank of that restriction by index.
-- Purpose:         Necessary for UX quality control on various methods to change and manipulate the settings.
GRM.IsSyncRankGuildLeaderRestricted = function ( selectedIndex )

    local result = false;
    local controlValue = GRM.GetGlobalControlValue ( selectedIndex );

    if controlValue ~= nil and controlValue ~= "X" then
        result = true;
        if tonumber ( controlValue ) ~= nil then
            controlValue = tonumber ( controlValue );
        end
    
        if selectedIndex == 1 then
            if controlValue > 15 or controlValue < 1 then
                controlValue = 1;
            end
    
        elseif selectedIndex > 1 and selectedIndex < 5 then -- 2 , 3 , 4
            if controlValue > GuildControlGetNumRanks() - 1 then   -- Set to lowest if necessary.
                controlValue = GuildControlGetNumRanks() - 1;
            end
    
        elseif selectedIndex == 5 then
            if controlValue > 3 or controlValue < 0 then
                -- default
                controlValue = 1;
            end
    
        elseif selectedIndex == 6 then
            if controlValue ~= "+" and controlValue ~= "-" then
                result = false;
            end
    
        elseif selectedIndex == 9 then
            if controlValue < 1 or controlValue > 2 then
                result = false;
            end
        end
    end    

    return result , controlValue;
end

-- Method:          GRM.ReportRankRestriction ( int )
-- What it Does:    Reports restriction rank if you try make a change to sync rank restrictions, yet in the guild info there are values set to control it.
-- Purpose:         Use for reporting any of the sync rank restrictions...
GRM.ReportRankRestriction = function ( rankIndex )
    if rankIndex ~= nil then
        GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "Unable to Change Rank. Guild Leader has set restriction to {name} or higher" , GuildControlGetRankName ( rankIndex + 1 ) ) );
    else
        GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "Unable to Change Rank. Guild Leader has set restriction level." ) );
    end
end

-- Method:          GRM.GetRankRestrictionText ( int )
-- What it Does:    Returns the text of the restriction rank guild leader has set it to for the controlled values in the guild info window
-- Purpose:         To make it obvious for player why they do not have access to sync rank controls.
GRM.GetRankRestrictionText = function ( rankIndex )
    local result = "";
    if rankIndex ~= nil then
        result = GRM.L ( "Unable to Change Rank. Guild Leader has set restriction to {name} or higher" , GuildControlGetRankName ( rankIndex + 1 ) );
    else
        result = GRM.L ( "Unable to Change Rank. Guild Leader has set restriction level." );
    end
    return result;
end

-- Method:          GRM.GetGlobalSettingsProgress()
-- What it Does:    Compiles the progress with the global functions
-- Purpose:         For use with creating an export of them.
GRM.GetGlobalSettingsProgress = function()
    local rulesString = GRM.GetRulesString();
    local result = {};

    if rulesString ~= nil then
        local guildControl = { GRM.GetEachGlobalControl( rulesString ) };
            
        for i = 1 , #guildControl do
            if guildControl[i] ~= "X" then
                table.insert ( result , { true , guildControl[i] } );
            else
                table.insert ( result , { false } );
            end
        end
        
    end
    return result;
end

-- Method:          GRM.AllGlobalsAreSet()
-- What it Does:    Returns true if all globals are set in the guildInfo properly
-- Purpose:         To help with guild Info global settings and tooltip info for UX
GRM.AllGlobalsAreSet = function()
    local result = true;
    local globals = GRM.GetGlobalSettingsProgress();

    if #globals > 0 then
        for i = 1 , #globals do
            if i < 10 and not globals[i][1] then
                result = false;
                break;
            end
        end
    else
        result = false;
    end

    return result;
end

-- Method:          GRM.SetGlobalControlsToGuildInfo()
-- What it Does:    Rebuilds the guild control tags and inserts them back into the guildInfo
-- Purpose:         Give the user the ability to control some specific functions globally.
GRM.SetGlobalControlsToGuildInfo = function()
    local controlString = GRM.GetAllGlobalRulesAsString();
    local existingRules = GRM.GetRulesString();
    local notes = GetGuildInfoText();
    local result = "";

    if existingRules ~= nil then
        -- ok rules already there, so we will need to save over them.
        local startIndex , endIndex = string.find ( notes , existingRules , 1 , true );
        if startIndex ~= nil and endIndex ~= nil then
            result = ( string.sub ( notes , 1 , startIndex - 5 ) .. controlString .. string.sub ( notes , endIndex + 3 ) );
        end
    else
        -- Just append to the end
        result = GRM.Trim ( notes ) .. "\n" .. controlString;
    end
    
    -- Ok, let's set the values
    if #result > 0 and GRM_UI.GuildInfoEditBox then
        if #result <= GRM_UI.GuildInfoEditBox:GetMaxLetters() then

            SetGuildInfoText ( result );
            GRM.Report ( GRM.L ( "Global controls exported to the guild info note. Updating..." ) );
            
            -- No delay is really needed but this is a fun progression feeling
            C_Timer.After ( 1.5 , function()
                GRM.Report ( GRM.L ( "Complete" ) .. " - " .. GRM.L ( "It may take up to 60 seconds for other guild members to detect the changes and update." ) );
            end);
            
            -- The delay needs to be here as sometimes the note, while update on your end, takes about 10 seconds or less to get a callback that the server properly updated it.
            C_Timer.After ( 10 , function()
                GRMsync.SendMessage ( "GRM_GCHAT" , "GINFOUPDATE?" , "GUILD" );     -- Send out to force others to update their permissions
                GRM.UpdateGuildLeaderPermissions ( true , true );
            end);

        else
            GRM.Report ( GRM.L ( "Unable to add globals controls to GuildInfo. There is not enough room." ) );
            GRM.Report ( GRM.L ( "You need to clear {num} characters to fit the control tags" , nil , nil , ( #result - GRM_UI.GuildInfoEditBox:GetMaxLetters() ) ) );
        end
    end

end

-- Method:          GRM.GetAllGlobalRulesAsString()
-- What it Does:    Builds the Globals string for the GuildInfo window
-- Purpose:         Quality of Life to help ease new players into the addon.
GRM.GetAllGlobalRulesAsString = function()

    local modifier = "+";
    if not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeTag then
        modifier = "-";
    end

    local customHeader = "^^";
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[1] == "" or GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[1] == GRM.L ( "Joined:" ) then
        customHeader = "XX";
    else
        customHeader = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[1];
    end

    local customRejoinHeader = "";
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[2] == "" or GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[2] == GRM.L ( "Rejoined:" ) then
        customRejoinHeader = "XX";
    else
        customRejoinHeader = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].customTags[2];
    end

    local noteEnabled = 1;
    if not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].noteSetEnabled then
        noteEnabled = 2;
    end
 
    local globalRules = ( "grm^" .. 
    GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].dateFormat .. ";" ..
    GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRank .. ";" ..
    GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankBanList .. ";" ..
    GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncRankCustom .. ";" ..
    GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination .. ";" ..
    modifier .. ";" ..
    customHeader .. ";" ..
    customRejoinHeader .. ";" ..
    noteEnabled .. ";" .. 
    "X" .. "^g" );

    return globalRules;
end

------------------------------
------ NOTE SCANNING ---------
---- SEARCH AND PARSING ------
------------------------------

-- Method:          GRM.CheckAllDates( boolean )
-- What it Does:    Returns all of the date info of players who have notes, but no GRM data, ot the other way around, which note it is in, and the status of the note in terms of consistency with addon/note data
-- Purpose:         To manage the extremely complicated parsing of the notes to properly display note data...
GRM.CheckAllDates = function( showAll )
    local result;
    local canReadOfficer = GRM.CanEditOfficerNote();    -- No need to call for it over and over
    local collectNamesThatMisMatched = {};
    local noteLocation = 0;     -- 1 = officer, 2 = public, 3 = custom, 4 = multipleLocations
    local noteStatus = 0; -- 1 = doNotMatch , 2 = noteInWrongNote , 3 = NoNoteExists , 4 = doNotMatch && noteInWrongNote , 5 = doNotMatch && correctLocation && dateInMultipleNotes , 6 = doNotMatch && noteInWrongNote && dateInMultipleNotes
                          -- 7 = noteInWrongNote && dateInMultipleNotes , 8 == doMatch && CorrectLocation && multipleNotes , 9-12 - no GRM date imported yet
    
    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];
    local player;

    for x in pairs ( guildData ) do
        if type ( guildData[x] ) == "table" then
            player = guildData[x];
            noteStatus = 0;
            noteLocation = 0;
            result = nil;

            -- Check Officer
            if canReadOfficer and player.officerNote~= "" then
                result = GRM.GetParsedDate ( player.officerNote);
                -- If the note is found here, but not found in the proper place...
                if result ~= nil then
                    noteLocation = 1;
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination ~= 1 then
                        noteStatus = 2
                    end
                end
            end
            
            -- Custom Notes
            if result == nil and player.customNote[6] ~= "" then
                result = GRM.GetParsedDate ( player.customNote[6] );
                if result ~= nil then
                    noteLocation = 3;
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination ~= 3 then
                        noteStatus = 2
                    end
                end
            end

            -- Check Public
            if result == nil and player.note ~= "" then
                result = GRM.GetParsedDate ( player.note );
                if result ~= nil then
                    noteLocation = 2;
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination ~= 2 then
                        noteStatus = 2
                    end
                end
            end

            if result ~= nil then
                -- If GRM addon date does not match parsed note date.
                if not GRM.DatesDoMatch ( result , player.name ) then

                    -- If note is not in the right location AND it doesn't match
                    if noteStatus == 2 then
                        noteStatus = 4; -- 4 = doNotMatch && noteInWrongNote
                        if GRM.IsDateInOtherNotes ( player.name , noteLocation ) then
                            noteStatus = 6; -- 6 = doNotMatch && noteInWrongNote && dateInMultipleNotes
                            noteLocation = 4; -- Multiple Locations
                        end
                        if #player.joinDate == 0 then
                            if noteStatus == 4 then
                                noteStatus = 9;     -- noteInWrongNote && no GRM date saved
                            elseif noteStatus == 6 then
                                noteStatus = 10;    -- noteInWrongNote && dateInMultipleNotes && no GRM date saved
                            end
                        end
                    else
                        -- Note is IN the right location, but they do not match... Let's see if they don't match, IS in the right location, but is also in multiple notes.
                        noteStatus = 1;
                        if GRM.IsDateInOtherNotes ( player.name , noteLocation ) then
                            noteStatus = 5; -- 5 = doNotMatch && correctLocation && dateInMultipleNotes
                            noteLocation = 4; -- Multiple Locations
                            if #player.joinDate == 0 then
                                noteStatus = 11; -- correctLocation && dateInMultipleNotes && No GRM date saved
                            end
                        else
                            if #player.joinDate == 0 then
                                noteStatus = 12; -- correctLocation && No GRM date saved
                            end
                        end
                    end
                    table.insert ( collectNamesThatMisMatched , { player.name , result , noteLocation , noteStatus , player.name , false } );
                else
                    -- Else, they DO match!!!
                    -- If note matches, but is in the wrong location
                    if noteStatus == 2 then
                        if GRM.IsDateInOtherNotes ( player.name , noteLocation ) then
                            noteStatus = 7; -- 7 = noteInWrongNote && dateInMultipleNotes
                            noteLocation = 4; -- Multiple Locations
                        end
                    else
                        -- doMatch && InRightNote && MultipleNotes
                        if GRM.IsDateInOtherNotes ( player.name , noteLocation ) then
                            noteStatus = 8; -- 8 == doMatch && CorrectLocation && multipleNotes
                            noteLocation = 4; -- Multiple Locations
                        end
                    end
                    if showAll or ( not showAll and noteStatus ~= 0 ) then
                        table.insert ( collectNamesThatMisMatched , { player.name , result , noteLocation , noteStatus , player.name , false } );
                    end
                end
            elseif #player.joinDate ~= 0 then
                -- Date is in GRM, but there is no note.
                noteStatus = 3 -- Doesn't exist, or at least was not identified in the parsing...
                table.insert ( collectNamesThatMisMatched , { player.name , result , noteLocation , noteStatus , player.name , false } );
            end
        end
    end
    return collectNamesThatMisMatched;
end

-- Method:          GRM.IsDateInOtherNotes ( int , int )
-- What it Does:    Returns true if the date is found in multiple locations...
-- Purpose:         So the addon can know if it is necessary to cleanup notes.
GRM.IsDateInOtherNotes = function ( name , noteLocation )
    local result = false;
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][GRM_G.guildName][name];

    -- Check officer first
    if noteLocation ~= 1 and GRM.CanEditOfficerNote() then
        if GRM.GetParsedDate ( player.officerNote ) ~= nil then
            result = true;
        end
    end

    -- Check Custom Note next
    if not result and noteLocation ~= 3 then
        if GRM.GetParsedDate ( player.customNote[6] ) ~= nil then
            result = true;
        end
    end

    -- Check Public Note next
    if not result and noteLocation ~= 2 then
        if GRM.GetParsedDate ( player.note ) ~= nil then
            result = true;
        end
    end

    return result;
end

-- Method:          GRM.NoteHasJoinTag ( string )
-- What it Does:    Checks a given string from a note and searches for the "Joined:" text and returns true, as well as the string at the next appropriate start position
-- Purpose:         Make parsing notes extremely simple.
GRM.NoteHasJoinTag = function ( note )
    local headers = { GRM.L ( "Joined" ) , "joined" , "Joined" , GRM.L ( "Rejoined" ) , "rejoined" , "Rejoined" , "Rejoin" , "rejoin" , "Returned" , "returned" , GRM_G.customHeaderJoin , GRM_G.customHeaderRejoin };
    local indEnd;

    -- Check for all potential date headers
    local tagFound = function ( note )
        local ind;
        for i = 1 , #headers do
            ind = select ( 2 , string.find ( note , headers[i] , 1 , true ) );       -- checks for tags with selected language translation
            if ind ~= nil then
                break;
            end
        end
        return ind;
    end
    indEnd = tagFound ( note );
    if indEnd ~= nil then
        local increment = 2;
        if string.sub ( note , indEnd + 1 , indEnd + 1 ) ~= ":" then                -- Adds increased compatibility as some removed the ":" on their own custom formats
            increment = 1;
        end
        return { true , GRM.Trim ( string.sub ( note , indEnd + increment ) ) };
    else
        return { false , nil };
    end
end

-- Method:          GRM.CleanupNoteDate ( string )
-- What it Does:    Removes any "st" or "th" or the "," abbrev. in English - Universally compatible as no other selected languages have dates that this would mess with.
-- Purpose:         Increases compatibility to some custom formats for parsing.
GRM.CleanupNoteDate = function ( note )
    note = string.gsub ( string.gsub ( string.gsub ( note , ", " , " " ) , "%. " , " " ) , "," , " " );    -- just pulls the 1 value
    local first,last;

    -- Remove the "th" if it is preceeded by a number -- %d represents a digit
    while string.find ( note , "%dth" ) ~= nil do
        first , last = string.find ( note , "%dth" );
        note = string.sub ( note , 1 , first ) .. string.sub ( note , last + 1 );
    end

    -- same as above.
    while string.find ( note , "%dst" ) ~= nil do
        first , last = string.find ( note , "%dst" );
        note = string.sub ( note , 1 , first ) .. string.sub ( note , last + 1 );
    end

    return note;
end

-- Method:          GRM.MonthNameFirst ( string )
-- What it Does:    Returns true if it is determined the first character is a number, in the note
-- Purpose:         To help with parsing the string into buckets for refinement
GRM.MonthNameFirst = function ( note )
    local result = false;
    if tonumber ( string.sub ( note , 1 , 1 ) ) ~= nil then
        result = true;
    end
    return result;
end

-- Method:          GRM.IsValidYear ( int )
-- What it Does:    Returns true if the year is greater than 3 (2004 is when game launched), and less than or equal to current year
-- Purpose:         Easy reusable check when parsing dates in the notes.
GRM.IsValidYear = function ( year )
    local result = false;
    if year ~= nil and type ( year ) == "number" and year > 3 and year <= ( GRM.GetCurrentCalendarTime().year - 2000 ) then
        result = true;
    end
    return result;
end

-- Method:          GRM.IsValidMonth ( int )
-- What it Does:    Returns true if the index is valid between 1 and 12
-- Purpose:         Verification of accurate data.
GRM.IsValidMonth = function ( month )
    local result = false;
    if month ~= nil and type ( month ) == "number" and month > 0 and month < 12 then
        result = true;
    end
    return result;
end 

-- Method:          GRM.IsValidDay ( int , int , int )
-- What it Does:    Returns true if the day is within the correct limits of that month, leap year accounted for.
-- Purpose:         Easy reusable check when parsing dates in the notes.
GRM.IsValidDay = function ( day , month , year )
    local result = false;
    if day ~= nil and type ( day ) == "number" and type ( month ) == "number" and type ( year ) == "number" and day < 32 and day > 0 and month < 13 and month > 0 then
        if ( month == 2 and day == 29 and GRM.IsLeapYear ( year + 2000 ) ) or ( day <= daysInMonth[ tostring(month) ] ) then
            result = true;
        end
    end
    return result;
end

-- Method:          GRM.GetFormattedDatesFromMonthStrFirstNote ( string )
-- What it Does:    Returns the day,mos,year array from parsed string that starts with "text" date first (3 formats)
-- Purpose:         Reverse engineer the date in the notes.
GRM.GetFormattedDatesFromMonthStrFirstNote = function ( note )
    local result = nil;
    local tempRes = {};
    local day , mos , year;
    local restOfString = "";

    -- Returns if proper month format, regardless of language.
    local monthFound = function ( monthStr )
        local result = false;
        local month = GRM.OrigL ( monthStr );                   -- No need to reprocess over and over in the nested boolean check in for loop

        for i = 1 , 12 do
            if monthEnum2 [ tostring ( i ) ] == month then
                result = true;
                break;
            end
        end

        return { result , month };
    end

    if string.find ( note , "-" ) == nil and string.find ( note , " " ) ~= nil then
        -- 2 potential formats
        tempRes = monthFound ( string.sub ( note , 1 , string.find ( note , " " ) - 1 ) );
        
        -- Moment of truth, if we find result, we now need to split it.
        -- Example 2 formats: "Apr 24 '19" or "Apr 24 19"
        if tempRes[1] then
            mos = monthEnum[tempRes[2]];
            note = string.sub ( note , string.find ( note , " " ) + 1 );
            
            -- to prevent lua errors on eventual possible nil results on finds, due to player potential input could be anything...
            if string.find ( note , " " ) ~= nil then
                day = tonumber ( string.sub ( note , 1 , string.find ( note , " " ) - 1 ) );
                note = string.sub ( note , string.find ( note , " " ) + 1 );
            end

            -- carry on! Still cannot set to true valid return info as of yet as we do not know if it will be good
            if day ~= nil then
                -- Now, need to determine if apostrophe
                if string.sub ( note , 1 , 1 ) == "'" then
                    year = tonumber ( string.sub ( note , 2 , 3 ) );
                    restOfString = string.sub ( note , 4 );
                else
                    year = tonumber ( string.sub ( note , 1 , 4 ) );
                    restOfString = string.sub ( note , 5 );
                    if year == nil then
                        year = tonumber ( string.sub ( note , 1 , 2 ) );
                        restOfString = string.sub ( note , 3 );
                    elseif GRM.IsValidYear ( year - 2000 ) then
                        year = year - 2000;
                    end
                end
            end
            
            if GRM.IsValidYear ( year ) and GRM.IsValidDay ( day , mos , year ) then
                result = { day , mos , year }
            end
        end
    elseif string.find ( note , "-" ) ~= nil then
        -- 1 potential format - "Apr-24-2019"
        tempRes = monthFound ( string.sub ( note , 1 , string.find ( note , "-" ) - 1 ) );
        if tempRes[1] then
            mos = monthEnum[tempRes[2]];
            note = string.sub ( note , string.find ( note , "-" ) + 1 );

            if string.find ( note , "-" ) ~= nil then
                day = tonumber ( string.sub ( note , 1 , string.find ( note , "-" ) - 1 ) );
                note = string.sub ( note , string.find ( note , "-" ) + 1 );
            end

            if #( string.sub ( note , 1 , 4 ) ) == 4 and tonumber ( string.sub ( note , 1 , 4 ) ) ~= nil then
                year = tonumber ( string.sub ( note , 1 , 4 ) ) - 2000;
                restOfString = string.sub ( note , 5 );
            end

            if GRM.IsValidYear ( year ) and GRM.IsValidDay ( day , mos , year ) then
                result = { day , mos , year };
            end
        end
    end

    return result , restOfString;
end

-- Method:          GRM.GetNoteParsingBucket ( string )
-- What it Does:    Places the category into one of 4 buckets to be further parsed, or returns nil if none are valid
-- Purpose:         To assist with the date parsing out of the officer notes.
GRM.GetNoteParsingBucket = function ( note )
    local result;
    local separators = { " " , "-" , "/" , "." };

    -- Determine if day format is valid (or year in case of dropdown 15 for bucket 2)
    local GetBucket = function ( text , str , bucketNum )
        local d;

        if ( string.sub ( text , 2 , 2 ) == str and tonumber ( string.sub ( text , 1 , 1 ) ) ~= nil ) then
            d = tonumber ( string.sub ( text , 1 , 1 ) );
            text = string.sub ( text , 3 );                 -- parsing out the first saved number, returning the remaining to save time.
        elseif string.sub ( text , 3 , 3 ) == str and tonumber ( string.sub ( text , 1 , 2 ) ) ~= nil then
            d = tonumber ( string.sub ( text , 1 , 2 ) );
            text = string.sub ( text , 4 );
        elseif bucketNum == 2 and string.sub ( text , 5 , 5 ) == str and tonumber ( string.sub ( text , 1 , 4 ) ) ~= nil then
            d = tonumber ( string.sub ( note , 1 , 4 ) );
            text = string.sub ( text , 6 );
        end
        if d ~= nil then
            return { bucketNum , d , text };
        else
            return nil;
        end
    end

    for i = 1 , #separators do
        if string.find ( note , separators[i] ) ~= nil then
            result = GetBucket ( note , separators[i] , i );
            if result ~= nil then
                break;
            end
        end
    end

    return result;
end

-- Method:          GRM.DatesDoMatch ( array , string )
-- What it Does:    Returns true of the parsed date and the set date are the same, otherwise returns false
-- Purpose:         So player cna easily flag inconsistencies in the notes and correct them.
GRM.DatesDoMatch = function ( dates , name )
    local result = false;
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][GRM_G.guildName][name];

    if player.verifiedJoinDate[1] ~= "" then
        local dateSet = player.verifiedJoinDate[1];
        local noteStamp = GRM.GetDefaultTimestampUsingIntValues ( dates[1] , dates[2] , dates[3] );
        if noteStamp == string.sub ( dateSet , 1 , string.find ( dateSet , "'" ) + 2 ) then
            result = true;
        end    
    end
    return result
end

-- Method:          GRM.GetParsedDate ( string )
-- What it Does:    Algorithm for combining all pieces of the puzzle into attempting to parse out the date properly, and returns the { day, month , year } if valid
--                  Otherwise it returns nil;
-- Purpose:         Assist in controlling the overall logic of parsing the date into a workable format to reverse engineer the date stored in the notes.
GRM.GetParsedDate = function( note )
    local temp, temp2 = {};
    local result;

    temp = GRM.NoteHasJoinTag ( note );
    if temp[1] then
        note = temp[2];
    end
    note = GRM.CleanupNoteDate ( note );
    temp2 = GRM.GetFormattedDatesFromMonthStrFirstNote ( note );
    if temp2 == nil then
        temp2 = GRM.GetNoteParsingBucket ( note );
        if temp2 ~= nil then
            result = GRM.SetParseBucketLogic ( temp2 )
        end
    else
        result = temp2;
    end
    return result;
end

-- Method:          GRM.SetParseBucketLogic ( array )
-- What it Does:    Determines which function to use to parse the string further
-- Purpose:         To control the parsing more orderly.
GRM.SetParseBucketLogic = function ( noteDetails )
    local parsingFunctions = { GRM.ParseBucket1 , GRM.ParseBucket2 , GRM.ParseBucket3 , GRM.ParseBucket4 };
    local result , restOfString = parsingFunctions[noteDetails[1]]( noteDetails[2] , noteDetails[3] ); 

    return result , restOfString;
end

-- Method:          GRM.GetParsedYear ( string )
-- What it Does:    Returns the year as an int parsed from the end of the string date formats
-- Purpose:         To avoid reusing too much code and just referencing this.
GRM.GetParsedYear = function ( note )
    local year;
    local restOfString = "";

    if string.sub ( note , 1 , 1 ) == "'" then
        year = tonumber ( string.sub ( note , 2 , 3 ) );
        restOfString = string.sub ( note , 4 );
    else
        year = tonumber ( string.sub ( note , 1 , 4 ) );
        restOfString = string.sub ( note , 5 );
        if year == nil then
            year = tonumber ( string.sub ( note , 1 , 2 ) );
            restOfString = string.sub ( note , 3 );
        elseif GRM.IsValidYear ( year - 2000 ) then
            year = year - 2000;
        end
    end

    return year , restOfString;
end

-- BUCKETS TO PARSE FURTHER - Based on date format dropdown selection in the options

-- Bucket 1 = 1,2
-- Method:          GRM.ParseBucket1 ( int , string )
-- What it Does:    Parses out the timestamp into an array of ints representing the day, month, and year
-- Purpose:         For parsing the notes properly
GRM.ParseBucket1 = function( day , note )
    local mos, year;
    local result;
    local restOfString;

    if string.find ( note , " " ) ~= nil then
        mos = monthEnum[ GRM.OrigL ( string.sub ( note , 1 , string.find ( note , " " ) -1 ) ) ];
        note = string.sub ( note , string.find ( note , " " ) + 1 );
    end

    if mos ~= nil then
        year , restOfString = GRM.GetParsedYear ( note );
    end

    if year ~= nil and day ~= nil and GRM.IsValidYear ( year ) and GRM.IsValidDay ( day , mos , year ) then
        result = { day , mos , year };
    end
    return result , restOfString;
end

-- Bucket 2 = 3,4,11,15
-- Method:          GRM.ParseBucket2 ( int , string )
-- What it Does:    Parses out the timestamp into an array of ints representing the day, month, and year
-- Purpose:         For parsing the notes properly
GRM.ParseBucket2 = function( day , note )
    local mos, year;
    local result;
    local restOfString;

    if string.find ( note , "-" ) ~= nil then
        if day < 2000 then
            local middleVal = string.sub ( note , 1 , string.find ( note , "-" ) - 1 );
            note = string.sub ( note , string.find ( note , "-" ) + 1 );

            if tonumber ( middleVal ) ~= nil then
                -- 4,11
                -- Determine preferred format
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].dateFormat < 8 then
                    -- assume it is 4
                    mos = tonumber ( middleVal );
                    if not GRM.IsValidMonth ( mos ) and mos ~= nil and mos > 12 then    -- Swap if necessary
                        mos = day;
                        day = tonumber ( middleVal );
                    end
                else
                    -- assume it is 11
                    mos = day;
                    day = tonumber ( middleVal );

                    -- Extra redundancy in case preferred format isn't accurate, you can catch many cases where month > 13 thus you know it should be reversed.
                    if not GRM.IsValidMonth ( mos ) and mos ~= nil and mos > 12 then
                        day = mos;
                        mos = tonumber ( middleVal );
                    end
                end

            else -- 3
                mos = monthEnum[ GRM.OrigL ( middleVal ) ];
            end

            if mos ~= nil then
                -- Now find the year
                year , restOfString = GRM.GetParsedYear ( note );
            end
        else
            -- 15
            year = day - 2000;
            day = nil; -- reset the value
            -- now need to find mos and day
            mos = tonumber ( string.sub ( note , 1 , string.find ( note , "-" ) - 1 ) );
            if GRM.IsValidMonth ( mos ) then
                note = string.sub ( note , string.find ( note , "-" ) + 1 );
                day = tonumber ( string.sub ( note , 1 , 2 ) );
                restOfString = string.sub ( note , 3 );
                if day == nil then
                    day = tonumber ( string.sub ( note , 1 , 1 ) );
                    restOfString = string.sub ( note , 2 );
                end
            end
        end
    end

    if year ~= nil and mos ~= nil and day ~= nil and GRM.IsValidYear ( year ) and GRM.IsValidDay ( day , mos , year ) then -- need both month and year to not be nil this time due to flip flopping formats with countries or players who like year first
        result = { day , mos , year };
    end
    return result , restOfString;
end

-- Bucket 3 = 5,12
-- Method:          GRM.ParseBucket3 ( int , string )
-- What it Does:    Parses out the timestamp into an array of ints representing the day, month, and year
-- Purpose:         For parsing the notes properly
GRM.ParseBucket3 = function( day , note )
    local mos, year;
    local result;
    local restOfString = "";

    if string.find ( note , "/" ) ~= nil then
        local middleVal = tonumber ( string.sub ( note , 1 , string.find ( note , "/" ) -1 ) );
        note = string.sub ( note , string.find ( note , "/" ) + 1 );

        if middleVal ~= nil then
            -- Determine preferred format
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].dateFormat < 8 then
                -- assume it is 5
                mos = tonumber ( middleVal );
                if not GRM.IsValidMonth ( mos ) and mos ~= nil and mos > 12 then    -- Swap if necessary
                    mos = day;
                    day = middleVal;
                end
            else
                -- assume it is 12
                mos = day;
                day = tonumber ( middleVal );

                -- Extra redundancy in case preferred format isn't accurate, you can catch many cases where month > 13 thus you know it should be reversed.
                if not GRM.IsValidMonth ( mos ) and mos ~= nil and mos > 12 then
                    day = mos;
                    mos = tonumber ( middleVal );
                end
            end
        end

        if mos ~= nil then
            year , restOfString = GRM.GetParsedYear ( note );
        end
    end

    if year ~= nil and day ~= nil and GRM.IsValidYear ( year ) and GRM.IsValidDay ( day , mos , year ) then
        result = { day , mos , year };
    end
    return result , restOfString;
end

-- Bucket 4 = 6,7,13,14
-- Method:          GRM.ParseBucket4 ( int , string )
-- What it Does:    Parses out the timestamp into an array of ints representing the day, month, and year
-- Purpose:         For parsing the notes properly
GRM.ParseBucket4 = function( day , note)
    local mos, year;
    local result;
    local restOfString = "";

    if string.find ( note , "%." ) ~= nil then
        local middleVal = tonumber ( string.sub ( note , 1 , string.find ( note , "%." ) -1 ) );
        note = string.sub ( note , string.find ( note , "%." ) + 1 );

        if middleVal ~= nil then
            -- Determine preferred format
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].dateFormat < 8 then
                -- assume it is 6,7
                mos = tonumber ( middleVal );
                if not GRM.IsValidMonth ( mos ) and mos ~= nil and mos > 12 then    -- Swap if necessary
                    mos = day;
                    day = middleVal;
                end
            else
                -- assume it is 13,14
                mos = day;
                day = tonumber ( middleVal );

                -- Extra redundancy in case preferred format isn't accurate, you can catch many cases where month > 13 thus you know it should be reversed.
                if not GRM.IsValidMonth ( mos ) and mos ~= nil and mos > 12 then
                    day = mos;
                    mos = tonumber ( middleVal );
                end
            end
        end

        if mos ~= nil then
            year , restOfString = GRM.GetParsedYear ( note );
        end
    end

    if year ~= nil and mos ~= nil and day ~= nil and GRM.IsValidYear ( year ) and GRM.IsValidDay ( day , mos , year ) then -- need both month and year to not be nil this time due to flip flopping formats with countries or players who like year first
        result = { day , mos , year };
    end

    return result , restOfString;
end

-----------------------
-- AUDIT TOOL FRAMES --
-----------------------

-- Method:          GRM.AuditRefresh()
-- What it Does:    Refreshes and updates all the frames in the audit JD tool
-- Purpose:         Easy refresh management for UX
GRM.AuditRefresh = function( fullRefresh )   
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].JDAuditToolFilter then
        GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolCheckBox:SetChecked( true );
    end
    
    GRM.RefreshJDAuditToolFrames ( not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].JDAuditToolFilter , fullRefresh );
end

-- Method:          GRM.IsAnyHighlighted()
-- What it Does:    Returns true if at least one button is highlighted
-- Purpose:         Use as boolean gate
GRM.IsAnyHighlighted = function()
    local result = false;

    for i = 1 , #GRM_G.AuditToolGuildies do
        if GRM_G.AuditToolGuildies[i][6] then
            result = true;
            break;
        end
    end

    return result;
end

-- Method:          GRM.GetNoteName()
-- What it Does:    Returns the string note name of the configured default destination for the join date
-- Purpose:         Cleaner code is all
GRM.GetNoteName = function ()
    local result = "";
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 1 then
       result = GRM.L ( "Officer Note" );
    elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 2 then
        result = GRM.L ( "Public Note" );
    else
        result = GRM.L ( "Custom Note" );
    end
    return result;
end

-- Method:          GRM.GetCountNeedingAttention ( bool )
-- What it Does:    Gets the number of players in the audit that still need to be dealt with
-- Purpose:         For UI info reporting to the player.
GRM.GetCountNeedingAttention = function ( showAll )
    local count = #GRM_G.AuditToolGuildies;
    if showAll then
        count = 0;
        for i = 1 , #GRM_G.AuditToolGuildies do
            if GRM_G.AuditToolGuildies[i][4] > 0 then
                count = count + 1;
            end
        end
    end

    return count;
end

-- Method:          GRM.RefreshJDAuditToolsTextFrames ( bool )
-- What it Does:    Rechecks that the button text matches the logic
-- Purpose:         Quality controls on UX
GRM.RefreshJDAuditToolsTextFrames = function( showAll )
    local count = GRM.GetCountNeedingAttention ( showAll );

    if count > 0 then
        GRM_UI.GRM_AuditJDTool.GRM_AuditToolText6:SetText ( GRM.L ( "{num} Join Dates Need Attention" , nil , nil , count ) );
        GRM_UI.GRM_AuditJDTool.GRM_AuditToolText6:SetTextColor ( 0.64 , 0.102 , 0.102 );
    else
        GRM_UI.GRM_AuditJDTool.GRM_AuditToolText6:SetText ( GRM.L ( "All Complete" , nil , nil , count ) );
        GRM_UI.GRM_AuditJDTool.GRM_AuditToolText6:SetTextColor ( 0 , 0.8 , 1.0 );
    end

    if GRM.CanEditOfficerNote() then
        GRM_UI.GRM_AuditJDTool.GRM_AuditToolText7:Hide();
    else
        GRM_UI.GRM_AuditJDTool.GRM_AuditToolText7:Show();
    end

    local isHighlighted = GRM.IsAnyHighlighted();

    if not isHighlighted then
        GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton4Text:SetText ( GRM.L ( "Fix all mismatched GRM dates with the note date" ) );
        GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton3Text:SetText ( GRM.L ( "Fix all mismatched note dates with GRM saved date" ) );
        GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton2Text:SetText ( GRM.L ( "Import all missing join dates from the note date" ) );
        GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton1Text:SetText ( GRM.L ( "Add all missing saved dates to the {name}" , GRM.GetNoteName() ) );
        GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton7Text:SetText ( GRM.L ( "Clear all join dates from incorrect note locations" ) );
    else
        GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton4Text:SetText ( GRM.L ( "Fix selected mismatched GRM dates with the note date" ) );
        GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton3Text:SetText ( GRM.L ( "Fix selected mismatched note dates with GRM saved date" ) );
        GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton2Text:SetText ( GRM.L ( "Import selected missing join dates from the note date" ) );
        GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton1Text:SetText ( GRM.L ( "Add selected missing saved dates to {name}" , GRM.GetNoteName() ) );
        GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton7Text:SetText ( GRM.L ( "Clear selected join dates from incorrect note locations" ) );
    end
end

-- Method:          GRM.RefreshJDAuditToolFrames ()
-- What it Does:    Builds the scroll frame for the audit tools
-- Purpose:         Quality of Life management feature
GRM.RefreshJDAuditToolFrames = function ( showAll , fullRefresh )
    local hybridScrollFrameButtonCount = 13;
    local buttonHeight = 25;
    local scrollHeight = 0;
    local buttonWidth = GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollFrame:GetWidth() - 5;
    if fullRefresh then
        GRM_G.AuditToolGuildies = GRM.CheckAllDates( showAll );
        GRM_G.JDAuditToolLastSelection = "";
        GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.Offset = hybridScrollFrameButtonCount;
        GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollFrameSlider:SetValue ( 0 );  
    end

    -- Just refreshes the frames
    GRM.RefreshJDAuditToolsTextFrames ( showAll );

    GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons = GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons or {};  -- Create a table for the Buttons.
    GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.Offset = GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.Offset or ( hybridScrollFrameButtonCount );

    if GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.Offset < hybridScrollFrameButtonCount then
        GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.Offset = hybridScrollFrameButtonCount;
    elseif GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.Offset > hybridScrollFrameButtonCount and GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.Offset > #GRM_G.AuditToolGuildies then
        GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.Offset = #GRM_G.AuditToolGuildies;
    end

    for i = 1 , #GRM_G.AuditToolGuildies do
        -- Build HybridScrollFrame Buttons
        if i <= hybridScrollFrameButtonCount then
            if not GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[i] then
                local button = CreateFrame ( "Button" , "JDAuditButton" .. i .. "_" .. 1, GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame );
                GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[i] = { button , button:CreateFontString ( "JDAuditButtonText" .. i .. "_" .. 1 , "OVERLAY" , "GameFontWhiteTiny" ),
                                                                                                    button:CreateFontString ( "JDAuditButtonText" .. i .. "_" .. 2 , "OVERLAY" , "GameFontWhiteTiny" ),
                                                                                                    button:CreateFontString ( "JDAuditButtonText" .. i .. "_" .. 3 , "OVERLAY" , "GameFontWhiteTiny" ),
                                                                                                    button:CreateFontString ( "JDAuditButtonText" .. i .. "_" .. 4 , "OVERLAY" , "GameFontWhiteTiny" ),
                                                                                                    button:CreateFontString ( "JDAuditButtonText" .. i .. "_" .. 5 , "OVERLAY" , "GameFontWhiteTiny" )
                };
                button = GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[i][1];
                if i == 1 then
                    button:SetPoint ( "TOP" , GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame , "TOP" , -1 , 0 );
                else 
                    button:SetPoint ( "TOPLEFT" , GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[i-1][1] , "BOTTOMLEFT" , 0 , 0 );
                end

                GRM.BuildJDToolHybridButtons ( i );

                button:SetSize ( GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollFrame:GetWidth() - 7 , buttonHeight );
                button:SetHighlightTexture ( "Interface\\Buttons\\UI-Panel-Button-Highlight" );
            end
        end

        if i >= ( GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.Offset - hybridScrollFrameButtonCount + 1 ) and i <= GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.Offset then
            GRM.SetJDAuditValues ( i - ( GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.Offset - hybridScrollFrameButtonCount ) , i );
            GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[i - ( GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.Offset - hybridScrollFrameButtonCount )][1]:Show();
        end
        
        -- Slider Height is controlled by tallying how many of these are necessary
        scrollHeight = scrollHeight + buttonHeight;
    end
    -- Hide unused buttons...
    for i = #GRM_G.AuditToolGuildies + 1 , #GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons do
        GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[i][1]:Hide();
    end

    GRM.SetHybridScrollFrameSliderParameters ( 
        GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame , GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollFrame , GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollFrameSlider , 
        buttonWidth , buttonHeight , scrollHeight , #GRM_G.AuditToolGuildies , GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons , 
        GRM.JDAuditToolHybridShiftDown , GRM.JDAuditToolHybridShiftUp , hybridScrollFrameButtonCount
    );
end

-- Method:          GRM.BuildJDToolHybridButtons ( int )
-- What it Does:    Initiates the buttons and logic for the JD Audit tool buttons
-- Purpose:         Compartmentalize the code for easier call back to.
GRM.BuildJDToolHybridButtons = function ( ind , isResizeAction )

    local button1 = GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][1];
    local buttonText1 = GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][2];
    local buttonText2 = GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][3];
    local buttonText3 = GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][4];
    local buttonText4 = GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][5];
    local buttonText5 = GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][6];

    
    buttonText1:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 11 );
    buttonText1:SetJustifyH ( "LEFT" );
    buttonText1:SetWidth ( 115 )
    buttonText1:SetPoint ( "LEFT" , button1 , 10 , 0 );

    buttonText2:SetPoint ( "LEFT" , buttonText1 , "RIGHT" , 10 , 0 );
    buttonText2:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 11 );
    buttonText2:SetWidth ( 100 );
    buttonText2:SetJustifyH ( "CENTER" );

    buttonText3:SetPoint ( "LEFT" , buttonText2 , "RIGHT" , 10 , 0 );
    buttonText3:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 11 );
    buttonText3:SetWidth ( 100 );
    buttonText3:SetJustifyH ( "CENTER" );

    buttonText4:SetPoint ( "LEFT" , buttonText3 , "RIGHT" , 0 , 0 );
    buttonText4:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 11 );
    buttonText4:SetWidth ( 100 );
    buttonText4:SetJustifyH ( "CENTER" );

    buttonText5:SetPoint ( "LEFT" , buttonText4 , "RIGHT" , 10 , 0 );
    buttonText5:SetFont ( GRM_G.FontChoice , GRM_G.FontModifier + 11 );
    buttonText5:SetWidth ( 205 );
    buttonText5:SetJustifyH ( "LEFT" );

    if not isResizeAction then
        button1:EnableMouse ( true );
        button1:RegisterForDrag ( "LeftButton" );

        button1:SetScript ( "OnDragStart" , function()
            GRM_UI.GRM_AuditJDTool:StartMoving();
        end);
        button1:SetScript ( "OnDragStop" , function()
            GRM_UI.GRM_AuditJDTool:StopMovingOrSizing()
        end);

        -- Set button logic...
        button1:SetScript ( "OnClick" , function ( self , key )
            if key == "LeftButton" then
                for j = 1 , #GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons do
                    if self == GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[j][1] then
                        if not IsShiftKeyDown() or GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[j][2]:GetText() == GRM_G.JDAuditToolLastSelection then
                            GRM_G.JDAuditToolLastSelection = GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[j][2]:GetText();
                            -- Identify the player in the GRM_G.AuditToolGuildies
                            for k = 1 , #GRM_G.AuditToolGuildies do
                                if GRM_G.AuditToolGuildies[k][1] == GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[j][2]:GetText() then

                                    if IsControlKeyDown() then
                                        if IsShiftKeyDown() and IsControlKeyDown() then
                                            GRM.SearchPlayerInLog ( GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[j][2]:GetText() );
                                        else
                                            GRM.OpenPlayerWindow ( GRM_G.AuditToolGuildies[k][1] );
                                        end
                                    else
                                        if GRM_G.AuditToolGuildies[k][6] then
                                            self:UnlockHighlight();
                                            GRM_G.AuditToolGuildies[k][6] = false;
                                        else
                                            self:LockHighlight();
                                            GRM_G.AuditToolGuildies[k][6] = true;
                                        end
                                        GRM.AuditRefresh ( false );
                                    end
                                    break;
                                end
                            end
                        -- For mass Shift-Click selection.
                        elseif GRM_G.JDAuditToolLastSelection ~= "" then
                            if IsShiftKeyDown() and IsControlKeyDown() then
                                GRM.SearchPlayerInLog ( GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[j][2]:GetText() );
                            else
                                local count = 0;
                                local currentSelec = 0;
                                local prevSelec = 0;

                                for k = 1 , #GRM_G.AuditToolGuildies do
                                    if GRM_G.AuditToolGuildies[k][1] == GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[j][2]:GetText() then
                                        count = count + 1;
                                        currentSelec = k;
                                    elseif GRM_G.AuditToolGuildies[k][1] == GRM_G.JDAuditToolLastSelection then
                                        count = count + 1;
                                        prevSelec = k
                                    end

                                    if count == 2 then
                                        break;
                                    end
                                end
                                local diff = 0;
                                if currentSelec > prevSelec then
                                    diff = currentSelec - prevSelec;
                                    for k = currentSelec , prevSelec , - 1 do
                                        GRM_G.AuditToolGuildies[k][6] = true;
                                    end

                                    if j > diff then
                                        diff = j-diff;
                                    else
                                        diff = 1;
                                    end

                                    -- Lock the buttons' Highlights
                                    for s = j , diff , -1 do
                                        GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[s][1]:LockHighlight();
                                    end
                                    
                                else
                                    diff = prevSelec - currentSelec;
                                    for k = currentSelec , prevSelec do
                                        GRM_G.AuditToolGuildies[k][6] = true;
                                    end

                                    if j + diff > #GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons then
                                        diff = #GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons;
                                    else
                                        diff = diff + j;
                                    end

                                    -- Lock the buttons' Highlights
                                    for s = j , diff do
                                        GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[s][1]:LockHighlight();
                                    end
                                end
                                GRM.AuditRefresh ( false );
                            end
                        end
                        break;
                    end
                end
            end
        end);

        -- Tooltips
        button1:SetScript ( "OnEnter" , function ()
            GRM_UI.SetTooltipScale();
            GRM.UpdateJDAuditTooltip( ind );
            GameTooltip:Show();
        end);
        button1:SetScript ( "OnLeave" , function ()
            GRM.RestoreTooltip();
        end);
    end

end

-- Method:          GRM.UpdateJDAuditTooltip ( int )
-- What it Does:    Refreshes the JD Audit Tool Tooltip
-- Purpose:         So, on scrolling, the tooltip easily updates on the fly as well.
GRM.UpdateJDAuditTooltip = function ( ind )
    GameTooltip:SetOwner ( GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][1] , "ANCHOR_CURSOR" );
    GameTooltip:AddLine ( GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][2]:GetText() , GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][2]:GetTextColor() );
    GameTooltip:AddLine ( GRM.L ( "|CFFE6CC7FClick|r to select player" ) );
    GameTooltip:AddLine ( GRM.L ( "|CFFE6CC7FShift-Click|r Second Button to Select All In-Between" ) );
    GameTooltip:AddLine ( GRM.L ( "|CFFE6CC7FCtrl-Click|r to open Player Window" ) );
    GameTooltip:AddLine ( GRM.L ( "|CFFE6CC7FCtrl-Shift-Click|r to Search the Log for Player" ) );
end

-- Method:          GRM.JDAuditToolHybridShiftDown()
-- What it Does:    Shifts the buttons up one spot for hybrid scrollframe purpose
-- Purpose:         To keep from having to refresh the entire frames over and over. Now, it just shifts everything over once. Fast and lightweight for scrollwheel control
GRM.JDAuditToolHybridShiftDown = function ()
    local buttons = GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons;
    local MouseOverButton = 0;
    -- Shift them down...
    for i = 1 , #buttons - 1 do
        buttons[i][2]:SetText( buttons[i+1][2]:GetText() );
        buttons[i][2]:SetTextColor ( buttons[i+1][2]:GetTextColor ( buttons[i+1][2]:GetText() ) );
        buttons[i][3]:SetText( buttons[i+1][3]:GetText() );
        buttons[i][3]:SetTextColor ( buttons[i+1][3]:GetTextColor ( buttons[i+1][3]:GetText() ) );
        buttons[i][4]:SetText( buttons[i+1][4]:GetText() );
        buttons[i][4]:SetTextColor ( buttons[i+1][4]:GetTextColor ( buttons[i+1][4]:GetText() ) );
        buttons[i][5]:SetText( buttons[i+1][5]:GetText() );
        buttons[i][5]:SetTextColor ( buttons[i+1][5]:GetTextColor ( buttons[i+1][5]:GetText() ) );
        buttons[i][6]:SetText( buttons[i+1][6]:GetText() );
        buttons[i][6]:SetTextColor ( buttons[i+1][6]:GetTextColor ( buttons[i+1][6]:GetText() ) );
        -- Identify the player in the guildies
        for k = 1 , #GRM_G.AuditToolGuildies do
            if GRM_G.AuditToolGuildies[k][1] == buttons[i+1][2]:GetText() then
                if GRM_G.AuditToolGuildies[k][6] then
                    buttons[i][1]:LockHighlight();
                else
                    buttons[i][1]:UnlockHighlight();
                end
                break;
            end
        end

        if MouseOverButton == 0 and buttons[i][1]:IsMouseOver() then
            MouseOverButton = i;
        end
    end
    GRM.JDAuditSetLastValue();
    if MouseOverButton > 0 then
        GRM.UpdateJDAuditTooltip( MouseOverButton );
    end
end

-- Method:          GRM.JDAuditToolHybridShiftUp()
-- What it Does:    Shifts the buttons up one spot for hybrid scrollframe purpose
-- Purpose:         To keep from having to refresh the entire frames over and over. Now, it just shifts everything over once. Fast and lightweight for scrollwheel control
GRM.JDAuditToolHybridShiftUp = function ()
    local buttons = GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons;
    local MouseOverButton = 0;
    
    -- Shift them down...
    for i = #buttons , 2 , -1 do
        buttons[i][2]:SetText( buttons[i-1][2]:GetText() );
        buttons[i][2]:SetTextColor ( buttons[i-1][2]:GetTextColor ( buttons[i-1][2]:GetText() ) );
        buttons[i][3]:SetText( buttons[i-1][3]:GetText() );
        buttons[i][3]:SetTextColor ( buttons[i-1][3]:GetTextColor ( buttons[i-1][3]:GetText() ) );
        buttons[i][4]:SetText( buttons[i-1][4]:GetText() );
        buttons[i][4]:SetTextColor ( buttons[i-1][4]:GetTextColor ( buttons[i-1][4]:GetText() ) );
        buttons[i][5]:SetText( buttons[i-1][5]:GetText() );
        buttons[i][5]:SetTextColor ( buttons[i-1][5]:GetTextColor ( buttons[i-1][5]:GetText() ) );
        buttons[i][6]:SetText( buttons[i-1][6]:GetText() );
        buttons[i][6]:SetTextColor ( buttons[i-1][6]:GetTextColor ( buttons[i-1][6]:GetText() ) );
        -- Identify the player in the guildies
        for k = 1 , #GRM_G.AuditToolGuildies do
            if GRM_G.AuditToolGuildies[k][1] == buttons[i-1][2]:GetText() then
                if GRM_G.AuditToolGuildies[k][6] then
                    buttons[i][1]:LockHighlight();
                else
                    buttons[i][1]:UnlockHighlight();
                end
                break;
            end
        end

        if MouseOverButton == 0 and buttons[i][1]:IsMouseOver() then
            MouseOverButton = i;
        end
    end
    GRM.JDAuditSetFirstValue();
    if MouseOverButton > 0 then
        GRM.UpdateJDAuditTooltip( MouseOverButton );
    end
end

-- Method           GRM.SetJDAuditValues ( int , int )
-- What it Does:    Sets the values of the fontstrings for each button
-- Purpose:         Build the JD Audit Tool UI
GRM.SetJDAuditValues = function ( ind , ind2 )
    local name = GRM_G.AuditToolGuildies[ind2][1];
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];
    local classColor = GRM.GetClassColorRGB ( player.class , false )
    local grmDate = "";
    local noteDate = "";
    local status = "";
    local loc = "";
    local noteLoc = { GRM.L ( "Officer Note" ) , GRM.L ( "Public Note" ) , GRM.L ( "Custom Note" ) , GRM.L ( "Multiple" ) };
    local reasons = { 
        GRM.L ( "Mismatched dates" ) , GRM.L ( "Matching date found in wrong note location" ) , GRM.L ( "Date not added to note" ) , 
        GRM.L ( "Mismatched dates, and found in wrong note location" ) , GRM.L ( "Mismatched date found in multiple locations, including correct" ) , GRM.L ( "Mismatched date found in multiple incorrect note locations" ) ,
        GRM.L ( "Matching date found in multiple incorrect note locations" ) , GRM.L ( "Matching date found in multiple locations, including correct" ) , GRM.L ( "Date found in incorrect note, but not yet imported" ) , 
        GRM.L ( "Date found in multiple incorrect notes, but not yet imported" ) , GRM.L ( "Date found in multiple locations, including correct, but not yet imported" ) , GRM.L ( "Date found in correct note location, but not yet imported" )
     };

    -- Text Coloring
    local complete = { 0 , 0.77 , 0.063 };
    local Incomplete = { 0.64 , 0.102 , 0.102 };
    local badNote = { 1.0 , 0.647 , 0 };
    local normN = { 1 , 1 , 1 };
    local noteLocC , statusC , noteF , dateSetF = {} , {} , {} , {};


    if player.verifiedJoinDate[1] ~= "" then
        grmDate = GRM.FormatTimeStamp ( player.verifiedJoinDate[1] );
        dateSetF = normN;
    else
        grmDate = GRM.L ( "Not Set" );
        dateSetF = Incomplete;
    end

    if GRM_G.AuditToolGuildies[ind2][3] == 0 then
        noteDate = GRM.L ( "Not Found" );
        loc = noteDate;
        noteLocC = badNote;
        noteF = Incomplete;
    else
        noteDate = GRM.FormatTimeStamp ( { GRM_G.AuditToolGuildies[ind2][2][1] , GRM_G.AuditToolGuildies[ind2][2][2] , GRM_G.AuditToolGuildies[ind2][2][3] } , false , false );
        noteF = normN;
        loc = noteLoc[GRM_G.AuditToolGuildies[ind2][3]];
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == GRM_G.AuditToolGuildies[ind2][3] then
            noteLocC = complete;
        else
            noteLocC = badNote;
        end
    end
    
    if GRM_G.AuditToolGuildies[ind2][4] > 0 then
        status = reasons [ GRM_G.AuditToolGuildies[ind2][4] ]
        statusC = Incomplete;
    else
        status = GRM.L ( "Complete" );
        statusC = complete;
    end

    GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][2]:SetText ( name );
    GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][2]:SetTextColor ( classColor[1] , classColor[2] , classColor[3] );
    GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][3]:SetText ( grmDate );
    GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][3]:SetTextColor ( dateSetF[1] , dateSetF[2] , dateSetF[3] );
    GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][4]:SetText ( noteDate );
    GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][4]:SetTextColor ( noteF[1] , noteF[2] , noteF[3] );
    GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][5]:SetText ( loc );
    GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][5]:SetTextColor ( noteLocC[1] , noteLocC[2] , noteLocC[3] );
    GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][6]:SetText ( status );
    GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][6]:SetTextColor ( statusC[1] , statusC[2] , statusC[3] );

    if GRM_G.AuditToolGuildies[ind2][6] then
        GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][1]:LockHighlight();
    else
        GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][1]:UnlockHighlight();
    end

    -- Update the tooltip if underlying data changes
    if GameTooltip:IsVisible() and GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons[ind][1]:IsMouseOver() then 
        GRM.UpdateJDAuditTooltip ( ind )
    end
end

-- Method:          GRM.JDAuditSetLastValue()
-- What it Does:    Triggers the setting fo all values at index 13 of the JD Audit tool
-- Purpose:         On the single line shift, you end up with 1 on the edge that needs to be built
GRM.JDAuditSetLastValue = function()
    GRM.SetJDAuditValues ( #GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons , GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.Offset );
end

-- Method:          GRM.JDAuditSetFirstValue()
-- What it Does:    Triggers the setting fo all values at index 1 of the JD Audit tool
-- Purpose:         On the single line shift, you end up with 1 on the edge that needs to be built
GRM.JDAuditSetFirstValue = function()
    GRM.SetJDAuditValues ( 1 , GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.Offset - #GRM_UI.GRM_AuditJDTool.GRM_JDToolScrollChildFrame.AllButtons + 1 );
end

-- Method:          GRM.ConvertGRMDateFromNote ( array )
-- What it Does:    Initiates the flow of editing the GRM dates using the notes as reference, and refreshes the frames
-- Purpose:         For mass use in the JD Audit tool
GRM.ConvertGRMDateFromNote = function ( player )
    -- Set Join Date
    GRM.EditJoinDateManually ( player[1] , player[2][1] , player[2][2] , player[2][3] );
end

-- Method:          GRM.ConvertNoteFromGRM ( array )
-- What it Does:    Initiates the flow of editing the GRM dates using the notes as reference, and refreshes the frames
-- Purpose:         For mass use in the JD Audit tool
GRM.ConvertNoteFromGRM = function ( player )
    GRM.EditSavedNoteDateManually ( player );
end

-- Method:          GRM.EditJoinDateManually ( string , int , int , int )
-- What it Does:    changes the player's GRM join date to match the note join date.
-- Purpose:         Useful mini join date tool to cycle mass update join dates for guildies.
GRM.EditJoinDateManually = function ( name , day , month , year )
    if year < 10 then
        year = "0" .. tostring ( year );
    end
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][name];
    local joinDate = ( "Joined: " .. day .. " " .. monthEnum2[ tostring ( month ) ] .. " '" ..  year );
    local finalTStamp = ( string.sub ( joinDate , 9 ) .. " 12:01am" );
    local finalEpochStamp = GRM.TimeStampToEpoch ( joinDate , true );
    
    if player ~= nil then
        if #player.joinDate == 0 then
            table.insert ( player.joinDate , finalTStamp );
            table.insert ( player.joinDateEpoch , finalEpochStamp )
        else
            player.joinDate[#player.joinDate] = finalTStamp;
            player.joinDateEpoch[#player.joinDateEpoch] = finalEpochStamp;
        end

        -- For Sync
        player.verifiedJoinDate[1] = finalTStamp;
        player.verifiedJoinDate[2] = time();

        -- If it was unKnown before
        player.joinDateUnknown = false;

        -- Gotta update the event tracker date too for anniversary!!!
        local date = GRM.ConvertGenericTimestampToIntValues ( string.sub ( joinDate , 9 ) );
        player.events[1][1][1] = date[1];
        player.events[1][1][2] = date[2];
        player.events[1][1][3] = date[3];
        player.events[1][2] = false;  -- Gotta Reset the "reported already" boolean!
        GRM.RemoveFromCalendarQue ( player.name , 1 , nil );

    end

    -- Refresh the dates on the frames!!!
    if GRM_UI.GRM_MemberDetailMetaData:IsVisible() and GRM_G.currentName == player.name then
        if GRM_UI.GRM_MemberDetailMetaData.GRM_SetUnknownButton:IsVisible() or GRM_UI.GRM_MemberDetailMetaData.GRM_SyncJoinDateSideFrame:IsVisible() or GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame:IsVisible() then
            GRM.ClearAllFrames( false );
        end
        GRM.PopulateMemberDetails ( GRM_G.currentName );
    end
end

-- Method:          GRM.EditSavedNoteDateManually ( string , int )
-- What it Does:    changes the player's note date to match the GRM save date.
-- Purpose:         Useful mini join date tool to cycle mass update join dates for guildies.
GRM.EditSavedNoteDateManually = function ( member )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][member[1]];
    local tempNote = "";
    local name = "";
    local index;
    local success = false;
    local noteHeader = "";
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeTag then
        noteHeader = GRM_G.customHeaderJoin;
    end
    local finalNote = "";
    
    if player ~= nil then
        GRM_G.changeHappenedExitScan = true;
        index = nil;
        name = "";
        tempNote = "";

        -- Find them in the guild indexes to modify offier/public notes
        for h = 1 , GRM.GetNumGuildies() do
            name = GetGuildRosterInfo( h );
            if name == member[1] then
                index = h;
                break;
            end
        end

        -- Set the repeated note formatting to be added to note.
        finalNote = noteHeader .. " " .. GRM.FormatTimeStamp ( player.joinDate[#player.joinDate] , false , false ) .. " ";

        -- Modify the notes
        if member[3] == 4 then       -- if true, multiple locations
            if GRM.CanViewOfficerNote() then
                tempNote , success = GRM.RemoveDateFromNote ( player.officerNote );
                if success then
                    -- yes, it was modified
                    finalNote = ( GRM.Trim ( finalNote .. tempNote ) );
                    if #finalNote <= GRM_G.MaxNoteChars then                    -- To avoid errors need to add protections against trying to add > 31 chars.
                        player.officerNote = finalNote;
                        GuildRosterSetOfficerNote ( index , player.officerNote );
                    else
                        finalNote = ( GRM.Trim ( GRM.FormatTimeStamp ( player.joinDate[#player.joinDate] , false , false ) ) .. " " .. tempNote );    -- Remove header, try adding again.
                        if #finalNote <= GRM_G.MaxNoteChars then
                            player.officerNote = finalNote;
                            GuildRosterSetOfficerNote ( index , player.officerNote );
                        end
                    end
                    success = false;
                end
            end
            if ( GRM.CanViewOfficerNote() or player.name == GRM_G.addonUser ) then
                tempNote , success = GRM.RemoveDateFromNote ( player.note );
                if success then
                    -- yes, it was modified
                    finalNote = ( GRM.Trim ( finalNote .. tempNote ) );
                    if #finalNote <= GRM_G.MaxNoteChars then                    -- To avoid errors need to add protections against trying to add > 31 chars.
                        player.note = finalNote;
                        GuildRosterSetPublicNote ( index , player.note );
                    else
                        finalNote = ( GRM.Trim ( GRM.FormatTimeStamp ( player.joinDate[#player.joinDate] , false , false ) ) .. " " .. tempNote );    -- Remove header, try adding again.
                        if #finalNote <= GRM_G.MaxNoteChars then
                            player.note = finalNote;
                            GuildRosterSetPublicNote ( index , player.note );
                        end
                    end
                    success = false;
                end
            end
            tempNote , success = GRM.RemoveDateFromNote ( player.customNote[6] );
            if success then
                -- yes, it was modified
                player.customNote[2] = time();
                player.customNote[3] = GRM_G.addonUser;
                finalNote = ( GRM.Trim ( finalNote .. tempNote ) );
                if #finalNote > GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:GetMaxLetters() then
                    finalNote = string.sub ( finalNote , 1 , GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:GetMaxLetters() );  -- Cheating a little here by just cutting off the end. Likely no one will ever notice with 150 chars to spare
                end
                player.customNote[6] = ( finalNote );
                success = false;
            end
        else
            if member[3] == 1 and GRM.CanViewOfficerNote()  then
                tempNote , success = GRM.RemoveDateFromNote ( player.officerNote );
                if success then
                    -- yes, it was modified
                    finalNote = ( GRM.Trim ( finalNote .. tempNote ) );
                    if #finalNote <= GRM_G.MaxNoteChars then                    -- To avoid errors need to add protections against trying to add > 31 chars.
                        player.officerNote = finalNote;
                        GuildRosterSetOfficerNote ( index , player.officerNote );
                    else
                        finalNote = ( GRM.Trim ( GRM.FormatTimeStamp ( player.joinDate[#player.joinDate] , false , false ) ) .. " " .. tempNote );    -- Remove header, try adding again.
                        if #finalNote <= GRM_G.MaxNoteChars then
                            player.officerNote = finalNote;
                            GuildRosterSetOfficerNote ( index , player.officerNote );
                        end
                    end
                    success = false;
                end
            elseif member[3] == 2 and ( GRM.CanViewOfficerNote() or player.name == GRM_G.addonUser ) then
                tempNote , success = GRM.RemoveDateFromNote ( player.note );
                if success then
                    -- yes, it was modified
                    finalNote = ( GRM.Trim ( finalNote .. tempNote ) );
                    if #finalNote <= GRM_G.MaxNoteChars then                    -- To avoid errors need to add protections against trying to add > 31 chars.
                        player.note = finalNote;
                        GuildRosterSetPublicNote ( index , player.note );
                    else
                        finalNote = ( GRM.Trim ( GRM.FormatTimeStamp ( player.joinDate[#player.joinDate] , false , false ) ) .. " " .. tempNote );    -- Remove header, try adding again.
                        if #finalNote <= GRM_G.MaxNoteChars then
                            player.note = finalNote;
                            GuildRosterSetPublicNote ( index , player.note );
                        end
                    end
                    success = false;
                end
            elseif member[3] == 3 then
                tempNote , success = GRM.RemoveDateFromNote ( player.customNote[6] );
                if success then
                    -- yes, it was modified
                    player.customNote[2] = time();
                    player.customNote[3] = GRM_G.addonUser;
                    finalNote = ( GRM.Trim ( finalNote .. tempNote ) );
                    if #finalNote > GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:GetMaxLetters() then
                        finalNote = string.sub ( finalNote , 1 , GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:GetMaxLetters() );  -- Cheating a little here by just cutting off the end. Likely no one will ever notice with 150 chars to spare
                    end
                    player.customNote[6] = ( finalNote );
                    success = false;
                end
            end
        end
    end

    -- Refresh the dates on the frames!!!
    if GRM_UI.GRM_MemberDetailMetaData:IsVisible() and GRM_G.currentName == member[1] then
        if GRM_UI.GRM_MemberDetailMetaData.GRM_SetUnknownButton:IsVisible() or GRM_UI.GRM_MemberDetailMetaData.GRM_SyncJoinDateSideFrame:IsVisible() or GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame:IsVisible() then
            GRM.ClearAllFrames( false );
        end
        GRM_G.pause = false;
        GRM.PopulateMemberDetails ( GRM_G.currentName );
        GRM_G.pause = true;
    end
end

-- Method:          GRM.RemoveDateFromNote ( string )
-- What it Does:    Removes the date from the note
-- Purpose:         To reverse engineer the notes and replace them with different dates.
GRM.RemoveDateFromNote = function ( note )
    local result = note;
    local temp, temp2 = {};
    local restOfString = "";
    local success = false;

    -- Cleanup headers, we don't need it. We need end of the name
    temp = GRM.NoteHasJoinTag ( note );
    if temp[1] then
        note = temp[2];
    end
    -- Remove unnecessary common symbols.
    note = GRM.CleanupNoteDate ( note );
    temp2 , restOfString = GRM.GetFormattedDatesFromMonthStrFirstNote ( note );
    if temp2 == nil then
        temp2 = GRM.GetNoteParsingBucket ( note );
        if temp2 ~= nil then
            result = select ( 2 , GRM.SetParseBucketLogic ( temp2 ) );
            if result == nil then
                success = false;
            else
                success = true;
            end
        end

    else
        result = restOfString;
        success = true;
    end

    return GRM.Trim ( result ) , success;     -- Cleanup the result;
end
 
-- Method:          GRM.AddDateTagToDefaultNote ( array )
-- What it Does:    Adds the join date properly to the default note
-- Purpose:         To control the Join Date being added to default note location, whilst not adding it if already found there, and not overwriting current note, and editing it slightly
--                  to be joined within the character count limit of the notes properly
GRM.AddDateTagToDefaultNote = function ( member , getCount )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][member[1]];
    local name = "";
    local index;
    local noteHeader = "";
    local count = 0;
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].includeTag then
        noteHeader = GRM_G.customHeaderJoin;
    end
    local finalNote = "";
    local success = false;
    
    if player ~= nil then
        if not getCount then
            GRM_G.changeHappenedExitScan = true;
        end
        index = nil;
        name = "";

        -- Find them in the guild indexes to modify offier/public notes
        if not getCount then
            for h = 1 , GRM.GetNumGuildies() do
                name = GetGuildRosterInfo( h );
                if name == member[1] then
                    index = h;
                    break;
                end
            end
        end

        -- Set the repeated note formatting to be added to note.
        finalNote = noteHeader .. " " .. GRM.FormatTimeStamp ( player.joinDate[#player.joinDate] , false , false );

        if GRM.CanViewOfficerNote() then
            -- Public and officer
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 1 then
                -- Officer is default.
                success = select ( 2 , GRM.RemoveDateFromNote ( player.officerNote ) ); -- If it successfully removes something we know we can stop here and don't need to continue further as it is not necessary to continue
                if not success then
                    -- Nope, not modified, which means good to import
                    finalNote = GRM.Trim ( finalNote .. " " .. player.officerNote );
                    if #finalNote <= GRM_G.MaxNoteChars then
                        if not getCount then
                            player.officerNote = finalNote;
                            GuildRosterSetOfficerNote ( index , player.officerNote );
                        else
                            count = count + 1;
                        end
                    else
                        finalNote = GRM.FormatTimeStamp ( player.joinDate[#player.joinDate] , false , false ) .. " " .. player.officerNote;        -- Remove the header and try again
                        if #finalNote <= GRM_G.MaxNoteChars then
                            if not getCount then
                                player.officerNote = finalNote;
                                GuildRosterSetOfficerNote ( index , player.officerNote );
                            else
                                count = count + 1;
                            end
                        end
                    end
                    success = false;
                end
            elseif GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 2 then
                success = select ( 2 , GRM.RemoveDateFromNote ( player.note ) ); -- If it successfully removes something we know we can stop here and don't need to continue further as it is not necessary to continue
                if not success then
                    -- Nope, not modified, which means good to import
                    finalNote = GRM.Trim ( finalNote .. " " .. player.note );
                    if #finalNote <= GRM_G.MaxNoteChars then
                        if not getCount then
                            player.note = finalNote;
                            GuildRosterSetPublicNote ( index , player.note );
                        else
                            count = count + 1;
                        end
                    else
                        finalNote = GRM.FormatTimeStamp ( player.joinDate[#player.joinDate] , false , false ) .. " " .. player.officerNote;        -- Remove the header and try again
                        if #finalNote <= GRM_G.MaxNoteChars then
                            if not getCount then
                                player.note = finalNote;
                                GuildRosterSetPublicNote ( index , player.note );
                            else
                                count = count + 1;
                            end
                        end
                    end
                    success = false;
                end
            end
        end

        -- Custom Note
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination == 3 then
            success = select ( 2 , GRM.RemoveDateFromNote ( player.customNote[6] ) );
            if not success then
                -- yes, it was modified
                if not getCount then
                    player.customNote[2] = time();
                    player.customNote[3] = GRM_G.addonUser;
                    finalNote = ( GRM.Trim ( finalNote .. "\n" .. player.customNote[6] ) );
                    if #finalNote > GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:GetMaxLetters() then
                        finalNote = string.sub ( finalNote , 1 , GRM_UI.GRM_MemberDetailMetaData.GRM_CustomNoteEditBoxFrame.GRM_CustomNoteEditBox:GetMaxLetters() );  -- Cheating a little here by just cutting off the end. Likely no one will ever notice with 150 chars to spare
                    end
                    player.customNote[6] = ( finalNote );
                else
                    count = count + 1;
                end
                success = false;
            end
        end
    end

    -- Refresh the dates on the frames!!!
    if GRM_UI.GRM_MemberDetailMetaData:IsVisible() and GRM_G.currentName == member[1] then
        if GRM_UI.GRM_MemberDetailMetaData.GRM_SetUnknownButton:IsVisible() or GRM_UI.GRM_MemberDetailMetaData.GRM_SyncJoinDateSideFrame:IsVisible() or GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame:IsVisible() then
            GRM.ClearAllFrames( false );
        end
        GRM_G.pause = false;
        GRM.PopulateMemberDetails ( GRM_G.currentName );
        GRM_G.pause = true;
    end

    return count;
end


-- Method:          GRM.RemoveDatesFromNonDefaultNotes ( array )
-- What it Does:    Removes the join dates from the notes that are not set as default
-- Purpose:         Easy Cleanup of the notes.
-- This should be bottom option... to prevent from erroring and removing all notes prematurely.
GRM.RemoveDatesFromNonDefaultNotes = function( member )
    local player = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ][member[1]];
    local tempNote = "";
    local name = "";
    local index;
    local success = false;

    if player ~= nil then
        GRM_G.changeHappenedExitScan = true;
        index = nil;
        name = "";
        tempNote = "";

        -- Find them in the guild indexes to modify offier/public notes
        for h = 1 , GRM.GetNumGuildies() do
            name = GetGuildRosterInfo( h );
            if name == member[1] then
                index = h;
                break;
            end
        end

        if GRM.CanViewOfficerNote() and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination ~= 1 then
            tempNote , success = GRM.RemoveDateFromNote ( player.officerNote );
            if success then
                -- yes, it was modified
                if tempNote == nil then
                    tempNote = "";
                end
                player.officerNote = GRM.Trim ( tempNote );
                GuildRosterSetOfficerNote ( index , tempNote );
                success = false;
            end
        end
        if ( GRM.CanViewOfficerNote() or player.name == GRM_G.addonUser ) and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination ~= 2 then
            tempNote , success = GRM.RemoveDateFromNote ( player.note );
            if success then
                -- yes, it was modified
                if tempNote == nil then
                    tempNote = "";
                end
                player.note = GRM.Trim ( tempNote );
                GuildRosterSetPublicNote ( index , player.note );
                success = false;
            end
        end
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination ~= 3 then
            tempNote , success = GRM.RemoveDateFromNote ( player.customNote[6] );
            if success then
                -- yes, it was modified
                player.customNote[2] = time();
                player.customNote[3] = GRM_G.addonUser;
                if tempNote == nil then
                    tempNote = "";
                end
                player.customNote[6] = GRM.Trim ( tempNote );
                success = false;
            end
        end
    end

    -- Refresh the dates on the frames!!!
    if GRM_UI.GRM_MemberDetailMetaData:IsVisible() and GRM_G.currentName == member[1] then
        if GRM_UI.GRM_MemberDetailMetaData.GRM_SetUnknownButton:IsVisible() or GRM_UI.GRM_MemberDetailMetaData.GRM_SyncJoinDateSideFrame:IsVisible() or GRM_UI.GRM_CoreAltFrame.GRM_AddAltEditFrame:IsVisible() then
            GRM.ClearAllFrames( false );
        end
        GRM_G.pause = false;
        GRM.PopulateMemberDetails ( GRM_G.currentName );
        GRM_G.pause = true;
    end
end

-- Method:          GRM.FixMismatchedGRMdatesUsingNote ( bool )
-- What it Does:    Scans through all of the given names of the guild and their note status, determines which ones need to be fixed
-- Purpose:         UX tool for managing join date conformity easier.
GRM.FixMismatchedGRMdatesUsingNote = function ( selectedOnly )
    for i = 1 , #GRM_G.AuditToolGuildies do
        if not selectedOnly or ( selectedOnly and GRM_G.AuditToolGuildies[i][6] ) then      -- if All or notAll, but IS selected.
            if GRM_G.AuditToolGuildies[i][4] == 1 or ( GRM_G.AuditToolGuildies[i][4] > 3 and GRM_G.AuditToolGuildies[i][4] < 7 ) then       -- 1,4,5,6 are the mismatched values
                GRM.ConvertGRMDateFromNote ( GRM_G.AuditToolGuildies[i] );
            end
        end
    end

    -- Refresh the frames AFTER it finishes with all...
    if GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame:IsVisible() then
        GRM.RefreshAuditFrames ( true , true );
    end
    GRM.AuditRefresh( true );
end


-- Method:          GRM.FixMismatchedNotesUsingGRM ( bool )
-- What it Does:    Scans through all of the given names of the guild and their note status, determines which ones need to be fixed using the GRM save to overwrite the ntoes
-- Purpose:         UX tool for managing join date conformity easier.
GRM.FixMismatchedNotesUsingGRM = function ( selectedOnly )
    for i = 1 , #GRM_G.AuditToolGuildies do
        if not selectedOnly or ( selectedOnly and GRM_G.AuditToolGuildies[i][6] ) then      -- if All or notAll, but IS selected.
            if GRM_G.AuditToolGuildies[i][4] == 1 or ( GRM_G.AuditToolGuildies[i][4] > 3 and GRM_G.AuditToolGuildies[i][4] < 7 ) then       -- 1,4,5,6 are the mismatched values
                GRM.ConvertNoteFromGRM ( GRM_G.AuditToolGuildies[i] );
            end
        end
    end

    GRM.AuditRefresh( true );
end

-- Method:          GRM.ImportDatesFromNoteToGRM ( bool )
-- What it Does:    Scans through all the notes and imports the date to GRM as needed.
-- Purpose:         UX tool for ease of importing dates from the player notes.
GRM.ImportDatesFromNoteToGRM = function ( selectedOnly )
    for i = 1 , #GRM_G.AuditToolGuildies do
        if not selectedOnly or ( selectedOnly and GRM_G.AuditToolGuildies[i][6] ) then      -- if All or notAll, but IS selected.
            if GRM_G.AuditToolGuildies[i][4] > 8 and GRM_G.AuditToolGuildies[i][4] < 13 then       -- 9-13
                GRM.ConvertGRMDateFromNote ( GRM_G.AuditToolGuildies[i] );
            end
        end
    end

    GRM.AuditRefresh( true );
end

-- Method:          GRM.AddJoinDatesToDefaultNotes ( bool )
-- What it Does:    Scans through all the notes and imports the date to GRM as needed.
-- Purpose:         UX tool for ease of importing dates from the player notes.
GRM.AddJoinDatesToDefaultNotes = function ( selectedOnly )
    for i = 1 , #GRM_G.AuditToolGuildies do
        if not selectedOnly or ( selectedOnly and GRM_G.AuditToolGuildies[i][6] ) then      -- if All or notAll, but IS selected.
            if GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton1Text:GetText() == GRM.L ( "Add all missing saved dates to the {name}" , GRM.GetNoteName() ) or GRM_G.AuditToolGuildies[i][6] then      -- if All or notAll, but IS selected.
                if ( GRM_G.AuditToolGuildies[i][4] > 1 and GRM_G.AuditToolGuildies[i][4] < 5 ) or GRM_G.AuditToolGuildies[i][4] == 6 or GRM_G.AuditToolGuildies[i][4] == 7 then       -- 2,3,4,6,7  -- 3 = not just not found
                    -- parse the default note positions now...
                    GRM.AddDateTagToDefaultNote ( GRM_G.AuditToolGuildies[i] , false );
                end
            end
        end
    end

    GRM.AuditRefresh( true );
end

-- Method:          GRM.CleanUpNonDefaultNotes ()
-- What it Does:    Scans through all the notes to remove join dates from the incorrect notes.
-- Purpose:         UX tool for ease of cleaning up notes
GRM.CleanUpNonDefaultNotes = function ()
    for i = 1 , #GRM_G.AuditToolGuildies do
        if GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton7Text:GetText() == GRM.L ( "Clear all join dates from incorrect note locations" ) or GRM_G.AuditToolGuildies[i][6] then      -- if All or notAll, but IS selected.
            if GRM_G.AuditToolGuildies[i][3] == 4 then       -- 4 = multiple locations (1=officer, 2 = public, 3=custom, thus if in more than 1 it equals 4)
                GRM.RemoveDatesFromNonDefaultNotes ( GRM_G.AuditToolGuildies[i] );
            end
        end
    end
    GRM.AuditRefresh( true );
end

-- Method:          GRM.MismatchConfirmConfig()
-- What it Does:    Builds function template to be inserted into the confirmation window and determines if we are fixing all mismatched or only those highlighted (from note to GRM save)
-- Purpose:         Clean UX experience that prevents players from accidentally making an error
GRM.MismatchConfirmConfig = function()
    if GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton4Text:GetText() == GRM.L ( "Fix all mismatched GRM dates with the note date" ) then
        GRM.FixMismatchedGRMdatesUsingNote ( false );
    else
        GRM.FixMismatchedGRMdatesUsingNote ( true );
    end
end

-- Method:          GRM.MismatchConfirmConfig2()
-- What it Does:    Builds function template to be inserted into the confirmation window and determines if we are fixing all mismatched or only those highlighted (from GRM save to note)
-- Purpose:         Clean UX experience that prevents players from accidentally making an error
GRM.MismatchConfirmConfig2 = function()
    if GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton3Text:GetText() == GRM.L ( "Fix all mismatched note dates with GRM saved date" ) then
        GRM.FixMismatchedNotesUsingGRM ( false );
    else
        GRM.FixMismatchedNotesUsingGRM ( true );
    end
end

-- Method:          GRM.MismatchConfirmConfig3()
-- What it Does:    Builds function template to be inserted into the confirmation window and determines if we are fixing all mismatched or only those highlighted (from GRM save to note)
-- Purpose:         Clean UX experience that prevents players from accidentally making an error
GRM.MismatchConfirmConfig3 = function()
    if GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton2Text:GetText() == GRM.L ( "Import all missing join dates from the note date" ) then
        GRM.ImportDatesFromNoteToGRM ( false );
    else
        GRM.ImportDatesFromNoteToGRM ( true );
    end
end

-- Method:          GRM.MismatchConfirmConfig4()
-- What it Does:    Builds function template to be inserted into the confirmation window and determines if we are fixing all or only those highlighted (from GRM save to note)
-- Purpose:         Clean UX experience that prevents players from accidentally making an error
GRM.MismatchConfirmConfig4 = function()
    if GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton1Text:GetText() == GRM.L ( "Add all missing saved dates to the {name}" , GRM.GetNoteName() ) then
        GRM.AddJoinDatesToDefaultNotes ( false );
    else
        GRM.AddJoinDatesToDefaultNotes ( true );
    end
end

-- Method:          GRM.MismatchConfirmConfig5()
-- What it Does:    Builds function template to be inserted into the confirmation window and determines if we are fixing all or only those highlighted (cleanup of non-default notes)
-- Purpose:         Clean UX experience that prevents players from accidentally making an error
GRM.MismatchConfirmConfig5 = function()
    if GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton7Text:GetText() == GRM.L ( "Clear all join dates from incorrect note locations" , GRM.GetNoteName() ) then
        GRM.CleanUpNonDefaultNotes ( false );
    else
        GRM.CleanUpNonDefaultNotes ( true );
    end
end

-- Method:          GRM.GetNumMismatchedButton4()
-- What it Does:    Returns the count of the number of mismatched, either all, or of that selected.
-- Purpose:         UX
GRM.GetNumMismatchedButton4 = function()
    local count = 0;

    for i = 1 , #GRM_G.AuditToolGuildies do
        if GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton4Text:GetText() == GRM.L ( "Fix all mismatched GRM dates with the note date" ) or GRM_G.AuditToolGuildies[i][6] then      -- if All or notAll, but IS selected.
            if GRM_G.AuditToolGuildies[i][4] == 1 or ( GRM_G.AuditToolGuildies[i][4] > 3 and GRM_G.AuditToolGuildies[i][4] < 7 ) then       -- 1,4,5,6 are the mismatched values
                count = count + 1;
            end
        end
    end

    return count;
end

-- Method:          GRM.GetNumMismatchedButton3()
-- What it Does:    Returns the count of the number of mismatched, either all, or of that selected.
-- Purpose:         UX
GRM.GetNumMismatchedButton3 = function()
    local count = 0;
    
    for i = 1 , #GRM_G.AuditToolGuildies do
        if GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton3Text:GetText() == GRM.L ( "Fix all mismatched note dates with GRM saved date" ) or GRM_G.AuditToolGuildies[i][6] then      -- if All or notAll, but IS selected.
            if GRM_G.AuditToolGuildies[i][4] == 1 or ( GRM_G.AuditToolGuildies[i][4] > 3 and GRM_G.AuditToolGuildies[i][4] < 7 ) then       -- 1,4,5,6 are the mismatched values
                count = count + 1;
            end
        end
    end

    return count;
end

-- Method:          GRM.GetNumMismatchedButton2()
-- What it Does:    Returns the count of the number of dates that need to be imported, either all, or of that selected.
-- Purpose:         UX
GRM.GetNumMismatchedButton2 = function()
    local count = 0;
    
    for i = 1 , #GRM_G.AuditToolGuildies do
        if GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton2Text:GetText() == GRM.L ( "Import all missing join dates from the note date" ) or GRM_G.AuditToolGuildies[i][6] then      -- if All or notAll, but IS selected.
            if GRM_G.AuditToolGuildies[i][4] > 8 and GRM_G.AuditToolGuildies[i][4] < 13 then       -- 1,4,5,6 are the mismatched values
                count = count + 1;
            end
        end
    end

    return count;
end

-- Method:          GRM.GetNumMismatchedButton1()
-- What it Does:    Returns the count of the number of notes that need to be added to the officer note
-- Purpose:         UX
GRM.GetNumMismatchedButton1 = function()
    local count = 0;
    for i = 1 , #GRM_G.AuditToolGuildies do
        if GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton1Text:GetText() == GRM.L ( "Add all missing saved dates to the {name}" , GRM.GetNoteName() ) or GRM_G.AuditToolGuildies[i][6] then      -- if All or notAll, but IS selected.
            if ( GRM_G.AuditToolGuildies[i][4] > 1 and GRM_G.AuditToolGuildies[i][4] < 5 ) or GRM_G.AuditToolGuildies[i][4] == 6 or GRM_G.AuditToolGuildies[i][4] == 7 then       -- 2,3,4,6,7  -- 3 = not just not found
                -- parse the default note positions now...
                count = count + GRM.AddDateTagToDefaultNote ( GRM_G.AuditToolGuildies[i] , true );
            end
        end
    end
    return count;
end

-- Method:          GRM.GetNumMismatchedButton7()
-- What it Does:    Returns the count of the number of players that have join dates in incorrect notes.
-- Purpose:         UX
GRM.GetNumMismatchedButton7 = function()
    local count = 0;
    for i = 1 , #GRM_G.AuditToolGuildies do
        if GRM_UI.GRM_AuditJDTool.GRM_AuditJDToolButton7Text:GetText() == GRM.L ( "Clear all join dates from incorrect note locations" ) or GRM_G.AuditToolGuildies[i][6] then      -- if All or notAll, but IS selected.
            if GRM_G.AuditToolGuildies[i][3] == 4 or GRM_G.AuditToolGuildies[i][3] == 2 then       -- 4 = multiple locations (1=officer, 2 = public, 3=custom, thus if in more than 1 it equals 4)
                -- parse the default note positions now...
                count = count + 1;
            end
        end
    end
    return count;
end



-- GRM.FindLikelyDateStart = function ( note )

-- end
--------------------------------------------
-------- HYBRID SCROLLFRAME TEMPLATES ------
--------------------------------------------

-- Method:          GRM.SetHybridScrollFrameSliderParameters ( frame , frame , frame , int , int , int , int , arrayOfButtons , function , function )
-- What it Does:    Acts as a template for all future hybrid scrollframe configuration of the slider logic on mousewheel scrolling and so on
-- Purpose:         Clean, repeatable code that can be used in conjunction with all other large scrollframes
GRM.SetHybridScrollFrameSliderParameters = function( childFrame , HscrollFrame , HscrollFrameSlider , buttonW , buttonH , scrollH , totalEntries , buttons , logicFunction1 , logicFunction2 , totalPotentialButtons )
    
    childFrame:SetSize ( buttonW , HscrollFrame:GetHeight() );

    local scrollMax = ( scrollH - HscrollFrame:GetHeight() );
    if scrollMax < 0 or totalEntries <= totalPotentialButtons then
        scrollMax = 0;
    end

    HscrollFrameSlider:SetMinMaxValues ( 0 , scrollMax );

    if scrollMax > 0 then
        HscrollFrame:EnableMouseWheel( true );
    else
        HscrollFrame:EnableMouseWheel ( false );
    end

    if scrollMax > 0 then
        HscrollFrame:SetScript( "OnMouseWheel" , function( _ , delta )
            if totalEntries > 0 then
                GRM.HybridControl( HscrollFrameSlider );
                local current = HscrollFrameSlider:GetValue();

                -- Scroll Down
                if delta < 0 and current < scrollMax then
                    if IsControlKeyDown() then
                        HscrollFrameSlider.HybridControlBool = false;
                        if IsShiftKeyDown() then
                            HscrollFrameSlider:SetValue ( current + ( buttonH * 12 ) );
                        else
                            HscrollFrameSlider:SetValue ( current + ( buttonH * 3 ) );
                        end
                    elseif IsShiftKeyDown() then
                        HscrollFrameSlider.HybridControlBool = false;
                        HscrollFrameSlider:SetValue ( select ( 2 , HscrollFrameSlider:GetMinMaxValues() ) );
                    else
                        HscrollFrameSlider:SetValue ( current + buttonH );
                    end

                -- Scroll Up
                elseif delta > 0 and current > 1 then
                    if IsControlKeyDown() then
                        HscrollFrameSlider.HybridControlBool = false;
                        if IsShiftKeyDown() then
                            HscrollFrameSlider:SetValue ( current - ( buttonH * 12 ) );
                        else
                            HscrollFrameSlider:SetValue ( current - ( buttonH * 3 ) );
                        end
                    elseif IsShiftKeyDown() then
                        HscrollFrameSlider.HybridControlBool = false;
                        HscrollFrameSlider:SetValue ( 0 );
                    else
                        HscrollFrameSlider:SetValue ( current - buttonH );
                    end
                end
                
                if HscrollFrameSlider.HybridControlBool then
                    -- Scroll Down
                    if delta < 0 and childFrame.Offset < totalEntries then
                        childFrame.Offset = childFrame.Offset + 1;
                        logicFunction1();

                    -- Scroll Up
                    elseif delta > 0 and childFrame.Offset > #buttons then
                        childFrame.Offset = childFrame.Offset - 1;
                        logicFunction2();
                    end
                end
            end
        end);
    end
end

-- Method:          GRM.HybridScrollOnValueChangedConfig ( slider , float , scrollchildframe , scrollframe , int , int , function , array )
-- What it Does:    In conjunction with the GRM.SetHybridScrollFrameSliderParameters() tool, manage the hybridscrollframe functionality of the given frames
-- Purpose:         Reusable framework for hybrid scrollframes in this addon's style.
GRM.HybridScrollOnValueChangedConfig = function ( HscrollFrameSlider , value , scrollChildFrame, scrollFrame , numButtons , stepSize , refreshFunction , dataArray )
    local maxSize = 0
    local arraySize = #dataArray;

    if value > HscrollFrameSlider.currentV then
        if HscrollFrameSlider.HybridControlBool and scrollFrame:GetVerticalScroll() < maxSize then
            scrollFrame:SetVerticalScroll ( value );
        else
            if not HscrollFrameSlider.HybridControlBool then
                if value >= maxSize and scrollFrame:GetVerticalScroll() < maxSize then
                    scrollFrame:SetVerticalScroll ( maxSize );
                end
                scrollChildFrame.Offset = math.floor ( ( value / stepSize ) ) + numButtons;
                -- Prevent overlap errors
                if scrollChildFrame.Offset > arraySize then
                    scrollChildFrame.Offset = arraySize;
                end
                refreshFunction ( true , false );
            end
        end
    else
        if HscrollFrameSlider.HybridControlBool and scrollFrame:GetVerticalScroll() > 0 and value <= maxSize then
            scrollFrame:SetVerticalScroll ( value );
        else
            if not HscrollFrameSlider.HybridControlBool then

                if value == 0 and scrollFrame:GetVerticalScroll() <= maxSize then
                    scrollFrame:SetVerticalScroll ( 0 );
                end
                
                scrollChildFrame.Offset = math.floor ( ( value / stepSize ) ) + numButtons;
                -- Prevent overlap errors
                if scrollChildFrame.Offset > arraySize then
                    scrollChildFrame.Offset = arraySize;
                end
                refreshFunction ( true , false );
            end
        end
    end
    HscrollFrameSlider.currentV = value;
end

-- Method:          GRM.HybridControl()
-- What it Does:    Sets a gate check that lasts for 0.1 seconds
-- Purpose:         So that when the slider value changes, it ignores double work when an "OnMouseWheel" handler is also activated.
GRM.HybridControl = function( slider )
    slider.HybridControlBool = true;
    C_Timer.After ( 0.1 , function()
        slider.HybridControlBool = false;
    end);
end


-------------------------------------
----- END HYBRID SCROLLFRAMES -------
-------------------------------------

-------------------------------------
-- VARIOUS TOOLTIP CLICK PARAMETERS -
-------------------------------------

-- Method:          GRM.SearchPlayerInLog ( string )
-- What it Does:    Opens the Log and searches the given string
-- Purpose:         For repeat use actions from other frames
GRM.SearchPlayerInLog = function ( playerName ) 
    GRM.RestoreTooltip()
    GRM_UI.GRM_RosterChangeLogFrame:Show();
    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogTab:Click();
    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame.GRM_LogEditBox:SetText( GRM.SlimName ( playerName ) );
end

-- Method:          GRM.DisplayRosterMember ( string , index )
-- What it Does:    It finds the player's roster index, and then it set it to that
-- Purpose:         To get around the issues with the new kick system
GRM.DisplayRosterMember = function ( playerName , index )
    local getRosterIndex = function()
        for i = 1 , GRM.GetNumGuildies() do
            if GetGuildRosterInfo ( i ) == playerName then
                return i;
            end
        end
        return nil;
    end
    local i = index or getRosterIndex();
    SetGuildRosterSelection ( i );
    if not GRM_UI.MemberDetailFrameClassic:IsVisible() then
        GRM_UI.MemberDetailFrameClassic:Show();
    end
    if GRM_G.BuildVersion < 40000 then
        GuildStatus_Update();
    else
        GuildRoster_Update();
    end
end

-- Method:          GRM.OpenPlayerWindow ( string )
-- What it Does:    Opens the community frame and brings up the player window for editing
-- Purpose:         Easy access to find the player from various frames
GRM.OpenPlayerWindow = function ( playerName )
    if GuildFrame and not GuildFrame:IsVisible() then
        if GRM_G.BuildVersion < 80000 then
            SocialsMicroButton:Click();
            C_Timer.After ( 0.1 , function()
                if not GuildFrame:IsVisible() then
                    if not GRM_G.ClassicTaintWarning then
                        GRM_G.ClassicTaintWarning = true;
                        GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "To avoid addon taint/blocking errors in Classic, the player must manually open the Guild Roster tab the first time." ) );
                    end
                end
            end);
        else
            -- Force community Frame open
            GuildFrame_Toggle();
            GuildFrame_TabClicked ( GuildFrameTab2 );
        end
    end
    
    if GRM_UI.MemberDetailFrame and GRM_UI.MemberDetailFrame:IsVisible() then
        GRM_UI.MemberDetailFrame:Hide();
    end

    local timer = 0;

    if not GRM_G.BlizzFramePinsInitialized then
        timer = 0.5;
    end

    -- need to set the proper index of selection

    C_Timer.After ( timer , function()
    
        GRM_G.pause = false;
        GRM_G.currentName = playerName;
        GRM_G.RosterSelection = GRM.GetRosterSelectionID ( playerName );
        GRM.ClearAllFrames( true );
        GRM.PopulateMemberDetails ( GRM_G.currentName );
        GRM_UI.GRM_MemberDetailMetaData:Show();
        GRM.DisplayRosterMember ( playerName , GRM_G.RosterSelection );
        GRM.UpdateMemberDetailNameClassColor();
        GRM_G.pause = true;

    end);
    
end

-- Method:          GRM.GetRosterSelectionID ( string )
-- What it Does:    Updates the selected index of the manually opened player window
-- Purpose:         To ensure regular update intervals are accurately looking at the correct player.
GRM.GetRosterSelectionID = function ( name )
    local index = 1;            -- Defaults to 1 to prevent errors

    for i = 1 , GRM.GetNumGuildies() do
        if GetGuildRosterInfo ( i ) == name then
            index = i;
            break;
        end
    end

    return index
end

--------------------------------
---- END TOOLTIP CLICK PARAS ---
--------------------------------

------------------------------
----- CHAT MSG LISTENING -----
------------------------------

-- Need to work with the built-in sync system to ensure only 1 leader reports to chat...
C_ChatInfo.RegisterAddonMessagePrefix( "GRM_GCHAT" );
GRM.GRM_FrameChatTest = CreateFrame ( "Frame" , "GRM_FrameChatTest");
GRM.GRM_FrameChatTest:RegisterEvent ( "CHAT_MSG_ADDON" );
GRM.GRM_FrameChatTest.IsInLoop = false;

GRM.GRM_FrameChatTest:SetScript ( "OnEvent" , function ( _ , event , prefix , msg , _ , sender )
    -- Only acknowledge if you are sending it to yourself, as this is just a check to see if you have access
    if event == "CHAT_MSG_ADDON" and prefix == "GRM_GCHAT" and sender ~= GRM_G.addonUser then
        local prefix2 = string.sub ( msg , 1 , string.find ( msg , "?" ) - 1 );

        -- Force global controls update check
        if prefix2 == "GINFOUPDATE" and not GRM.GRM_FrameChatTest.IsInLoop then
            GRM.GRM_FrameChatTest.IsInLoop = true;
            local needsLoopCheck = false;

            if GRM_G.BuildVersion >= 80000 then
                needsLoopCheck = true;
                if GuildRoster then
                    GuildInfoFrame_UpdateText();
                end
                CommunitiesGuildInfoFrame_UpdateText ( CommunitiesFrameGuildDetailsFrameInfo );
            else
                if GRM_UI.MemberDetailFrame ~= nil and not GRM_UI.MemberDetailFrame:IsVisible() and ( GetCurrentKeyBoardFocus() == nil or ( GetCurrentKeyBoardFocus():GetObjectType() == nil ) or ( GetCurrentKeyBoardFocus():GetObjectType() ~= "EditBox" ) ) then
                    needsLoopCheck = true;
                    local isVisible = GuildInfoFrame:IsVisible();
                    ToggleGuildInfoFrame();
                    if not isVisible and GuildInfoFrame:IsVisible() then
                        GuildInfoFrame:Hide();
                    elseif isVisible then
                        ToggleGuildInfoFrame();
                    end
                end
            end
            if needsLoopCheck then
                GRM.GlobalSettingsLoopCheck( nil , true );
            else
                GRM.GRM_FrameChatTest.IsInLoop = false;
            end
        end
    end
end);

-- Method:          GRM.GlobalSettingsLoopCheck ( int )
-- What it Does:    Controls the recheck on the guildInfo as when the window is closed it doesn't refresh immediately.
-- Purpose:         Quality control to keep the settings check as fast as possible on global control changesw
GRM.GlobalSettingsLoopCheck = function( epochTime , first )
    local timer = epochTime or time();
    local reCheck = function()
        local canExit = false;
        if GRM_G.GuildInfo ~= GetGuildInfoText() and not first then
            canExit = true;
        end

        return canExit;
    end

    if ( time() - timer ) <= 10 then
        if reCheck() then
            GRM.UpdateGuildLeaderPermissions ( false , true );
            GRM.GRM_FrameChatTest.IsInLoop = false;
        else
            GRM.GuildRoster();
            C_Timer.After ( 1 , function()
                GRM.GlobalSettingsLoopCheck ( timer , false );
                return;
            end);
        end
    else
        GRM.GRM_FrameChatTest.IsInLoop = false;
    end
end

-- Method:          GRM.TriggerPlayerNote ( string , string )
-- What it Does:    Parses every comment in the guild frame and looks for the !note tag - and adds anything after it, up to 31 characters, to player note
-- Purpose:         Mainly for use in Classic
GRM.TriggerPlayerNote = function ( player , msg )
    local caseFreeTxt = string.lower ( msg );
    local startIndex = string.find ( GRM.Trim ( caseFreeTxt ) , GRM.L ( "!note" ) );
    local isEnglish = false;

    if startIndex == nil then
        string.find ( GRM.Trim ( caseFreeTxt ) , "!note" );     -- Always working in case of other language
        isEnglish = true;
    end
    
    -- No need to continue on if it is not found at start... No need to do extra attempt processing
    if startIndex ~= nil then
        msg , startIndex = GRM.SpecialAddonAltTagCleanup ( GRM.RemoveStringColoring ( msg ) , startIndex ); -- Cleaning it up!!!
        if startIndex == 1 then

            -- Success!
            -- Let's check what we have... No point in carrying on if there is no note to set...
            local note = "";
            
            if not isEnglish then
                note = GRM.Trim ( string.sub ( msg , select ( 2 , string.find ( string.lower ( msg ) , GRM.L ( "!note" ) ) ) + 1 ) );
            else
                note = GRM.Trim ( string.sub ( msg , select ( 2 , string.find ( string.lower ( msg ) , "!note" ) ) + 1 ) );
            end

            if #note > 0 then
                -- Note is good, let's trigger the boolean...
                if ( GRM_G.BuildVersion >= 80000 and CanEditPublicNote() and ( GRM.CanEditOfficerNote() or GRM_G.addonUser == player ) ) or ( GRM_G.BuildVersion < 80000 and CanEditPublicNote() ) then             -- Don't need to write your own public note.
                    GRM.UpdateNoteFromChat ( player , note );
                else
 
                    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].note then
                        -- Do an officer check of addon users online...
                        GRM.RegisterGuildAddonUsersRefresh();
                        C_Timer.After ( 2.1 , function()
                            local addonUsers = GRM_G.currentAddonUsers;
                            local officerFoundOnlineWithAddon = false;
                            for i = 1 , #addonUsers do
                                if GRM.IsGuildieAnOfficer ( addonUsers[i][1] ) then
                                    officerFoundOnlineWithAddon = true;
                                    break;
                                end
                            end

                            if not officerFoundOnlineWithAddon and not GRM_G.ReportedNoOfficerOnly then
                                GRM_G.ReportedNoOfficerOnly = true;
                                if player ~= GRM_G.addonUser then
                                    GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "No officer online to set {name}'s note" , GRM.SlimName ( player ) ) );
                                else
                                    GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "No officer is currently online to update your note" ) );
                                end
                            elseif officerFoundOnlineWithAddon then
                                C_Timer.After ( 1 , function()
                                    GRM_G.ReportedNoOfficerOnly = false;
                                    GRM.Report ( GRM.L ( "{name}'s note has been updated!" , GRM.SlimName ( player ) ) );
                                end);
                            end
                        end);
                    end
                end
            end
        end
    end
end

-- Method:          GRM.UpdateNoteFromChat ( string , string )
-- What it Does:    Updates the player note if you are an officer to do it, and the player typed !note in chat
-- Purpose:         Help with adding public notes for guildies who it is restricted due to officer changes.
GRM.UpdateNoteFromChat = function ( player , note )
    local maxNoteSize = 31;

    if GRM.GetNumLetters ( note ) > maxNoteSize then
        C_Timer.After ( 0.1 , function()
            GRM.Report ( GRM.L ( "The note is too long. Only the first {num} characters will be set." , nil , nil , maxNoteSize ) );
        end);
    end
    -- Let's find the player
    for i = 1 , GRM.GetNumGuildies() do
        if GetGuildRosterInfo ( i ) == player then
            GuildRosterSetPublicNote ( i , note );
            if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser]["toChat"].note then
                C_Timer.After ( 1 , function()
                    GRM_G.ReportedNoOfficerOnly = false;
                    GRM.Report ( GRM.L ( "{name}'s note has been updated!" , GRM.SlimName ( player ) ) );
                end);
            end
            break;
        end
    end
end

-- Method:          GRM.IsPlayerAnAddonUser ( string )
-- What it Does:    It returns true if the player is currently online and is an addon user
-- Purpose:         For reporting updates...
GRM.IsPlayerAnAddonUser = function ( name )
    local result = false;
    local users = GRM_G.currentAddonUsers;
    for i = 1 , #users do
        if users[i][1] == name then
            result = true;
            break;
        end
    end
    return result;
end

-- Method:          GRM.SpecialAddonAltTagCleanup ( string , int )
-- What it Does:    If the player is using one of those alt name/main guild message addons this parses it out
-- Purpose:         So it is compatible with guild chat manipulated addons...
GRM.SpecialAddonAltTagCleanup = function ( note , startIndex )
    local tags = { ":" , "]" , ")" , "-" , ">" };
    local isSpecial = false;
    if startIndex > 1 then
        for i = startIndex - 1 , 1 , -1 do
            local tag = string.sub ( note , i , i )
            for j = 1 , #tags do
                if tag == tags[j] then
                    note = string.sub ( note , startIndex );
                    startIndex = 1;     -- Need to update it!
                    isSpecial = true;
                    break;
                end
            end
            if isSpecial or ( i == startIndex - 2 ) then
                break;
            end
        end
    end
    return note , startIndex;
end

----------------------
--- FRAME VALUES -----
--- AND PARAMETERS ---
--- AND REUSABLES ----
----------------------

-- Method:          GRM.SetConfirmationWindow ( function , string )
-- What it Does:    Creates the settings for a reusable popup confirmation window that can be used for any reason
-- Purpose:         Useful to prevent errors and prevents code clutter by being flexibly reusable.
GRM.SetConfirmationWindow = function( addedFunction , text )
    -- Configure the yes button
    GRM_UI.GRM_GeneralPopupWindow.GRM_GeneralPopupWindowYesButton:SetScript ( "OnClick" , function( _ , button ) 
        if button == "LeftButton" then
            addedFunction();
            GRM_UI.GRM_GeneralPopupWindow:Hide();
        end
    end);

    GRM_UI.GRM_GeneralPopupWindow.GRM_GeneralPopupWindowText:SetText ( text );
    if not GRM_UI.GRM_GeneralPopupWindow:IsVisible() then
        GRM_UI.GRM_GeneralPopupWindow:Show();
    end
end

-- Method:          GRM.GetTransitionFrameToFade()
-- What it Does:    Gets the frame that is currently visible. The tab the player is currently looking at.
-- Purpose:         To save on resources, rather than reuse this code over and over. I could potentially just make a global holder, but I want it to be flixible.
GRM.GetTransitionFrameToFade = function()
    local fadeFrame;
    if GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame:GetAlpha() == 1 then
        fadeFrame = GRM_UI.GRM_RosterChangeLogFrame.GRM_LogFrame;
    elseif GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame:GetAlpha() == 1 then
        fadeFrame = GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame;
    elseif GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame:GetAlpha() == 1 then
        fadeFrame = GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame;
    elseif GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame:GetAlpha() == 1 then
        fadeFrame = GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame;
    elseif GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame:GetAlpha() == 1 then
        fadeFrame = GRM_UI.GRM_RosterChangeLogFrame.GRM_EventsFrame;
    elseif GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame:GetAlpha() == 1 then
        fadeFrame = GRM_UI.GRM_RosterChangeLogFrame.GRM_AuditFrame;
    end
    return fadeFrame;
end

-- Method:          GRM.FrameTransition()
-- What it Does:    Fades frame to frame on tab check...
-- Purpose:         Really, just aesthetics for User Experience. This also is built to be flexible, to account for any given tab.
GRM.FrameTransition = function( fadeInName , fadeOutName , isOptionsTab , isOptionsSubTab )
    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].useFade then 
        if fadeInName ~= nil then
            fadeInName:Show();
            fadeInName:SetAlpha( fadeInName:GetAlpha() + 0.04 );
        end
        if fadeOutName ~= nil then
            fadeOutName:SetAlpha( fadeOutName:GetAlpha() - 0.04 );
        end

        if ( fadeInName ~= nil and fadeInName:GetAlpha() < 1 ) or ( fadeInName == nil and fadeOutName:GetAlpha() > 0 ) then
            C_Timer.After ( 0.01 , function()
                GRM.FrameTransition ( fadeInName , fadeOutName , isOptionsTab , isOptionsSubTab );
            end);
        else
            if fadeOutName ~= nil then
                fadeOutName:SetAlpha ( 0 );
                fadeOutName:Hide();
            end
            if fadeInName ~= nil then
                fadeInName:SetAlpha ( 1 );
            end
            if isOptionsTab then
                GRM.DisableTabButtons ( false );
            elseif isOptionsSubTab then
                GRM.DisableSubTabButtons ( false );
            end
        end
    else
        if fadeInName ~= nil then
            fadeInName:SetAlpha ( 1 );
            fadeInName:Show();
        end
        if fadeOutName ~= nil then
            fadeOutName:SetAlpha ( 0 );
            fadeOutName:Hide();
        end
        if isOptionsTab then
            GRM.DisableTabButtons ( false );
        elseif isOptionsSubTab then
            GRM.DisableSubTabButtons ( false );
        end
    end
end

-- Method:          GRM.OptionTabFrameControl ( buttonWidget )
-- What it Does:    It Locks the highlight of the current tab and it unlocks the others, as well as showing the correct frame, whilst hiding the others
-- Purpose:         Options are plentiful. Need sub-tabs to keep it clean. This helps control UI display logic on the tabs.
GRM.OptionTabFrameControl = function ( tabNotToUnlock )
    GRM.DisableSubTabButtons ( true );
    local tabs = { GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralTab , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ScanTab , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncTab , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_HelpTab , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UITab , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_OfficerTab , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UXTab , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ModulesTab };
    local frames = { GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralOptionsFrame , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ScanningOptionsFrame , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_HelpOptionsFrame , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UIOptionsFrame , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_OfficerOptionsFrame , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UXOptionsFrame , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ModulesFrame };
    local fadeFrame;
    local fadeInFrame; 
            
    for i = 1 , #tabs do
        if tabs[i] ~= tabNotToUnlock then
            -- Lock highlight
            tabs[i]:UnlockHighlight();
        else
            -- Corresponding tab frame should now be shown whilst hiding the rest. Index will match
            fadeInFrame = frames[i];                
        end
    end

    for j = 1 , #frames do
        if frames[j]:GetAlpha() == 1 then
            fadeFrame = frames[j];
            break;
        end
    end
    -- Ok, let's do the work!
    GRM.FrameTransition ( fadeInFrame , fadeFrame , false , true );
end

-- Method:          GRM.DisableTabButtons()
-- What it Does:    Temporarily disables the buttons. Don't want to allow player to trigger click spam the button on transition.
-- Purpose:         Clicking button too fast will be error prone.
local tempTabScript = {};
GRM.DisableTabButtons = function( toDisable )
    if toDisable then
        -- Storing the scripts
        tempTabScript = { GRM_UI.GRM_RosterChangeLogFrame.GRM_LogTab:GetScript ( "OnClick" ) , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsTab:GetScript ( "OnClick" ) , GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersTab:GetScript ( "OnClick" ) , GRM_UI.GRM_RosterChangeLogFrame.GRM_AddEventTab:GetScript ( "OnClick" ) , GRM_UI.GRM_RosterChangeLogFrame.GRM_BanListTab:GetScript ( "OnClick" ) , GRM_UI.GRM_RosterChangeLogFrame.GRM_GuildAuditTab:GetScript ( "OnClick" ) };

        -- removing the script
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogTab:SetScript ( "OnClick" , nil );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsTab:SetScript ( "OnClick" , nil );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersTab:SetScript ( "OnClick" , nil );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AddEventTab:SetScript ( "OnClick" , nil );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_BanListTab:SetScript ( "OnClick" , nil );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_GuildAuditTab:SetScript ( "OnClick" , nil );
    else
        -- restoring the script
        GRM_UI.GRM_RosterChangeLogFrame.GRM_LogTab:SetScript ( "OnClick" , tempTabScript[1] );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsTab:SetScript ( "OnClick" , tempTabScript[2] );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersTab:SetScript ( "OnClick" , tempTabScript[3] );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_AddEventTab:SetScript ( "OnClick" , tempTabScript[4] );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_BanListTab:SetScript ( "OnClick" , tempTabScript[5] );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_GuildAuditTab:SetScript ( "OnClick" , tempTabScript[6] );
    end
end

-- Method:          GRM.DisableSubTabButtons()
-- What it Does:    Temporarily disables the Options sub tab buttons. Don't want to allow player to trigger click spam the button on transition.
-- Purpose:         Clicking button too fast will be error prone. This prevents that.
local tempTabScript2 = {};
GRM.DisableSubTabButtons = function( toDisable )
    if toDisable then
        -- Storing the scripts
        tempTabScript2 = { GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralTab:GetScript ( "OnClick" ) , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ScanTab:GetScript ( "OnClick" ) , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncTab:GetScript ( "OnClick" ) , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_HelpTab:GetScript ( "OnClick" ) , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UITab:GetScript ( "OnClick" ) , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_OfficerTab:GetScript ( "OnClick" ) , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UXTab:GetScript ( "OnClick" ) , GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ModulesTab:GetScript ( "OnClick" ) };

        -- removing the script
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralTab:SetScript ( "OnClick" , nil );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ScanTab:SetScript ( "OnClick" , nil );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncTab:SetScript ( "OnClick" , nil );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_HelpTab:SetScript ( "OnClick" , nil );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UITab:SetScript ( "OnClick" , nil );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_OfficerTab:SetScript ( "OnClick" , nil );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UXTab:SetScript ( "OnClick" , nil );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ModulesTab:SetScript ( "OnClick" , nil );
    else
        -- restoring the script
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_GeneralTab:SetScript ( "OnClick" , tempTabScript2[1] );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ScanTab:SetScript ( "OnClick" , tempTabScript2[2] );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncTab:SetScript ( "OnClick" , tempTabScript2[3] );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_HelpTab:SetScript ( "OnClick" , tempTabScript2[4] );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UITab:SetScript ( "OnClick" , tempTabScript2[5] );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_OfficerTab:SetScript ( "OnClick" , tempTabScript2[6] );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_UXTab:SetScript ( "OnClick" , tempTabScript2[7] );
        GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ModulesTab:SetScript ( "OnClick" , tempTabScript2[8] );
    end
end

-- Method:              GRM.GR_Roster_Click ( string )
-- What it Does:        For logic on mouseover, instead of mouseover, it simulates a click on the item by bringing it to show.
--                      The "pause" is just a call to pause the hiding of the frame in the GRM_RosterFrame() function until it finds a new window (to prevent wasteful clicking and resource hogging)
-- Purpose:             Smoother UI interface in the built-in Guild Roster in-game UI default window.
GRM.GR_Roster_Click = function ( name )
    local time = GetTime();
    if GRM_G.timer3 == 0 or time - GRM_G.timer3 > 0.5 then   -- 500ms
        -- We are going to be copying the name if the shift key is down!

        if IsShiftKeyDown() and not GRM_G.RecursiveStop then

            if GetCurrentKeyBoardFocus() ~= nil then
                if GetCurrentKeyBoardFocus():GetName() ~= nil then
                    if "GRM_AddAltEditBox" == GetCurrentKeyBoardFocus():GetName() then
                        GetCurrentKeyBoardFocus():SetText ( name );
                    else
                        GetCurrentKeyBoardFocus():Insert ( GRM.SlimName ( name ) ); -- Adds it at the cursor position...
                    end
                end

                GRM_G.RecursiveStop = true;

                if GetCurrentKeyBoardFocus() ~= nil then
                    if GetCurrentKeyBoardFocus():GetName() ~= nil and GetCurrentKeyBoardFocus():GetName() == "GRM_AddAltEditBox" then
                        GRM.AddAltAutoComplete();
                        GRM_G.pause = true;
                    end
                end
            else
                -- Since player doesn't have keyboard focus, let's just default it to main chat window
                ChatFrame1EditBox:SetFocus()
                ChatFrame1EditBox:Insert ( GRM.SlimName ( name ) );
            end
        end
        GRM_G.timer3 = time;
    end
    GRM_G.RecursiveStop = false;
end

-- Method:          GRM.TriggerTrackingCheck()
-- What it Does:    Helps regulate some resource and timed efficient server queries, 
-- Purpose:         to keep from spamming or double+ looping functions.
GRM.TriggerTrackingCheck = function()
    if GRM_G.BuildVersion >= 30000 and GRM.IsCalendarEventEditOpen() then
        C_Timer.After ( 5 , GRM.TriggerTrackingCheck );                                             -- Check again in 5 seconds.
    else
        GRM_G.trackingTriggered = false;
        if GRM_G.BuildVersion < 80000 and not GRM_G.IntegrityTackingEnabled then
            GRM.TrackingIntegrityCheck();
        end
        GRM.GuildRoster();
        if GRM_G.BuildVersion >= 40000 then
            QueryGuildEventLog();
        end
    end
end

-- Method:          GRM.TrackingIntegrityCheck ( bool )
-- What it Does:    Loops and rechecks in an interval for integrity tha the scan went off appropriately.
-- Purpose:         Useful for Classic Guild Roster loop integrity check
GRM.TrackingIntegrityCheck = function( isLoop )

    if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].scanEnabled then          -- if Scanning is enabled
        if isLoop or not GRM_G.IntegrityTackingEnabled then
            GRM_G.IntegrityTackingEnabled = true;
            if ( time() - GRM_G.ClassicSyncControl ) > ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].scanDelay + 6 ) then
                GRM.GuildRoster();
            end

            C_Timer.After ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].scanDelay , function()
                GRM.TrackingIntegrityCheck( true );
            end);
        end
    else
        GRM_G.IntegrityTackingEnabled = false;
    end
end

-- Method:          GRM.IsCalendarEventEditOpen()
-- What it Does:    Does a check if the calendar windows are open as the GuildRoster() check can cause things to lose cursor focus
-- Purpose:         Prevent background scanning and updating from messing up some UI interactions, like losing cursor focus.
GRM.IsCalendarEventEditOpen = function()
    local result = false;
    if ( CalendarCreateEventFrame and CalendarCreateEventFrame:IsVisible() ) or ( CalendarViewEventFrame and CalendarViewEventFrame:IsVisible() ) then
        result = true;
    end
    return result;
end

---------------------------------------------
-------- BACKGROUND SCANNING ----------------
---------------------------------------------

-- Method:          GRM.UpdateMacroToolSafeListExpirations()
-- What it Does:    It rechecks the roster to see if any of the players are on the ignore lists from the macro tool and removes them if the time has expired.
-- Purpose:         To be able to auto remove players from the ignore lists as time expires. Quality of life tool
GRM.UpdateMacroToolSafeListExpirations = function()
    if IsInGuild() then
        if not GRM_G.SafeListExpirationChecking then
            GRM_G.SafeListExpirationChecking = true;

            for _ , player in pairs ( GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ] ) do
                if type ( player ) == "table" then
                    GRM.ValidateIgnoreExpireDates ( player );
                end
            end
                
            C_Timer.After ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].scanDelay + 5 , function()     -- +5 is to ensure they don't end up ont he same parallel track dual scanning every time.
                GRM_G.SafeListExpirationChecking = false;
                GRM.UpdateMacroToolSafeListExpirations();
            end);
        end
    else
        GRM_G.SafeListExpirationChecking = false;
    end
end

-- Method:          GRM.ValidateIgnoreExpireDates ( table )
-- What it Does:    It checks to see if their safeListSetting has expired, and if their time HAS expired, they are removed from the list.
-- Purpose:         This is the check that will be done to ensure rules are being monitored.
GRM.ValidateIgnoreExpireDates = function( player )

    if player then
        local time = time();

        for type , safeListSetting in pairs ( player.safeList ) do
            if safeListSetting[4] ~= 0 and safeListSetting[4] <= time then
                -- we know this is no longer valid and time has expired...
                
                if safeListSetting[2] then
                    if type == "kick" and CanGuildRemove() then
                        GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "{name} is now being monitored by the kick macro rules after being on the ignore list for {num} days." , GRM.GetClassifiedName ( player.name , false ) , nil , safeListSetting[3] ) );
                    elseif type == "promote" and CanGuildPromote() then
                        GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "{name} is now being monitored by the promote macro rules after being on the ignore list for {num} days." , GRM.GetClassifiedName ( player.name , false ) , nil , safeListSetting[3] ) );
                    elseif type == "demote" and CanGuildDemote() then
                        GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "{name} is now being monitored by the demote macro rules after being on the ignore list for {num} days." , GRM.GetClassifiedName ( player.name , false ) , nil , safeListSetting[3] ) );
                    end
                end

                player.safeList[type] = { false , false , 0 , 0 };

                if player.name == GRM_G.currentName and GRM_UI.GRM_MemberDetailMetaData.GRM_MacroToolIgnoreListSettingsFrame:IsVisible() then
                    GRM_UI.MacroIgnoreCheckBoxesFrame_OnShow();
                end
            end
        end
    end
end

---------------------------------------------
-------- SLASH COMMAND FUNCTIONS ------------
---------------------------------------------

-- Method:          GRM.SlashCommandScan()
-- What it Does:    Triggers a one-time scan of the guild for changes.
-- Purpose:         Mainly useful for people that wish to disable active scanning and just do a 1-time check on occasion.
GRM.SlashCommandScan = function()
    GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "Scanning for Guild Changes Now. One Moment..." ) );
    GRM_G.ManualScanEnabled = true;
    GRM.GuildRoster();
    if GRM_G.BuildVersion >= 40000 then
        QueryGuildEventLog();
    end
    C_Timer.After ( 5 , GRM.TriggerTrackingCheck );
end

-- Method:          GRM.SyncCommandScan()
-- What it Does:    Activates a one-time data sync with guildies
-- Purpose:         For people that want to sync data, but don't want it to be on all the time, just on occasion as they choose.
--                  Flexibility to the user!
GRM.SyncCommandScan = function()
    if GRM_G.HasAccessToGuildChat and not IsInGroup() then
        if time() - GRM_G.slashCommandSyncTimer > 13 then
            GRM_G.slashCommandSyncTimer = time();
            -- Enable Temporary Syncing...
            if not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled then
                GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled = true;
                GRM_G.TemporarySync = true;
                GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_SyncOptionsFrame.CheckButton:SetChecked ( true );
            end
            local breakingSync = false;
            
            if GRMsyncGlobals.currentlySyncing and GRMsync.IsPlayerDataSyncCompatibleWithAnyOnline() then
                breakingSync = true;
                if GRMsyncGlobals.IsElectedLeader then
                    if GRMsync.IsPlayerDataSyncCompatible ( GRMsyncGlobals.CurrentSyncPlayer ) then
                        GRM.Report ( GRM.L ( "Breaking current Sync with {name}." , GRM.SlimName ( GRMsyncGlobals.CurrentSyncPlayer ) ) );
                    else
                        GRM.Report ( GRM.L ( "Breaking current Sync with the Guild..." ) ); 
                    end
                else
                    if GRMsync.IsPlayerDataSyncCompatible ( GRMsyncGlobals.DesignatedLeader ) then
                        GRM.Report ( GRM.L ( "Breaking current Sync with {name}." , GRM.SlimName ( GRMsyncGlobals.DesignatedLeader ) ) );
                    else
                        GRM.Report ( GRM.L ( "Breaking current Sync with the Guild..." ) ); 
                    end
                end
            end
            GRM.Report ( GRM.L ( "Initializing Sync Action. One Moment..." ) );
            if not GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersFrame:IsVisible() then
                GRM.RegisterGuildAddonUsersRefresh();
            end
            C_Timer.After( 1 , GRMsync.Initialize );
            -- Now, let's add a brief delay, 3 seconds, to trigger sync again
            C_Timer.After ( 2 , function()
                if #GRM_G.currentAddonUsers == 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled then
                    GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "No Players Currently Online to Sync With..." ) );
                elseif not GRMsync.IsPlayerDataSyncCompatibleWithAnyOnline() then
                    if not GRMsyncGlobals.firstMessageReceived then
                        GRMsyncGlobals.firstMessageReceived = true;
                        GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "No Addon Users Currently Compatible for FULL Sync." ) .. "\n" .. GRM.L ( "Check the \"Sync Users\" tab to find out why!" ) );
                        if #GRM_G.currentAddonUsers > 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].exportAllRanks then
                            GRM.Report ( "     " .. GRM.L ( "You will still share some outgoing data with the guild" ) );
                        end
                    end
                else
                    if not breakingSync then
                        C_Timer.After ( 10 , function ()
                            if not GRMsyncGlobals.currentlySyncing and ( time() - GRMsyncGlobals.timeOfLastSyncCompletion >= 15 ) then
                                GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "Sync has failed to start. Please try again!" ) );
                            end
                        end);
                    end
                end
            end);
        else
            GRM.Report ( GRM.L ( "Please wait {num} more seconds before manually initiating the sync process again." , nil , nil , 15 - ( time() - GRM_G.slashCommandSyncTimer ) ) );
        end
    elseif GRM_G.HasAccessToGuildChat and IsInGroup() then
        GRM.Report ( GRM.L ( "SYNC is currently disabled while you are grouped. Due to server restricted addon to addon talk data caps, and in an effort to avoid clogging up the shared global comm space of all addons, sync will be temporarily restricted while grouped." ) );

    elseif not GRM_G.HasAccessToGuildChat then
        GRM.Report ( GRM.L ( "SYNC is currently not possible! Unable to Sync with guildies when guild chat is restricted." ) );
    end
end

-- Method:          GRM.SlashCommandCenter()
-- What it Does:    It Centers all of the windows, in case the player dragged them off the screen
-- Purpose:         Help keep frames organized. Just a necessary feature as someone is eventually going to say they tossed the frame off screen.
GRM.SlashCommandCenter = function()
    GRM_UI.GRM_RosterChangeLogFrame:ClearAllPoints();
    GRM_UI.GRM_RosterChangeLogFrame:SetPoint ( "CENTER" , UIParent );
    GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].CoreWindowPos = { "" , "" , 0 , 0 };

    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame:ClearAllPoints();
    GRM_UI.GRM_RosterChangeLogFrame.GRM_CoreBanListFrame.GRM_AddBanFrame:SetPoint ( "CENTER" , UIParent );

    GRM_UI.GRM_AuditJDTool:ClearAllPoints();
    GRM_UI.GRM_AuditJDTool:SetPoint ( "CENTER" , UIParent );

    GRM_UI.GRM_ToolCoreFrame:ClearAllPoints();
    GRM_UI.GRM_ToolCoreFrame:SetPoint ( "CENTER" , UIParent );

    GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame:ClearAllPoints();
    GRM_UI.GRM_ToolCoreFrame.GRM_ToolCustomRulesFrame:SetPoint ( "CENTER" , UIParent );
    
    GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].macroToolCoordinates = { "" , "" , 0 , 0 };
end

-- Method:          GRM.SlashCommandHelp()
-- What it Does:    Displays a list of all slash commands and what they do
-- Purpose:         To HELP the player with slash commands lol
GRM.SlashCommandHelp = function()
    
    GRM.Report ( "\n" .. GRM.L ( "Guild Roster Manager" ) .. " " .. GRM.L ( "(Ver:" ) .. " " .. GRM_G.Version .. ")\n\n/grm                     - " .. 
        GRM.L ( "Opens Guild Log Window" ) .. "\n/grm clearall         - " .. 
        GRM.L ( "Resets ALL saved data" ) .. "\n/grm clearguild      - " .. 
        GRM.L ( "Resets saved data only for current guild" ) .. "\n/grm center          - " .. 
        GRM.L ( "Re-centers the Log window" ) .. "\n/grm sync             - " ..
        GRM.L ( "Triggers manual re-sync if sync is enabled" ) .. "\n/grm export           - " ..
        GRM.L ( "Opens the Data Export Tool" ) .. "\n/grm macro           - " ..
        GRM.L ( "Opens the Advanced Macro Tool" ) .. "\n/grm tabName      - " ..
        GRM.L ( "Open any GRM window: Log, Event, Ban, etc." ) .. "\n/grm scan             - " .. 
        GRM.L ( "Does a one-time manual scan for changes" ) .. "\n/grm ver               - " .. 
        GRM.L ( "Displays current Addon version" ) .. "\n/grm hardreset      - " .. 
        GRM.L ( "WARNING! complete hard wipe, including settings, as if addon was just installed." ) );
end

-- Method:          GRM.SlashCommandClearAll()
-- What it Does:    Resets all data account wide, as if the addon was just installed, on the click of the button.
-- Purpose:         Useful to purge data in case of corruption or trolling or other misc. reasons.
GRM.SlashCommandClearAll = function()
    GRM_UI.GRM_RosterChangeLogFrame:EnableMouse( false );
    GRM_UI.GRM_RosterChangeLogFrame:SetMovable( false );
    GRM_UI.GRM_RosterConfirmFrameText:SetText( GRM.L ( "Really Clear All Account-Wide Saved Data?" ) );
    GRM_UI.GRM_RosterConfirmYesButtonText:SetText ( GRM.L ( "Yes!" ) );
    GRM_UI.GRM_RosterConfirmYesButton:SetScript ( "OnClick" , function( _ , button )
        if button == "LeftButton" then
            GRM.ResetAllSavedData();      --Resetting!
            GRM_UI.GRM_RosterConfirmFrame:Hide();
        end
    end);
    GRM_UI.GRM_RosterConfirmFrame:Show();
end

-- Method:          GRM.SlashCommandClearGuild()
-- What it Does:    Resets all data guild wide, as if the guild is brand new or newly joined.
-- Purpose:         Useful to purge the data if someone trolled the guild and made a mess of the data, 
-- or if there is a major error corrupting the data, but you don't want to wipe all account wide
GRM.SlashCommandClearGuild = function()
    GRM_UI.GRM_RosterChangeLogFrame:EnableMouse( false );
    GRM_UI.GRM_RosterChangeLogFrame:SetMovable( false );
    GRM_UI.GRM_RosterConfirmFrameText:SetText( GRM.L ( "Really Clear All Guild Saved Data?" ) );
    GRM_UI.GRM_RosterConfirmYesButtonText:SetText ( GRM.L ( "Yes!" ) );
    GRM_UI.GRM_RosterConfirmYesButton:SetScript ( "OnClick" , function( _ , button )
        if button == "LeftButton" then
            GRM.ResetGuildSavedData( GRM_G.guildName );      --Resetting!
            GRM_UI.GRM_RosterConfirmFrame:Hide();
        end
    end);
    GRM_UI.GRM_RosterConfirmFrame:Show();
end

-- Method:          GRM.HardReset()
-- What it Does:    It deletes player addon settings, thus the addon detects that and assumes 
--                  this is the first time any toon has logged in with addon installed and 
--                  triggers full reset and initialization.
-- Purpose:         To bypass all UI features and do a full hard reset, if the player needs
--                  This has no warning, so it is mainly for emergency resets, just in case
GRM.HardReset = function()

    local hardReset = function()
        GRM_AddonSettings_Save = nil;
        GRM_AddonSettings_Save = {};
        ReloadUI();
    end

    GRM.SetConfirmationWindow ( hardReset , GRM.L ( "Do you really want to wipe all GRM data and settings account-wide?" ) );
end

-- Method:          GRM.SlashCommandVersion()
-- What it Does:    Displays the version of the addon (all viewable with /roster help)
-- Purpose:         General info if wanted.
GRM.SlashCommandVersion = function()
    GRM.Report ( "\n" .. GRM.L ( "Guild Roster Manager" ) .. "\nVer: " .. GRM_G.Version .. "\n" );
end

-- Method:          GRM.SlashCommandMinimapReset()
-- What it Does:    Resets the minimap to default position
-- Purpose:         In case player drags the minimap off screen...
GRM.SlashCommandMinimapReset = function()
    GRM_UI.ResetMinimapPositionToDefault()
end

-- Method:          GRM.SlashCommandKick()
-- What it Does:    Opens or closes the mass kick window
-- Purpose:         Allow slash command use of the mass kick window for ease
GRM.SlashCommandKick = function()
    if ( time() - GRMsyncGlobals.timeAtLogin ) > 5 or GRM_G.MinimapOk then
        if GRM_UI.GRM_ToolCoreFrame:IsVisible() then
            GRM_UI.GRM_ToolCoreFrame:Hide();
        else
            GRM_UI.GRM_ToolCoreFrame:Show();
        end
    else
        GRM.Report ( GRM.L ( "GRM:" ) .. " " .. GRM.L ( "One moment, GRM is still being configured." ) );
    end
end

-- Method:          GRM.SlashCommandBan()
-- What it Does:    Opens the Ban window by slash command
-- Purpose:         Expanded slash command controls.
GRM.SlashCommandBan = function()
    if not GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then
        GRM_UI.GRM_RosterChangeLogFrame:Show();
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_BanListTab:Click();
end

-- Method:          GRM.SlashCommandAudit()
-- What it Does:    Opens the Audit window by slash command
-- Purpose:         Expanded slash command controls.
GRM.SlashCommandAudit = function()
    if not GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then
        GRM_UI.GRM_RosterChangeLogFrame:Show();
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_GuildAuditTab:Click();
end

-- Method:          GRM.SlashCommandLog()
-- What it Does:    Opens the Log window by slash command
-- Purpose:         Expanded slash command controls.
GRM.SlashCommandLog = function()
    if not GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then
        GRM_UI.GRM_RosterChangeLogFrame:Show();
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogTab:Click();
end

-- Method:          GRM.SlashCommandUsers()
-- What it Does:    Opens the Sync Users window by slash command
-- Purpose:         Expanded slash command controls.
GRM.SlashCommandUsers = function()
    if not GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then
        GRM_UI.GRM_RosterChangeLogFrame:Show();
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_AddonUsersTab:Click();
end

-- Method:          GRM.SlashCommandEvents()
-- What it Does:    Opens the Events window by slash command
-- Purpose:         Expanded slash command controls.
GRM.SlashCommandEvents = function()
    if not GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then
        GRM_UI.GRM_RosterChangeLogFrame:Show();
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_AddEventTab:Click();
end

-- Method:          GRM.SlashCommandOptions()
-- What it Does:    Opens the Options window by slash command
-- Purpose:         Expanded slash command controls.
GRM.SlashCommandOptions = function()
    if not GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then
        GRM_UI.GRM_RosterChangeLogFrame:Show();
    end
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsTab:Click();
end

-- Method:          GRM.SlashCommandExport()
-- What it Does:    Opens the export window
-- Purpose:         Give player access to feature
GRM.SlashCommandExport = function()
    GRM_UI.GRM_RosterChangeLogFrame.GRM_LogTab:Click();
    if not GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame:IsVisible() then
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame.TabPosition = 1;
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame:Show();
    else
        GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame:Hide();
    end
end

-- Method:          GRM.SlashCommandModulesOptions()
-- What it Does:    Opens the Module tab under Options
-- Purpose:         Easy slash command control to get here.
GRM.SlashCommandModulesOptions = function()
    GRM.SlashCommandOptions();
    GRM_UI.GRM_RosterChangeLogFrame.GRM_OptionsFrame.GRM_ModulesTab:Click();
end

-- Method:          GRM.InitiateConfirmFrame ( string , function , string , string )
-- What it Does:    Configures the generic popup window for confirmation of an action
-- Purpose:         Repeat use of the popup window without needing to keep copying and pasting the configuration window.
GRM.InitiateConfirmFrame = function ( InfoText , buttonFunction , button1Text , button2Text , cancelButtonFunction , disableCloseFunctionOnHide )
    GRM_UI.GRM_RosterChangeLogFrame:EnableMouse( false );
    GRM_UI.GRM_RosterChangeLogFrame:SetMovable( false );
    -- Configure info text
    if InfoText ~= nil and InfoText ~= "" then
        GRM_UI.GRM_RosterConfirmFrameText:SetText( InfoText );
    else
        GRM_UI.GRM_RosterConfirmFrameText:SetText ( "" );
    end
    -- Yes Button
    if button1Text ~= nil and button1Text ~= "" then
        GRM_UI.GRM_RosterConfirmYesButtonText:SetText ( button1Text );
    else
        GRM_UI.GRM_RosterConfirmYesButtonText:SetText ( GRM.L ( "Yes!" ) );
    end
    -- Cancel Button
    if button2Text ~= nil and button2Text ~= "" then
        GRM_UI.GRM_RosterConfirmCancelButtonText:SetText ( button2Text );
    else
        GRM_UI.GRM_RosterConfirmCancelButtonText:SetText ( GRM.L ( "Cancel" ) );
    end
    GRM_UI.GRM_RosterConfirmYesButton:SetScript ( "OnClick" , function( _ , button )
        if button == "LeftButton" then
            if buttonFunction ~= nil then
                buttonFunction();
            else
                GRM.Report ( GRM.L ( "No Action Configured" ) );
            end
        end
    end);

    -- If custom logic on cancel as well
    if cancelButtonFunction then
        GRM_UI.GRM_RosterConfirmCancelButton:SetScript ( "OnClick" , function ( _ , button )
            if button == "LeftButton" then
                GRM_UI.GRM_RosterConfirmFrame:Hide();
                cancelButtonFunction();
            end
        end);
        GRM_UI.GRM_RosterConfirmFrame:SetScript ( "OnHide" , function ()
            if not disableCloseFunctionOnHide then
                cancelButtonFunction();
            end
            GRM_UI.GRM_RosterChangeLogFrame:EnableMouse ( true );
            GRM_UI.GRM_RosterChangeLogFrame:SetMovable ( true );
        end);

    -- or just the default actions
    else
        GRM_UI.GRM_RosterConfirmCancelButton:SetScript ( "OnClick" , function ( _ , button )
            if button == "LeftButton" then
                GRM_UI.GRM_RosterConfirmFrame:Hide();
            end
        end);
        GRM_UI.GRM_RosterConfirmFrame:SetScript ( "OnHide" , function ()
            GRM_UI.GRM_RosterChangeLogFrame:EnableMouse ( true );
            GRM_UI.GRM_RosterChangeLogFrame:SetMovable ( true );
        end);
    end

    GRM_UI.GRM_RosterConfirmFrame:Show();
end

-- Method:          GRM.DebugConfig( string )
-- What it Does:    Enables debugging logging
-- Purpose:         To help debug issues of course, by logging them.
GRM.DebugConfig = function( command )
    if GRM_G.DebugEnabled and not string.find ( command , " " ) then
        GRM_G.DebugEnabled = false;
        GRM.Report ( GRM.L ( "GRM Debugging Disabled." ) );
    else
        if GRM_G.DebugEnabled then
            local number = GRM.Trim ( string.sub ( command , string.find ( command , " " ) + 1 ) );
            if string.find ( command, " " ) ~= nil and tonumber ( number ) ~= nil then
                GRM.DebugLog ( tonumber ( number ) );
            else
                GRM.Report ( GRM.L ( "Error: Debug Command not recognized." ) .. "\n" .. GRM.L ( "Format: \"/grm debug 10\"" ) );
            end
        else
            GRM_G.DebugEnabled = true;
            GRM.Report ( GRM.L ( "GRM Debugging Enabled." ) .. "\n" .. GRM.L ( "Please type \"/grm debug 10\" to report 10 events (or any number)" ) );
            if #GRM_G.currentAddonUsers> 0 and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncEnabled then
                GRM.Report ( GRM.L ( "You may want to temporarily disable SYNC in the options if you are debugging another feature." ) );
            end
        end
    end
end


-- SLASH COMMAND LOGIC
SlashCmdList["GRM"] = function ( input )
    -- if input is invalid or is just a blank info... print details on addon.
    local command;
    local alreadyReported = false;
    local inGuild = IsInGuild();
    if input ~= nil and string.lower ( input ) ~= nil then -- and string.find ( input , "forcepurge" , 1 , true ) == nil then  -- purge data may have name after, don't want to lowercase that
        command = string.lower ( input );
    end

    local openCoreWindow = function( isGeneric )
        local openExport = false;
        if IsInGuild() and GRM_UI.GRM_RosterChangeLogFrame ~= nil and not GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then
            openExport = true;
            GRM_UI.GRM_RosterChangeLogFrame:Show();
        elseif isGeneric and GRM_UI.GRM_RosterChangeLogFrame ~= nil and GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then
            GRM_UI.GRM_RosterChangeLogFrame:Hide();
        elseif GRM_UI.GRM_RosterChangeLogFrame == nil then
            GRM.Report ( GRM.L ( "Please try again momentarily... Updating the Guild Event Log as we speak!" ) );
        elseif not isGeneric and GRM_UI.GRM_RosterChangeLogFrame:IsVisible() then
            openExport = true;
        end

        return openExport;
    end

    if input == nil or input:trim() == "" then
        openCoreWindow( true );

    -- Clears all saved data and resets to as if the addon was just installed. The only thing not reset is the default settings.
    elseif command == "clearall" or command == "resetall" or command == GRM.L ( "clearall" ) then
        alreadyReported = true;
        GRM.SlashCommandClearAll();
    
    -- Clears all saved data specific to the guild...
    elseif command == "clearguild" or command == "resetguild" or command == GRM.L ( "clearguild" ) then
        if inGuild then
            GRM.SlashCommandClearGuild();
        end

    -- Does a hard reset of the entire database...
    elseif command == "hardreset" or command == GRM.L ( "hardreset" ) then
        GRM.HardReset();
    -- List of all the slash commands at player's disposal.
    elseif command == "help" or command == GRM.L ( "help" ) then
        alreadyReported = true;
        GRM.SlashCommandHelp();

    -- Version
    elseif command == "version" or command == "ver" or command == GRM.L ( "version" ) then
        alreadyReported = true;
        GRM.SlashCommandVersion();

    -- Resets the poisition of the window back to the center.
    elseif command == "reset" or command == "center" or command == GRM.L ( "center" ) then
        alreadyReported = true;
        GRM.SlashCommandCenter();
    
    -- Re-triggering SYNC
    elseif command == "sync" or command == GRM.L ( "sync" ) then
        if inGuild then
            GRM.SyncCommandScan()
        end

    -- For manual scan trigger!
    elseif command == "scan" or command == GRM.L ( "scan" ) then
        if inGuild then
            GRM.SlashCommandScan();
        end

    -- for resetting the minimap
    elseif command == "minimap" or command == GRM.L ( "minimap" ) then
        alreadyReported = true
        GRM.SlashCommandMinimapReset();

    elseif command == "ban" or command == GRM.L ( "ban" ) then
        GRM.SlashCommandBan();

    elseif command == "audit" or command == GRM.L ( "audit" ) then
        GRM.SlashCommandAudit();

    elseif command == "log" or command == GRM.L ( "log" ) then
        GRM.SlashCommandLog();

    elseif command == "kick" or command == GRM.L ( "kick" ) or command == "tool" or command == string.lower ( GRM.L ( "Tool" ) ) or command == "promote" or command == string.lower ( GRM.L ( "Promote" ) ) or command == "demote" or command == string.lower ( GRM.L ( "Demote" ) ) or command == "macro" or command == string.lower ( GRM.L ( "Macro" ) ) then
        GRM.SlashCommandKick();

    elseif command == "users" or command == "syncusers" or command == GRM.L ( "users" ) or command == GRM.L ( "syncusers" ) then
        GRM.SlashCommandUsers();
        
    elseif command == "event" or command == "events" or command == GRM.L ( "event" ) or command == GRM.L ( "events" ) then
        GRM.SlashCommandEvents();

    elseif command == "opt" or command == "option" or command == "options" or command == GRM.L ( "opt" ) or command == GRM.L ( "option" ) or command == GRM.L ( "options" )then
        GRM.SlashCommandOptions();

    elseif command == "export" or command == string.lower ( GRM.L ( "Export" ) ) then
        if openCoreWindow( false ) then
            GRM.SlashCommandExport();
        end

    elseif command == "module" or command == string.lower ( GRM.L ( "Module" ) ) or command == "plugin" or command == string.lower ( GRM.L ( "Plugin" ) ) then
        GRM.SlashCommandModulesOptions();

    -- FOR FUN!!!
    elseif command == "hello" or command == "sexy" then
        alreadyReported = true;
        GRM.Report ( "Arkaan is SEXY! Mmmm Arkaan! Super, ridiculously hot addon dev!" );
    -- Invalid slash command.
    elseif string.find ( command , "debug" ) ~= nil then
        GRM.DebugConfig( command );
    else
        alreadyReported = true;
        GRM.Report ( GRM.L ( "Invalid Command: Please type '/grm help' for More Info!" ) );
    end
    
    if not inGuild and not alreadyReported then
        GRM.Report ( GRM.L ( "{name} is not currently in a guild. Unable to Proceed!" , GRM.SlimName( GRM_G.addonUser ) ) );
    end
end

------------------------------------------------
------------------------------------------------
----- INITIALIZATION AND LIVE TRACKING ---------
------------------------------------------------
------------------------------------------------

-- Method:              GRM.InitiateMemberDetailFrame()
-- What it Does:        Event Listener, it activates when the Guild Roster window is opened and interface is queried/triggered
--                      "GuildRoster()" needs to fire for this to activate as it creates the following 4 listeners this is looking for: GUILD_ROSTER_UPDATE
-- Purpose:             Create an Event Listener for the Guild Roster Frame in the guild window ('J' key)
GRM.InitiateMemberDetailFrame = function ()
    if not GRM_G.FramesInitialized then
        GRM_G.FramesInitialized = true;

        -- Member Detail Frame Info
        GRM_UI.EstablishClassicFrames()                 -- For compatibility reasons
        GRM_UI.GR_MetaDataInitializeUIFirst( false ); -- Initializing Frames
        GRM_UI.GR_MetaDataInitializeUISecond( false ); -- To avoid 60 upvalue Lua cap, place them in second list.
        GRM_UI.GR_MetaDataInitializeUIThird( false ); -- Also, to avoid another 60 upvalues!
        
        GRM_G.UIIsLoaded = true;

        GRM_UI.DefaultFramesRecursiveInit ( false , false );
    end
end

-- Method:          GRM.AllRemainingNonDelayFrameInitialization()
-- What it Does:    Initializes general important frames that are not in relations to the guild roster window.
-- Purpose:         By walling this off, it allows far greater resource control rather than needing to initialize entire UI.
GRM.AllRemainingNonDelayFrameInitialization = function()
    
    UI_Events:RegisterEvent ( "PLAYER_LOGOUT" );
    
    -- UI_Events:RegisterEvent ( "UPDATE_INSTANCE_INFO" );
    UI_Events:HookScript ( "OnEvent" , function( _ , event )
        if event == "PLAYER_LOGOUT" then
            -- Save debugging log, up to 250 instances
            GRM_DebugLog_Save = GRM_G.DebugLog;

            if GRM_AddonSettings_Save[GRM_G.F] and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser] then
                -- Sync Addon Settings...
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncSettings then
                    GRM.SyncAddonSettings();
                end

                -- Backup Guild data!
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].allowAutoBackups then
                    GRM.AutoSetBackup();
                end
            end

            -- Clear the macro in case it hasn't been cleared yet (GRM tool is open on a reload or logout.)
            GRM.CreateMacro ( "" , "GRM_Tool" , "INV_MISC_QUESTIONMARK" , "CTRL-SHIFT-K" , true );
        end
    end);

end

-- Method:          GRM.CheckIfNeedToAddAlt()
-- What it Does:    Lets you know if the player is already on the list of alts, and returns the position of the guild in the table as well.
-- Purpose:         For alt auto-tagging for the addon.
GRM.CheckIfNeedToAddAlt = function()
    local result = true;

    if GRM_PlayerListOfAlts_Save[GRM_G.F][GRM_G.guildName] ~= nil and GRM_PlayerListOfAlts_Save[GRM_G.F][GRM_G.guildName][GRM_G.addonUser] ~= nil then
        result = false;
    end

    return result;
end

-- Method:          GRM.GetOldGuildName()
-- What it Does:    Returns the players' former guild name, which might be different due to a guild name change
-- Purpose:         Useful lookup tool when checking on a guild Namechange
GRM.GetOldGuildName = function()

    for guildName in pairs ( GRM_PlayerListOfAlts_Save[GRM_G.F] ) do        -- Go through all guilds
        for j in pairs ( GRM_PlayerListOfAlts_Save[GRM_G.F][guildName] ) do   -- go through each player in a guild

            if j == GRM_G.addonUser then
                return guildName;
            end

        end
    end

    return nil;
end


-- Method:          GRM.ConfigureGuild()
-- What it Does:    It establishes the properly formated guildName as well as the clubID
-- Purpose:         Why cycle through the guilds over and over again to find the position, when you can store the index of the database in the array with a simple global variable? Massive resource save!
GRM.ConfigureGuild = function()

    -- Configure the guild
    local guildName , _ , _ , server = GetGuildInfo ( "PLAYER" );

    if server ~= nil then
        GRM_G.guildName = guildName .. "-" .. string.gsub ( string.gsub ( server , "-" , "" ) , "%s+" , "" );
    else
        GRM_G.guildName = guildName .. "-" .. GRM_G.realmName;
    end

    if GRM_G.BuildVersion >= 80000 then
        if C_Club.GetGuildClubId() ~= nil then
            GRM_G.gClubID = C_Club.GetGuildClubId();
        end
    else
        GRM_G.gClubID = GRM.CreateCustomGUIDValue( GRM_G.guildName );
    end

    local guildData = GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ];

    -- Fixes for old guilds... this can only be done when on that actual guild, however.
    if guildData ~= nil then
        -- Guild info
        if guildData.grmNumRanks == 0 then
            if GuildControlGetNumRanks() ~= nil then
                guildData.grmNumRanks = GuildControlGetNumRanks();
            end
        end
        if GRM_G.gClubID ~= nil and ( guildData.grmClubID == nil or guildData.grmClubID == 0 ) then
            guildData.grmClubID = GRM_G.gClubID;
        end
        if guildData.grmCreationDate == "" then
            guildData.grmCreationDate = GRM_G.guildCreationDate;
        end

        -- Left player info
        if GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ].grmCreationDate == "" then
            GRM_PlayersThatLeftHistory_Save[ GRM_G.F ][ GRM_G.guildName ].grmCreationDate = GRM_G.guildCreationDate;
        end

        -- Backup
        if GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ] ~= nil then
            if GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ][1] == 0 or GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ][1] == "" then
                GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ][1] = GRM_G.guildCreationDate;
            end
        else
            GRM.FixBackups();
        end

        if GRM_G.FirstTimeViewed and GRM_G.IndexOfLastLogEntry == 0  then
            GRM_G.IndexOfLastLogEntry = #GRM_LogReport_Save[GRM_G.F][GRM_G.guildName];
        end
    end

end

-- Method:          GRM.FixBackups()
-- What it Does:    Fixes an old issue of guild backups not properly converting on a name change by rebuilding the backup.
-- Purpose:         To Fix guild backups.
GRM.FixBackups = function()
    GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ] = { GRM_G.guildCreationDate };                                       -- Creates a backup index for this guild
    local type = "Auto";
    for i = 1 , 2 do
        if i == 2 then
            type = "Manual";
        end
        GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ][type] = {};
        GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ][type]["date"] = "";
        GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ][type]["epochDate"] = 0;
        GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ][type]["members"] = {};
        GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ][type]["formerMembers"] = {};
        GRM_GuildDataBackup_Save[ GRM_G.F ][ GRM_G.guildName ][type]["log"] = {};
    end
end

-- Method:          GRM.InitializePreCheck()
-- What it Does:    Checks if the intial changes and updates are all recorded, including the status of all players who have left or joined the guild
-- Purpose:         You do not want to trigger and initialize the addon to sync with others until this initial process is completed after login. It could take just a few seconds to complete
--                  particularly due to players who have removed many many from the guild and have limited friends slot places to determine their server status with the add frienc, check, remove trick
GRM.InitializePreCheck = function( recursive )
    if not GRM_G.InitializePreCheck or recursive then
        GRM_G.InitializePreCheck = true;
        if not GRM_G.OnFirstLoad then
            GRM_G.InitializePreCheck = false;
            C_Timer.After ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].syncDelay , function()
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].autoTriggerSync then
                    GRMsync.Initialize();
                end
            end);
        else
            C_Timer.After ( 1 , function()
                GRM.InitializePreCheck( true )
            end);
        end
    end
end

-- Method:          Tracking()
-- What it Does:    Checks the Roster once in a repeating time interval as long as player is in a guild
-- Purpose:         Constant checking for roster changes. Flexibility in timing changes. Default set to 10 now, could be 30 or 60.
--                  Keeping local
GRM.Tracking = function()
    if IsInGuild() and not GRM_G.trackingTriggered then
        GRM_G.trackingTriggered = true;
        GRM_G.currentlyTracking = true;             -- To prevent repeat checks.
        local timeCallJustOnce = time();
        if GRM_G.timeDelayValue == 0 or ( ( ( timeCallJustOnce - GRM_G.timeDelayValue ) >= GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].scanDelay ) and ( ( timeCallJustOnce - GRM_G.timeDelayValue ) > 2 ) ) then -- Initial scan is zero.
            GRM_G.timeDelayValue = timeCallJustOnce;
            
            -- Add an escape if necessary due to unloaded data points. It will try again in 10 seconds or less, whenever the server calls back.
            if GRM_G.guildCreationDate == "" or not GRM_G.NumberOfHoursTilRecommend["kick"] or not GRM_G.NumberOfHoursTilRecommend["kickActive"] or ( GRM_G.BuildVersion >= 80000 and GRM_G.gClubID == 0 ) then
                GRM.DelayForGuildInfoCallback();
                return
            end
            -- Establish proper database tags before building and scanning roster data
            -- For massive resourcing saving, let's establish core data points.
            if GRM_G.guildName == "" or GRM_G.gClubID == 0 then
                GRM.ConfigureGuild();
            end

            if GRM_G.OnFirstLoad and GRM_GuildMemberHistory_Save[ GRM_G.F ][ GRM_G.guildName ] ~= nil then
                if not GRM_G.IsNewToon then
                        GRM.SyncAddonSettings();
                else
                    GRM.SyncAddonSettingsOfNewToon();
                end
            end
           
            -- -- Checking Roster, tracking changes
            if not GRM_UI.GRM_ToolCoreFrame:IsVisible() and not GRM_UI.GRM_RosterChangeLogFrame.GRM_ExportLogBorderFrame:IsVisible() then       -- Temporarily disable when exporting data
                GRM_G.ClassicSyncControl = time();
                if not GRMsyncGlobals.currentlySyncing then
                    GRM.BuildNewRoster();
                end
            end

            -- Prevent from re-scanning changes
            -- On first load, bring up window.
            if GRM_G.OnFirstLoad then

                GRM.ConfigureOnlineStatusText();

                GRM.ScanRecommendationsList();

                GRM.UpdateGuildLeaderPermissions( false , false );

                GRM.UpdateMacroToolSafeListExpirations();

                -- Determine if player has access to guild chat or is in restricted chat rank
                GRM.RegisterGuildChatPermission();
           
                -- Determine if player is already listed as alt...
                if GRM.CheckIfNeedToAddAlt() then
                    GRM.AddPlayerToOwnAltList();
                end

                -- Establish Message Sharing as well!
                GRMsyncGlobals.SyncOK = true;
                
                -- Only trigger a sync on relog.
                GRM.InitializePreCheck();

                -- MISC frames to be loaded immediately, not on delay
                GRM.AllRemainingNonDelayFrameInitialization();

                -- Some redundancy to protect against some edge cases that happen when server doesn't fire guild updates due to lag so loop is missed. This just gets it back on track.
                if not GRM_G.ScanIntegrityCheckInitialized then
                    GRM_G.ScanIntegrityCheckInitialized = true;
                    GRM_G.TimeAtCompletion = time();

                    GRM.ScanIntegrityCheck();
                end

                -- Open the core addon frame...
                if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].viewOnLoad and not GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].onlyViewIfChanges then
                    GRM_UI.GRM_RosterChangeLogFrame:Show();
                end

            end
        end
        if GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].scanEnabled then
            C_Timer.After( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].scanDelay , GRM.TriggerTrackingCheck ); -- Recursive check every X seconds. + 0.1 
        end
    elseif not IsInGuild() then
        GRM_G.currentlyTracking = false;
    end
end

-- Method:          GRM.ScanIntegrityCheck()
-- What it Does:    Adds an extra layer redundancy for some edge cases that could cause scanning to stop
-- Purpose:         Ensure scanning continues for some edge case reasons in regards to server just occasionally not firing certain events reliably.
GRM.ScanIntegrityCheck = function()
    if IsInGuild() and ( time() - GRM_G.TimeAtCompletion ) > GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].scanDelay then
        if GRM_G.BuildVersion < 30000 or ( GRM_G.BuildVersion >= 30000 and not GRM.IsCalendarEventEditOpen() ) then
            GRM.GuildRoster();
            if GRM_G.BuildVersion >= 40000 then
                QueryGuildEventLog();
            end
        end
    end
    C_Timer.After ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].scanDelay + 10 , GRM.ScanIntegrityCheck );
end

-- Method:          GRM.DelayForGuildInfoCallback()
-- What it Does:    It basically recursively waits til the conditions are met and the server properly retrieved the guildCreationDate
-- Purpose:         If a guild is on more than one server with the same name, that can complicate things. This helps idenitfy the server by the creation date as well...
GRM.DelayForGuildInfoCallback = function()
    if GRM_G.guildCreationDate == "" then
        GRM_G.DelayCount = GRM_G.DelayCount + 1;
        if GRM_G.DelayCount == 5 then               -- At the 5th try, let's check if system messages are disabled. If they are, tell player to enable.
            GRM.SystemMessageEnabledCheck();
        end
        GRM.SetGuildInfoDetails();
        GRM.GuildRoster();
        C_Timer.After ( 1 , GRM.DelayForGuildInfoCallback );
    elseif GRM_G.NumberOfHoursTilRecommend["kick"] == nil or GRM_G.NumberOfHoursTilRecommend["kickActive"] == nil then
        GRM.RefreshNumberOfHoursTilRecommend();

        C_Timer.After ( 1 , GRM.DelayForGuildInfoCallback );

    elseif GRM_G.BuildVersion >= 80000 and GRM_G.gClubID == 0 then
        if C_Club.GetGuildClubId() ~= nil then
            GRM_G.gClubID = C_Club.GetGuildClubId();
        end
        C_Timer.After ( 1 , GRM.DelayForGuildInfoCallback );

    else
        GRM_G.timeDelayValue = 0;
        GRM_G.trackingTriggered = false;
        GRM.Tracking();
    end
end

-- Method:          GRM.MessageHookControl()
-- What it Does:    checks to ensure script modification is post all other addons to ensure compatibility
-- Purpose:         Simple solution than writing a whole new Raw Hook control and updating the AddMessage text which can be spammy.
GRM.MessageHookControl = function()
    local chatEvents = { "CHAT_MSG_GUILD" , "CHAT_MSG_WHISPER" , "CHAT_MSG_GUILD_ACHIEVEMENT" , "CHAT_MSG_PARTY" , "CHAT_MSG_PARTY_LEADER" , "CHAT_MSG_RAID", "CHAT_MSG_RAID_LEADER" , "CHAT_MSG_INSTANCE_CHAT" , "CHAT_MSG_INSTANCE_CHAT_LEADER" , "CHAT_MSG_OFFICER" }

    if GRM_G.MainHookConfigured then
        for i = 1 , #chatEvents do
            local events = ChatFrame_GetMessageEventFilters( chatEvents[i] );

            if #events > 1 then
                if events[#events] ~= GRM.AddMainToChat then
                    -- Let's unregister, then re-register!
                    ChatFrame_RemoveMessageEventFilter ( chatEvents[i] , GRM.AddMainToChat );
                    -- Now Re-Add it!
                    ChatFrame_AddMessageEventFilter ( chatEvents[i] , GRM.AddMainToChat );
                end
            end
        end
    else
        GRM_G.MainHookConfigured = true;
        for i = 1 , #chatEvents do
            ChatFrame_AddMessageEventFilter ( chatEvents[i] , GRM.AddMainToChat );
        end
    end
end

-- Method:          GRM.SystemMessageHookControl()
-- What it Does:    Checks to ensure script modification happens at the end of the sequential table to ensure all addon compatibility
-- Purpose:         Quality of life - prevent frustration for other addon devs
GRM.SystemMessageHookControl = function()

    local events = ChatFrame_GetMessageEventFilters( "CHAT_MSG_SYSTEM" );

    if #events > 1 then
        if events[#events] ~= GRM.SetSystemMessageFilter then
            -- Let's unregister, then re-register!
            ChatFrame_RemoveMessageEventFilter ( "CHAT_MSG_SYSTEM" , GRM.SetSystemMessageFilter );
            -- Now Re-Add it!
            ChatFrame_AddMessageEventFilter ( "CHAT_MSG_SYSTEM" , GRM.SetSystemMessageFilter );
        end
    end
end

-- Method:          GRM.GR_LoadAddon()
-- What it Does:    Enables tracking of when a player joins the guild or leaves the guild. Also fires upon login.
-- Purpose:         Manage tracking guild info. No need if player is not in guild, or to reactivate when player joins guild.
GRM.GR_LoadAddon = function()
    
    GeneralEventTracking:RegisterEvent ( "PLAYER_GUILD_UPDATE" ); -- If player leaves or joins a guild, this should fire.
    GeneralEventTracking:SetScript ( "OnEvent" , GRM.ManageGuildStatus );

    KickAndRankChecking:RegisterEvent ( "CHAT_MSG_SYSTEM" );
    if GRM_G.BuildVersion >= 80000 then
        KickAndRankChecking:RegisterEvent ( "CLUB_MEMBER_ADDED" );
    end
    KickAndRankChecking:SetScript ( "OnEvent" , GRM.KickPromoteOrJoinPlayer );
    
    -- Initialize chat tagging
    GRM.MessageHookControl();

    -- Quick Version Check
    if not GRM_G.VersionCheckRegistered then
        GRM.RegisterVersionCheck();
        C_ChatInfo.SendAddonMessage ( "GRMVER" , GRM_G.Version.. "?" .. tostring ( GRM_G.PatchDay ) , "GUILD" );
        GRM_G.VersionCheckRegistered = true;
    end

    if not GuildFrame then
        LoadAddOn ( "Blizzard_GuildUI" );
    end

    -- Modular load control
    GRM_G.GRMfunctionDisabled = false;

    -- Delay needs to be here to try to help prevent any initialization errors that might occur for some people.
    if GRM_G.BuildVersion >= 80000 and not CommunitiesFrame then
        C_Timer.After ( 3 , function()
            LoadAddOn ( "Blizzard_Communities" );
            GRM.LoadRecursiveErrorCheck();
        end);
    else
        GRM.LoadRecursiveErrorCheck();
    end
    
end

-- Method:          GRM.LoadRecursiveErrorCheck()
-- What it Does:    Rechecks if a certain frame function is loaded. 
-- Purpose:         For some reason some edge cases out there some clients load these very slow, and an addon can trigger before this is done.
GRM.LoadRecursiveErrorCheck = function()
    if UIDropDownMenu_CreateInfo() == nil then
        C_Timer.After ( 3 , function()
            GRM.LoadRecursiveErrorCheck();
        end);
    else
        GRM.finalLoadSteps();
    end
end

-- Method:          GRM.finalLoadSteps()
-- What it Does:    Throttles loading information
-- Purpose:         Compartmentalizes and throttles loading details until the frames are loaded by the system so as to prevent attempts to hook into frames that do not yet exist
GRM.finalLoadSteps = function()
        -- Determine who is using the addon...
    -- 3 second dely to account for initialization of various variables. Safety cushion.
    C_Timer.After ( 3 , GRM.RegisterGuildAddonUsers );

    -- The following event registartion is purely for UI registeration and activation... General tracking does not need the UI, but CommunitiesFrame should be visible bnefore triggering
    -- Each of the following events might trigger on event update.

    if GRM_G.BuildVersion >= 40000 then
        UI_Events:RegisterEvent ( "GUILD_EVENT_LOG_UPDATE" );
    else
        UI_Events:RegisterEvent ( "GUILD_ROSTER_UPDATE" );
    end

    UI_Events:SetScript ( "OnEvent" , function ( _ , event )
        if ( GRM_G.BuildVersion >= 40000 and event == "GUILD_EVENT_LOG_UPDATE" ) or ( GRM_G.BuildVersion < 40000 and event == "GUILD_ROSTER_UPDATE" ) then
            GRM.Tracking();
        end
    end);

    -- Important to set this in case player logs in whilst in a group
    if IsInGroup() then
        GRM_G.GroupSyncRestrictionControl = true;
    end

    if GRM_G.BuildVersion >= 40000 then
        GRM.GuildRoster();
        QueryGuildEventLog();
    else
        GRM.GuildRoster();
        GRM.Tracking();
    end

    -- Activate the GRM frames!
    GRM.InitiateMemberDetailFrame();
end

-- Method:          GRM.ReactivateAddon ()
-- What it Does:    If addon no longer needs to be enabled due to player not being in a guild, or leaving a guild, this slimmer reactivation protocol
--                  is necessary because it doesn't need to re-register frames like it would on the first activation upon logging in.
-- Purpose:         Resource efficiency.
GRM.ReactivateAddon = function()
     if GRM_G.faction == nil then
        GRM_G.faction = UnitFactionGroup ( "PLAYER" );
    end

    if GRM_G.faction == "Horde" then
        GRM_G.FID = 1;
        GRM_G.selectedFID = "H";
        GRM_G.F = "H";
    else
        GRM_G.FID = 2;
        GRM_G.selectedFID = "A";
        GRM_G.F = "A";
    end

    if GRM_G.F == "" then
        if GRM_G.faction == "Horde" then
            GRM_G.F = "H";
            GRM_G.selectedFID = "H";
        else
            GRM_G.F = "A";
            GRM_G.selectedFID = "A";
        end
    end

    C_Timer.After ( 5 , GRM.RegisterGuildChatPermission );

    GRM.SetGuildInfoDetails();
    GRM.GuildRoster();
    if GRM_G.BuildVersion >= 40000 then
        QueryGuildEventLog();
    end
    
    C_Timer.After ( 2 , GRM.GR_LoadAddon );
end

-- Method           GRM.ManageGuildStatus()
-- What it Does:    If player leaves or joins the guild, it deactivates/reactivates tracking - as well as re-checks guild to see if rejoining or new guild.    
-- Purpose:         Efficiency in resource use to prevent unnecessary tracking of info if out of the guild.
GRM.ManageGuildStatus = function ()
    GeneralEventTracking:UnregisterEvent ( "PLAYER_GUILD_UPDATE" );
    if GRM_G.guildStatusChecked ~= true then
       GRM_G.timeDelayValue2 = time(); -- Prevents it from doing "IsInGuild()" too soon by resetting timer as server reaction is slow.
    end
    if GRM_G.timeDelayValue2 == 0 or ( time() - GRM_G.timeDelayValue2 ) >= 2 then -- Let's do a recheck on guild status to prevent unnecessary scanning.
        if IsInGuild() then
            if GRM_G.DelayedAtLeastOnce then
                if not GRM_G.currentlyTracking then
                    GRM.ReactivateAddon();
                    -- return
                end
            else
                GRM_G.DelayedAtLeastOnce = true;
                C_Timer.After ( 5 , GRM.ManageGuildStatus );
                -- return
            end
        else
            -- Reset some values;
            if not GRM_G.GRMfunctionDisabled then
                GRM_G.GRMfunctionDisabled = true;
                GRMsyncGlobals.SyncOK = false;
                GRM_G.IndexOfLastLogEntry = 0;
                GRM_G.IndexOfLastTable = "";
                GRM_G.timeDelayValue = 0;
                GRM_G.OnFirstLoad = true;
                GRM_G.guildName = "";
                GRM_G.guildCreationDate = "";
                GRM_G.trackingTriggered = false;
                GRM_G.currentlyTracking = false;
                GRM_G.DelayedAtLeastOnce = true;                     -- Keeping it true as there does not need to be a delay at this point.
                GRM_G.IsNewToon = true;                             -- Ensures if a player leaves a guild and joins another, it syncs the new settings to the guild
                GRM_G.guildRankNames = nil;                         -- reset guild rank names.

                if GRM_G.BuildVersion >= 40000 then
                    UI_Events:UnregisterEvent ( "GUILD_EVENT_LOG_UPDATE" );         -- This prevents it from doing an unnecessary tracking call if not in guild.
                end
                if GRMsync.MessageTracking ~= nil then
                    GRMsync.MessageTracking:UnregisterAllEvents();
                end
                GRMsync.ResetDefaultValuesOnSyncReEnable();                     -- Need to reset sync algorithm too!
                GRM_UI.GRM_RosterChangeLogFrame:Hide();

                -- Modules enabling and Disabling
                GRM.DisableModulesIfLeavingGuild();
            end
        end
        GeneralEventTracking:RegisterEvent ( "PLAYER_GUILD_UPDATE" );
        GeneralEventTracking:SetScript ( "OnEvent" , GRM.ManageGuildStatus );
        GRM_G.guildStatusChecked = false;
    else
        GRM_G.guildStatusChecked = true;
        C_Timer.After ( 2 , GRM.ManageGuildStatus ); -- Recursively re-check on guild status trigger.
    end
end

-- Method:          ActivateAddon( self , string , string )
-- What it Does:    First, doesn't trigger to load until all variables of addon fully loaded.
--                  Then, it triggers to delay until player is fully in the world, in that order.
--                  Finally, it delays 5 seconds upon querying server as often initial Roster and Guild Event Log query takes a moment to return info.
-- Purpose:         To ensure the smooth handling and loading of the addon so all information is accurate before attempting to parse guild info.
GRM.ActivateAddon = function ( _ , event , addon , isReload )
    if event == "ADDON_LOADED" then
    -- initiate addon once all variable are loaded.
        if addon == GRM_G.addonName then
            Initialization:RegisterEvent ( "PLAYER_ENTERING_WORLD" ); -- Ensures this check does not occur until after Addon is fully loaded. By registering, it acts recursively throug hthis method
        end
    elseif event == "PLAYER_ENTERING_WORLD" then
        -- Initialize load settings! Don't need to be in a guild for this!
        -- Setting the index of the player's faction.
        if GRM_G.faction == nil then
            GRM_G.faction = UnitFactionGroup ( "PLAYER" );
        end

        if GRM_G.faction == "Horde" then
            GRM_G.FID = 1;
            GRM_G.selectedFID = "H";
            GRM_G.F = "H";
        else
            GRM_G.FID = 2;
            GRM_G.selectedFID = "A";
            GRM_G.F = "A";
        end

        if GRM_G.F == "" then
            if GRM_G.faction == "Horde" then
                GRM_G.F = "H";
                GRM_G.selectedFID = "H";
            else
                GRM_G.F = "A";
                GRM_G.selectedFID = "A";
            end
        end

        if isReload then
            GRMsyncGlobals.reloadControl = true;
        end

        GRM.DataLoadDelayProtection();
    end
end

-- Method:          GRM.DataLoadDelayProtection()
-- What it Does:    It checks if the calendar and date info is available from the server yet and if not, it recursively reloads
-- Purpose:         To prevent certain errors due to the server returning this information slowly as of patch 8.1.5 for some reason.
GRM.DataLoadDelayProtection = function()
    if GRM.GetCurrentCalendarTime().month ~= 0 and GRM.GetCurrentCalendarTime().month ~= nil then       -- Critical to be receiving data properly from the server...
        GRM.LoadSettings();
    else
        C_Timer.After ( 0.2 , GRM.DataLoadDelayProtection );
    end
end;

-- Method:          GRM.SettingsLoadedFinishDataLoad()
-- What it Does:    Compartmentalizes the OnLoad process of the addon so some things can be verified and pre-checked before continuing, like loading addon settings.
-- Purpose:         Prevent errors of course!
GRM.SettingsLoadedFinishDataLoad = function()
    -- Rerun this for the language changes...
    -- this will also build initial frames...
    GRML.SetNewLanguage ( GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].selectedLang , true , false );

    -- Restore debugLog since addonloaded
    GRM_G.DebugLog = GRM_DebugLog_Save;

    -- MISC Quality of Life Settings...
    -- Addon Compatibility Detection
    -- EPGP uses officer notes and is an incredibly popular addon. This now ensures auto-adding not will default to PUBLIC note rather than officer.
    if IsAddOnLoaded("epgp") and GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination ~= 3 then
        GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].joinDateDestination = 2;
        
    end

    if IsInGuild() then
        Initialization:UnregisterEvent ("PLAYER_ENTERING_WORLD");
        GRM.GuildRoster();                                       -- Initial queries...
        if GRM_G.BuildVersion >= 40000 then
            QueryGuildEventLog();
        end
        C_Timer.After ( 2 , GRM.GR_LoadAddon );                 -- Queries do not return info immediately, gives server a 5 second delay.
    else
        GRM.ManageGuildStatus();
    end
end

-- Method:          GRM.DisableModulesIfLeavingGuild()
-- What it Does:    Disables all the GRM plugin modules if player is no longer in group
-- Purpose:         To house all of the module disable calls in one function
GRM.DisableModulesIfLeavingGuild = function()
    
    if GRM_G.Module.GroupInfo ~= nil and GRM_G.Module.GroupInfo then
        GRM.DisableGroupInfoModule();
    end
end

-- Method:          GRM.GetNumModules()
-- What it Does:    Returns the int num of plugin modules for GRM that are enabled
-- Purpose:         Useful information and to display the modules tab properly if necessary
GRM.GetNumModules = function()
    local num = 0;
    for _ in pairs ( GRM_G.Module ) do
        num = num + 1;
    end

    return num;    
end


-- Initialize the first frames as game is being loaded.
Initialization:RegisterEvent ( "ADDON_LOADED" );
Initialization:SetScript ( "OnEvent" , GRM.ActivateAddon );


-- Macro verification of live pickup of rank change.

        -- First, collect names of people in macro
        -- Collect names of people who system message fires for change
        -- any missing? if so, keep count
        -- There is no need to rebuild a missing yet. However, at the end, at the final step, rather than rebuilding the queue, if count > 0, you know there was one failure, so force a scan immediately... like you would a /grm scan. Once scan completes then rebuild...



-- Macro Rules button positioning is all over the place when setting a custom note.

-- REDO all of the "CLICK" localizations

-- Make a video guide for macro tool

-- Redo current #news global controls video explaining the guild Info tag in more detail

-- full Slash Command List




-- Radio button for the "Send outgoing data"

-- 1) Receive changes from selected rank and above, send changes to everyone.
-- 2) Receive and Send changes to and from the selected rank and above only.


-- Auto set verified join date when you self join a guild.

-- Control-click player's name and chat to auto-open roster

-- Ability to have it just show "AltName <A>" in chat, instead of including the main.







-- Custom text template on new joins - for custom note.  -  "has Discord: <yes/no>"
-- like in configuration you can have a list of "Discord (Y/N)" with a checkbox next to it, and maybe a few other "default" choices - then a way to add custom
-- like "Likes sandwiches (Y/N)"
-- And anything that scans in the custom note as (Y/N) means it hasn't been answered yet

-- Filter "Include all alts on note check" in the rules

-- when unbanning a ban, add option to unban the alts as well.

-- Auto add join date based on main's - Use Main's join date as newly added Alt Join date - only works on verified...

-- Use Main's join date as newly added Alt Join date


-- Only rules that will be shared are ones that are valid for player's rank.

-- Auto select the proper font when adjusting the language.

-- In audit show who invited who - start Trackign who invited the player

-- Move all players in a grouping to the same rank as main

-- SYNC rules to other officers...

--I'm not sure if this behavior already exists cause it seems like it'd be a pretty niche scenario, but it might be useful to have GRM throw a warning if an "alt" and its "main" are online at the same time, since that'd mean (unless they're using more than one account)  there's an error with the way you've tagged someone.
-- Maybe just a non-obtrusive log entry WARN: $playername is listed as an alt of $mainname, but both are online at the same tim

-- Add an ignore all to events tab


-- I might suggest adding a line between the Changes & Old Log line that's only displayed when there's something to show but can't because the To Log portion isn't selected.
-- (bascally where there are unchecked lines so it says "New Lines" and "Old Lines")

-- Add message indicating that ranks can only move 1, due to macro limitations by Blizz.



-- -- ***BUSY WORK***

-- Something like:
-- GRM: $playername is online. They have incomplete entries!

-- GRM: $playername is online. Missing data: Main/Alt

-- Double dates getting set into some officer notes

-- no i myself invited the person/char .. then waited for a possibility where the addon detects the unique counter increase, which didnt happen .. then the char whispered me telling it was an alt of main X .. so went into grm -> audit tab -> pulled char info by shift-clicking -> clicked Add Alt, entered name - autocomplete showed the correct char -> clicked it so the field was populated -> then hit [enter] and waited for the other chars to appear under 'Player alts'  .. nothing happened ... redid the part from 'click Add Alt -button' and then the linkage was there.

-- Add ability to export Ban list

-- GRM one time message: GRM can auto-backup your guild data. This is NOT a file backup, so please backup your WTF folder on your own. This backup is for redundancy in case you ever have an officer go rogue and nefariously change a lot data, giving you the opportunity to restore it easily. While this may seem like an obvious thing to keep enabled, be aware that it can massively increase the memory use of the footprint, especially if you are a member of multiple guilds as GRM stores data account-wide. This can be disabled at any time and the backups can be deleted as well. Do you wish to enable this feature at this time? {Y/N}
    -- Consider a mass control where guild leader can make all of their settings the most current.

-- Add an option to swap the fonts for the notes or let GRM do it auto

-- EXPORT: 'Excel: Paste data into Excel, Click the Data Tab, Click "Text to Columns" '
-- 'Google Sheets: Paste data into Google Sheets, Click the Data Tab, Click Split "text to columns" ' 

-- Ability to import alt from the notes





-- Unable to exxport cert note formats. (unable to recreate)

-- Advance join date tool date format not being detected.


-- Store why the person left, kicked, inactive, removed and so on and report that in the log when they rejoin,.

-- Export have a dropdown selection of all guilds in DB


--/GRM error - disable all addons but GRM


-- Add an export of just a specific player (possibly right click option on player name)



-- https://github.com/rossnichols/LibSerialize



-- Ability to rearrange the order of the rules.


-- Slash command to force all epoch stamps for sync purposes to be today's date... mass tool for sync control power.

-- Slash command to mass set some settings?

-- "Exclude text match"  -- SAFE TAG -- 

-- Sync rules to other officers.

-- /grm search -- bring up Classic window with search box.

-- Add nickname functionality

-- Adjust name formatting = GRM_AddonSettings_Save[GRM_G.F][GRM_G.addonUser].useFullName

-- Make it more obvious how to set main (new video)

-- Add to the FAQ on bug reporting.


-- * Popup window for first-time users with video link to youtube vid guide * only if they can EditGuildInfo ( Also create setup guide for first time users :D)

-- * On Sync request, check for a GRM user of higher rank, not just time, possibly designate new syncLeader

-- * Player Query by GUID

-- * Snapping back on the scrolling. Click the player locks the player, but it continues to allow mouseover scrolling, and snaps back when you scroll off... but lock is released if t he player's name is scrolled off.

-- Safe list sync standalone.
    -- Check if player is the same

-- Change how all the Log reporting is... show the fullName on merged realms and show 



-- Statistics Panel And Module
--  -- Different Graph formats, to list to a histogram

-- Stop it form trying to build kick macro while in combat. Reload when combat ends..

-- Issue possibly with Bartender addon ??

-- ClEANUP BuildLogFrames() and compartmentalize each one to their proper page for ease...

-- Possible issue with the advanced join date tool YYYY-MM-DD  sometimes not parsing right and detecting as not found. - Rebuild the parsing of the advanced JD tool

-- Swap the last 2 steps in the Advanced JD tool
-- REBUILD JD tool logic.

-- Right click on macro tool to set ignore
-- Right click to set as alt... popup search box?
-- Have it say "Arkaan's x year anniversary" rather than just Arkaan's anniversary

-- Custom rule that applies to promotions and demotions but only to people still in the raid?




-- Show rank of player when they leave the guild as well.

-- Promotion/Demotions - Group rankings in log?

-- Allow export of main to player note - or really still build independent addon to handle it all. (Note building feature)


-- Option to extend the guild Info.

-- Syncing ranks of alt grouping to same rank

-- Right click options on the log, like BANNING a player if they leave the guild


-------------------------------------
---- SUGGESTIONS FOR RECRUITMENT ----
-------------------------------------

-- Sync ignored lists.

-- Warning on how many days left on recruitment window, or if it has expired

-- Option to allow the spamming of messages

-- Indicator that a player has been invited by WHO but they are offline ... like X players have been invited to the guild but are currently offline or not accepted.
-- player1, player2, etc... joined while you were offline.

-- X players have requested to join the guild

-- Possibly refresh the timer on recruitment window listing.

-- Popup to install GRM plugin if officer one time.

-- Warning that player they are about to invite is on ban list.

-- Popssibly set it so the custom rank for each note is the same as the overall filter. - Possibly set it to when you change the guildInfo tag it also sets all the custom Note settings to be in order as well.
-- /dump C_ClubFinder.ReturnClubApplicantList(GRM_G.clubID)
-- C_ClubFinder.ReturnPendingClubApplicantList(GRM_G.clubID)

-- Rules for recruitment, like minimum character level of 15+ or something...

-- Mouseover application note recall in the roster.

-- Add join date to recruitment history



-- ***POTENTIAL FEATURE ADDITIONS***

-- * Potential power tool - set all of a rank to "main"
-- * Add main info to ElvUI player mosueover tooltip in the world.
-- * Collapsable groups in the macro tool with main/alts as collapsable.
-- * Push the note to all alts as well.
-- * Module that basically is a list of pre-set actions to send to guild chat. Or, like /warn Ted, or something like that
-- * Not a ban list, but an "On Watch" list for players who are under warning as well.
-- * Show history of recent players you have sync'd with
-- * Allow players to give themselves a nickname (Add a new player setting, and if it is not "" then display it instead.)
-- * Interface to search public and officer notes
-- * Add option to only show main tags IF the player has alts.
-- * Update the alt side details window to be in the interface on mouseover...
-- * Import option for alt/main info from player notes?
-- * Add push of the MOTD to chat in an interval (possibly a scheduler for MOTDs)
-- * OF NOTE: Guild Promotion/demotion macro MUST include the full name-Server, even on non merged realms.
-- * More flexibility with the audit window - additional column of "Last online" dates, etc... ability to place columns in certain positions
-- * bring back promotions/demotions to the player windows, just build the macro, add the hotkey.
-- * Right click and set as main or clear details of player from the audit window.
-- * RGB color selection for each log item...
-- * Sync join/promo histories (right now it is only set to sync most recent - prevent maybe only up to most recent 10 to prevent too much spam?)
-- * Add player level to chat tag if wanted
-- * Scan the event log for players that joined the guild and left the guild since you last logged...
-- * On players that "Still in guild" on ban list add a button to remove them. * This will require hotkey creation
-- * When configuring a guild for the first time of a player's GRM use... scan the log to auto-find events and dates
-- * If in the middle of a scan, add the report to the scan rather than break the scan, delay, restart, as it is implemented now. Check if already there, if not, insert it.
-- * Auto-re-add to alts grouping when it's a rejoin!
-- * In "Log". Clickable character names that open the roster for the character
-- * Expand addon users window, features, tooltips stating what rank they need to be, etc...
--     - { "Ok!" , "Their Rank too Low" , "Your Rank too Low" , "Outdated Version" , "You Need Updated Version" , "Player Sync Disabled" }
--     - { sender , syncIsEnabled , syncOnlyCurrent , epochTimeVersion , version , senderRankRequirement , banRankRequirement }
-- * Audit log - 
-- * GRM.IsValidName * get ASCII byte values for the other 4 regions' Blizz selected fonts. * possible built-in check I can use?
-- * Titan Panel plugin was requested... not sure what this can do though yet
-- * Expandable and collapsable log groupings when reported on login and lots of items... like collapsable groups with +/- buttons on the side if > 5 in a group?
-- * Track who invited the player and add it to the tooltip on the join date.
-- * Tracking trends - data points of X numbers of members day guild formed... avg number of new members per week, avg number left, etc... stats for the GL..
--      --  max concurrent members online that saves weekly?
-- * Build an API for general use, like GRM slashcommands
-- * Log of everyone you've sync'd with...
-- * Implement colorblind function
--     - It doesn't have to be words.
--     - It can be icons, shapes, textures, anything.
-- * Request to join filters... Clear all that only X days or less left * possible to filter the log?
-- * Ability to edit the date on guild bans - As of now it only edits the details and resets new date
-- * For clickable log... if more than one name is parsed, then have selection of the 2 names, which one you wish to go to... "Left Click to go to playerA, Right click to go to playerB"
-- * Customized Trial period reminder at certain ranks for GL? Reminders in general?
-- * Notification text color selection - Color selection on all
-- * Ranks different colors, maybe a colored icon or symbol before each player in chat, like the main/alt tags
-- * When opening the mailbox, IF the player has people requesting to join the guild, popup window to send them a customizable recruitment message.
-- * Potential player hyperlink generation for wowprogress, guildox, and so on   GetCurrentRegion()  :  1 = US/Brazil/Oceanic  2 = Korea , 3 = EU , 4 = Taiwan , 5 = China
-- * More detailed list of people who have left, and the ability to purge it of most data except maybe banned, etc...
-- * Custom Messaging - stored select messages to send to guild chat. Customizable?   * Possibly standalone addon or plugin
-- * Global MOTD with more info for GRM users. Sync'd -- Just 1 item to compare.


-- ***ADDON PLUGIN IDEAS***

-- * 1) Groups! Create groups! Allow people to join RBG teams, Raid groups, Mythic groups, Arena teams, Custom... request info of those teams.
--     - Click on the team, it pops up all members of the team w/misc. stats of team.
--     - Options to invite all of that team into a group...
--     - Request to join a group: List all guild groups available in a display window... scrollable. Description of each group
--     - Create a guild group:    Required Title - Description
--     - They can remove it themselves, or guild leader can.
--     - If they leave the guild, and they were only member, guild group dissolves. If other members, admin with longest time in guild gets lead... so, need to track meta data on time player has been in the group
--     - If no admin, then it goes to person who has been in group the longest.
--     - Groups will have own custom window with text area for scheduling minor details.
--     - On inviting members, just use auto-complete logic too.
--     - Request to join list for admins... Deny w/reason

-- * 2) Custom Notifications/Reminders -  Basically, I want to build in a feature where the player types /roster remind, or something like that, which pops up a window to set a time and date for any kind of reminder the player wants, just type it out. I've written out a rough UI on how I wish this to look, and I think it is going to be killer useful. You could set reminder to minutes or hours from now, to days or months. Very useful for on-the-spot thoughts. 
--     - It will have a custom UI to quickly set a specific time and date, and note reminder
--     - Slash command will be advances as well. For example, instead of just /roster remind, you could type '/roster remind 30 Recheck AH for deals' Rather than popup the UI window, it will just automatically create a reminder 30 minutes from now that will notify you to "Recheck AH for deals" - Use the UI or use the slash command. UI might be necessary for things much further out, but for simple reminders in that game session... quite useful.
--     - Oh and, I will be adding a Birthday reminder, so guilds can enter player's RL bday, if they so choose.

-- * 3) Guild toolbox
--     - things like Inquiry where you can add a name and say "What happened to this player?" and the addon will attempt to find out through checking their main, their former alt list (add to friend/remove), check online...

-- * 5) SEARCH OPERATORS (notes on the idea)
--     - Operators in the search, like googles. Include "^Name" - include that player and all alts...
--     - Temporary guide on the left when searching for operators. Google Advanced search.
--     - https://cdn.discordapp.com/attachments/588012960018858029/601428359649689611/unknown.png
--     - "Arkaan | Ayr"
--     -  "Arkaan AND Ayr" (For finding specific actions taken on a person by a specific officer, for example.)
--     -  "Arkaan -joined -promoted"

-- * 6) search for TAGS - like "[RAIDER][PVP]" etc... GRM_Roles PLUGIN

-- * 7) Ticket System for a guild bank where people can place requests for items 
        -- Ticket can be marked fulfilled or removed by any officer.

-- * 8) Zone timer log for mapping player histories time in zones with overall guild heatmap or participation

-- * 9) Mailing module. Mass mail guildies pre-defined messages, or based on various filters.

-- * 10) Custom messages - auto grats, milestones, etc...

-- ***KNOWN OR POTENTIAL BUGS***

-- * Custom notes only syncing out if YOU are the one sending them out... and not the sync leader.
-- * What to do with the fact that a single database has restrictions, what if there are ban lists... restricted info, you join a guild and alt never promoted? Check database, if syncing ban info, check officer ban lists see if any, check guildInfo, see if restriction there, know not to share it
-- * Syncing the alt data is the last module that needs to be optimized as it is still slow an inefficient. Custom notes need extra info.
-- * Pratt messing up <M> formatting...and coloring
-- * Promote/demotion/kick/leave should be logged by parsing the chat locally... in Classic... this is a pain but will need to eventually be done for best UX.
-- * Issue with advanced join date tool - counter shows "0" on numbers to add if the designated note is already too full.
-- * JD reporting is properly reporting to JD and to promotion - but to the note it gives CURRENT server time... carryover?
-- * Inactive players > X months not triggering together at the same time but one after the other only. - Probably breaking the loop on find rather than cycling through all, so only 1 report per scan for changes
-- * if the log grows to > 25,000 lines we can potnetially get out of bound errors due to Lua 5.1 limitations (not a problem in 5.2 sadly). Break it into a table of 10000 lines per index
-- * If a player goes from not being able to read officer note, to having access, it spams your log. That should be known...
-- * Custom note cyrillic characters





-- Addon Idea
-- Auto-updating player note.
-- would be if it updated their roster notes for them, to add their item level to their note
-- to show who their main is in their note
-- and what spec they play
-- few people also put in their note some info about professions


-- ADDON IDEA
    -- Connected realms
    -- https://wow.gamepedia.com/API_UnitRealmRelationship 
    -- I'd really like some kind of indicator on player nameplate that you are same/connected realms or just sharded together randomly. I am more willing to help people on my own server
    -- only because I can trade with them, maybe join guilds together and so on.

-- "The Mad Hopper"
    -- Counts number of hops while in combat
    -- Stores hops per raid boss, checks if new count beats old count and announces a new record.
    -- Option to share with raid/party/guild chat
    -- Hop challenges, like only 33% or less of hops can be up and down... the rest have to move around.
    -- /hop start 60 -- all hops in that 60 second window in raid... tally winner.
    -- /hop stop stops counting...
    -- Raid leader or raid assist to access those features...
    -- DDR hop challenge! -- Score collecting!  @Dyfed-Zul'jin's idea.
    -- Passively tally total number of hops since date installed... average number of hops per hour of play time...

-- Backup Officer note recovery tool

    -- Promotion macro 1 rank
    -- /run local n,r,t,m,p,d=GetNumGuildMembers(),GuildControlGetNumRanks()-1,"","PromoteNew";DeleteMacro(m);for i=1,n do p,_,d=GetGuildRosterInfo(i);if d==r then t=t.."/gpromote "..p.."\n";end;end;CreateMacro(m,"inv_trinket_80_titan02e",t);

    -- Mass demote to lowest rank
    -- /run local n,r,t,m,p,d,s=GetNumGuildMembers(),GuildControlGetNumRanks()-1,"","D";DeleteMacro(m);for i=1,n do p,_,d=GetGuildRosterInfo(i);if d~=r and d~=0 then s="/gdemote "..p.."\n"; if #t+#s<256 then t=t..s;end;end;end;CreateMacro(m,"inv_misc_key_04",t)






-- RECRUITMENT

-- Events:

-- CLUB FINDER APPLICANT INVITE RECIEVED
-- CLUB FINDER APPLICATIONS UPDATED


    -- C_Club.GetMemberInfo ( GRM_G.gClubID , C_Club.GetClubMembers ( GRM_G.gClubID )[1] )
    -- C_Club.GetClubMembers ( GRM_G.gClubID )
    -- C_Club.GetInvitationCandidates ("" , 5 , nil , true , GRM_G.gClubID )
    -- "Guild Finder Posting expires in 30 days" refreshes only if you tab off and come backl