local L = LibStub("AceLocale-3.0"):GetLocale("Altoholic")

local WHITE		= "|cFFFFFFFF"
local TEAL		= "|cFF00FF9A"
local GREEN		= "|cFF00FF00"

-- Weekday constants
local CALENDAR_WEEKDAY_NORMALIZED_TEX_LEFT	= 0.0;
local CALENDAR_WEEKDAY_NORMALIZED_TEX_TOP		= 180 / 256;
local CALENDAR_WEEKDAY_NORMALIZED_TEX_WIDTH	= 90 / 256 - 0.001; -- fudge factor to prevent texture seams
local CALENDAR_WEEKDAY_NORMALIZED_TEX_HEIGHT	= 28 / 256 - 0.001; -- fudge factor to prevent texture seams

local CALENDAR_MAX_DAYS_PER_MONTH			= 42;		-- 6 weeks
local CALENDAR_DAYBUTTON_NORMALIZED_TEX_WIDTH	= 90 / 256 - 0.001; -- fudge factor to prevent texture seams
local CALENDAR_DAYBUTTON_NORMALIZED_TEX_HEIGHT	= 90 / 256 - 0.001; -- fudge factor to prevent texture seams
local CALENDAR_DAYBUTTON_HIGHLIGHT_ALPHA		= 0.5;
local DAY_BUTTON = "AltoCalendarDayButton"

local CALENDAR_FIRST_WEEKDAY = 1
-- 1 = Sunday, recreated locally to avoid the problem caused by the calendar addon not being loaded at startup.
-- On an EU client, CALENDAR_FIRST_WEEKDAY = 1 when the game is loaded, but becomes 2 as soon as the calendar is launched.
-- So default it to 1, and add an option to select Monday as 1st day of the week instead. If need be, use a slider.
-- Although the calendar is LoD, avoid it.

local CALENDAR_MONTH_NAMES = { CalendarGetMonthNames() };
local CALENDAR_WEEKDAY_NAMES = { CalendarGetWeekdayNames() };

local CALENDAR_FULLDATE_MONTH_NAMES = {
	-- month names show up differently for full date displays in some languages
	FULLDATE_MONTH_JANUARY,
	FULLDATE_MONTH_FEBRUARY,
	FULLDATE_MONTH_MARCH,
	FULLDATE_MONTH_APRIL,
	FULLDATE_MONTH_MAY,
	FULLDATE_MONTH_JUNE,
	FULLDATE_MONTH_JULY,
	FULLDATE_MONTH_AUGUST,
	FULLDATE_MONTH_SEPTEMBER,
	FULLDATE_MONTH_OCTOBER,
	FULLDATE_MONTH_NOVEMBER,
	FULLDATE_MONTH_DECEMBER,
};

local COOLDOWN_LINE = 1
local INSTANCE_LINE = 2
local CALENDAR_LINE = 3
local CONNECTMMO_LINE = 4
local TIMER_LINE = 5
local SHARED_CD_LINE = 6		-- this type is used for shared cooldowns (alchemy, etc..) among others.

Altoholic.Calendar = {}
Altoholic.Calendar.Days = {}
Altoholic.Calendar.Events = {}

local TimeTable = {}	-- to pass as an argument to time()	see http://lua-users.org/wiki/OsLibraryTutorial for details
local ClockDiff
local lastServerMinute

local function SetClockDiff(elapsed)
	-- this function is called every second until the server time changes (track minutes only)
	local ServerHour, ServerMinute = GetGameTime()
	local continue = true		-- keeps the task running
	
	if not lastServerMinute then		-- ServerMinute not set ? this is the first pass, save it
		lastServerMinute = ServerMinute
	else
		if lastServerMinute ~= ServerMinute then		-- next minute ? do our stuff and stop
			local _, ServerMonth, ServerDay, ServerYear = CalendarGetDate()
			TimeTable.year = ServerYear
			TimeTable.month = ServerMonth
			TimeTable.day = ServerDay
			TimeTable.hour = ServerHour
			TimeTable.min = ServerMinute
			TimeTable.sec = 0					-- minute just changed, so second is 0

			-- our goal is achieved, we can calculate the difference between server time and local time, in seconds.
			-- a positive value means that the server time is ahead of local time.
			-- ex: server: 21:05, local 21.02 could lead to something like 180 (or close to it, depending on seconds)
			ClockDiff = difftime(time(TimeTable), time())
			Altoholic.Calendar.Events:BuildList()		-- rebuild the event list to take the known difference into account
			
			-- now that the difference is known, we can check events for warnings, first check should occur right now (hence 0)
			Altoholic.Tasks:Add("EventWarning", 0, Altoholic.Calendar.CheckEvents, Altoholic.Calendar)
		
			lastServerMinute = nil
			continue = nil
		end
	end
	
	if continue then
		Altoholic.Tasks:Reschedule("SetClockDiff", 1)	-- 1 second later
		return true
	end
end

local function GetClockDiff()
	return ClockDiff or 0
end

function Altoholic.Calendar:Init()
	-- by default, the week starts on Sunday, adjust CALENDAR_FIRST_WEEKDAY if necessary
	if Altoholic.Options:Get("WeekStartsMonday") == 1 then
		CALENDAR_FIRST_WEEKDAY = 2
	end
	
	local _, thisMonth, _, thisYear = CalendarGetDate();
	CalendarSetAbsMonth(thisMonth, thisYear);
	
	-- only register after setting the current month
	Altoholic:RegisterEvent("CALENDAR_UPDATE_EVENT_LIST", Altoholic.Calendar.OnUpdate)
	
	local band = bit.band;
	
	-- initialize weekdays
	for i = 1, 7 do
		local bg = _G["AltoholicFrameCalendarWeekday"..i.."Background"]
		local left = (band(i, 1) * CALENDAR_WEEKDAY_NORMALIZED_TEX_WIDTH) + CALENDAR_WEEKDAY_NORMALIZED_TEX_LEFT;		-- mod(index, 2) * width
		local right = left + CALENDAR_WEEKDAY_NORMALIZED_TEX_WIDTH;
		local top = CALENDAR_WEEKDAY_NORMALIZED_TEX_TOP;
		local bottom = top + CALENDAR_WEEKDAY_NORMALIZED_TEX_HEIGHT;
		bg:SetTexCoord(left, right, top, bottom);
	end
	
	-- initialize day buttons
	for i = 1, CALENDAR_MAX_DAYS_PER_MONTH do
		CreateFrame("Button", DAY_BUTTON..i, AltoholicFrameCalendar, "AltoCalendarDayButtonTemplate");
		self.Days:Init(i)
	end
	
	self.Events:BuildList()
	self.Events:InitialExpiryCheck()
	
	-- determine the difference between local time and server time
	Altoholic.Tasks:Add("SetClockDiff", 0, SetClockDiff, Altoholic.Calendar)
end

local function GetWeekdayIndex(index)
	-- GetWeekdayIndex takes an index in the range [1, n] and maps it to a weekday starting
	-- at CALENDAR_FIRST_WEEKDAY. For example,
	-- CALENDAR_FIRST_WEEKDAY = 1 => [SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY]
	-- CALENDAR_FIRST_WEEKDAY = 2 => [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
	-- CALENDAR_FIRST_WEEKDAY = 6 => [FRIDAY, SATURDAY, SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY]
	
	-- the expanded form for the left input to mod() is:
	-- (index - 1) + (CALENDAR_FIRST_WEEKDAY - 1)
	-- why the - 1 and then + 1 before return? because lua has 1-based indexes! awesome!
	return mod(index - 2 + CALENDAR_FIRST_WEEKDAY, 7) + 1;
end

local function GetFullDate(weekday, month, day, year)
	local weekdayName = CALENDAR_WEEKDAY_NAMES[weekday];
	local monthName = CALENDAR_FULLDATE_MONTH_NAMES[month];
	return weekdayName, monthName, day, year, month;
end

local function GetDay(fullday)
	-- full day = a date as YYYY-MM-DD
	-- this function is actually different than the one in Blizzard_Calendar.lua, since weekday can't necessarily be determined from a UI button
	local refDate = {}		-- let's use the 1st of current month as reference date
	local refMonthFirstDay
	refDate.month, refDate.year, _, refMonthFirstDay = CalendarGetMonth()
	refDate.day = 1

	local t = {}
	local year, month, day = strsplit("-", fullday)
	t.year = tonumber(year)
	t.month = tonumber(month)
	t.day = tonumber(day)

	local numDays = floor(difftime(time(t), time(refDate)) / 86400)
	local weekday = mod(refMonthFirstDay + numDays, 7)
	
	-- at this point, weekday might be negative or 0, simply add 7 to keep it in the proper range
	weekday = (weekday <= 0) and (weekday+7) or weekday
	
	return t.year, t.month, t.day, weekday
end

local function GetEventExpiry(event)
	-- returns the number of seconds in which a calendar event expires
	assert(type(event) == "table")

	local year, month, day = strsplit("-", event.eventDate)
	local hour, minute = strsplit(":", event.eventTime)

	TimeTable.year = tonumber(year)
	TimeTable.month = tonumber(month)
	TimeTable.day = tonumber(day)
	TimeTable.hour = tonumber(hour)
	TimeTable.min = tonumber(minute)

	return difftime(time(TimeTable), time() + GetClockDiff())	-- in seconds
end

local TimerThresholds = { 30, 15, 10, 5, 4, 3, 2, 1 }

local CalendarEventTypes = {
	[COOLDOWN_LINE] = {
		GetReadyNowWarning = function(self, event)
				local name = DataStore:GetCraftCooldownInfo(event.source, event.parentID)
				return format(L["%s is now ready (%s on %s)"], name, event.char, event.realm)
			end,
		GetReadySoonWarning = function(self, event, minutes)
				local name = DataStore:GetCraftCooldownInfo(event.source, event.parentID)
				return format(L["%s will be ready in %d minutes (%s on %s)"], name, minutes, event.char, event.realm)
			end,
		GetInfo = function(self, event)
				local title, expiresIn = DataStore:GetCraftCooldownInfo(event.source, event.parentID)
				return title, format("%s %s", COOLDOWN_REMAINING, Altoholic:GetTimeString(expiresIn))
			end,
	},
	[INSTANCE_LINE] = {
		GetReadyNowWarning = function(self, event)
				local instance = strsplit("|", event.parentID)
				return format(L["%s is now unlocked (%s on %s)"], instance, event.char, event.realm)
			end,
		GetReadySoonWarning = function(self, event, minutes)
				local instance = strsplit("|", event.parentID)
				return format(L["%s will be unlocked in %d minutes (%s on %s)"], instance, minutes, event.char, event.realm)
			end,
		GetInfo = function(self, event)
				-- title gets the instance name, desc gets the raid id
				local instanceName, raidID = strsplit("|", event.parentID)
		
				--	CALENDAR_EVENTNAME_FORMAT_RAID_LOCKOUT = "%s Unlocks"; -- %s = Raid Name
				return instanceName, format("%s%s\nID: %s%s", WHITE,	format(CALENDAR_EVENTNAME_FORMAT_RAID_LOCKOUT, instanceName), GREEN, raidID)
			end,
	},
	[CALENDAR_LINE] = {
		GetReadyNowWarning = function(self, event)
				local c = Altoholic:GetCharacterTable(event.char, event.realm)
				local _, _, title = strsplit("|", c.Calendar[event.parentID])
				return format(CALENDAR_EVENTNAME_FORMAT_START .. " (%s/%s)", title, event.char, event.realm)
			end,
		GetReadySoonWarning = function(self, event, minutes)
				local c = Altoholic:GetCharacterTable(event.char, event.realm)
				local _, _, title = strsplit("|", c.Calendar[event.parentID])
				return format(L["%s starts in %d minutes (%s on %s)"], title, minutes, event.char, event.realm)
			end,
		GetInfo = function(self, event)
				local c = Altoholic:GetCharacterTable(event.char, event.realm)
				local _, _, title, eventType, inviteStatus = strsplit("|", c.Calendar[event.parentID])

				inviteStatus = tonumber(inviteStatus)
				
				local desc
				if type(inviteStatus) == "number" and (inviteStatus > 1) and (inviteStatus < 8) then
					local StatusText = {
						CALENDAR_STATUS_INVITED,		-- CALENDAR_INVITESTATUS_INVITED   = 1
						CALENDAR_STATUS_ACCEPTED,		-- CALENDAR_INVITESTATUS_ACCEPTED  = 2
						CALENDAR_STATUS_DECLINED,		-- CALENDAR_INVITESTATUS_DECLINED  = 3
						CALENDAR_STATUS_CONFIRMED,		-- CALENDAR_INVITESTATUS_CONFIRMED = 4
						CALENDAR_STATUS_OUT,				-- CALENDAR_INVITESTATUS_OUT       = 5
						CALENDAR_STATUS_STANDBY,		-- CALENDAR_INVITESTATUS_STANDBY   = 6
						CALENDAR_STATUS_SIGNEDUP,		-- CALENDAR_INVITESTATUS_SIGNEDUP     = 7
						CALENDAR_STATUS_NOT_SIGNEDUP	-- CALENDAR_INVITESTATUS_NOT_SIGNEDUP = 8
					}
				
					desc = format("%s: %s", STATUS, WHITE..StatusText[inviteStatus]) 
				else 
					desc = format("%s", STATUS) 
				end
		
				return title, desc
			end,
	},
	[CONNECTMMO_LINE] = {
		GetReadyNowWarning = function(self, event)
				local c = Altoholic:GetCharacterTable(event.char, event.realm)
				local _, _, title = strsplit("|", c.ConnectMMO[event.parentID])
				return format(CALENDAR_EVENTNAME_FORMAT_START .. " (%s/%s)", title, event.char, event.realm)
			end,
		GetReadySoonWarning = function(self, event, minutes)
				local c = Altoholic:GetCharacterTable(event.char, event.realm)
				local _, _, title = strsplit("|", c.ConnectMMO[event.parentID])
				return format(L["%s starts in %d minutes (%s on %s)"], title, minutes, event.char, event.realm)
			end,
		GetInfo = function(self, event)
				local c = Altoholic:GetCharacterTable(event.char, event.realm)
				local _, _, title, eventType, eventDesc, attendees = strsplit("|", c.ConnectMMO[e.parentID])
				
				local numPlayers, minLvl, maxLvl, privateToFriends, privateToGuild = strsplit(",", eventDesc)
				local eventTable = {}
			
				table.insert(eventTable, WHITE .. format(L["Number of players: %s"], GREEN .. numPlayers))
				table.insert(eventTable, WHITE .. format(L["Minimum Level: %s"], GREEN .. minLvl))
				table.insert(eventTable, WHITE .. format(L["Maximum Level: %s"], GREEN .. maxLvl))
				table.insert(eventTable, WHITE .. format(L["Private to friends: %s"], GREEN .. (tonumber(privateToFriends) == 1 and YES or NO)))
				table.insert(eventTable, WHITE .. format(L["Private to guild: %s"], GREEN .. (tonumber(privateToGuild) == 1 and YES or NO)))

				local attendeesTable = { strsplit(",", attendees) }
				
				if #attendeesTable > 0 then
					table.insert(eventTable, "")
					table.insert(eventTable, WHITE..L["Attendees: "].."|r")
					for _, name in pairs(attendeesTable) do
						table.insert(eventTable, " " .. name )
					end
					table.insert(eventTable, "")
					table.insert(eventTable, GREEN .. L["Left-click to invite attendees"])
				end
				
				return title, table.concat(eventTable, "\n")
			end,
	},
	[TIMER_LINE] = {
		GetReadyNowWarning = function(self, event)
				local c = Altoholic:GetCharacterTable(event.char, event.realm)
				local item = strsplit("|", c.Timers[event.parentID])
				return format(L["%s is now ready (%s on %s)"], item, event.char, event.realm)
			end,
		GetReadySoonWarning = function(self, event, minutes)
				local c = Altoholic:GetCharacterTable(event.char, event.realm)
				local item = strsplit("|", c.Timers[event.parentID])
				return format(L["%s will be ready in %d minutes (%s on %s)"], item, minutes, event.char, event.realm)
			end,
		GetInfo = function(self, event)
				local c = Altoholic:GetCharacterTable(event.char, event.realm)
				local title = strsplit("|", c.Timers[event.parentID])
				local expiresIn = GetEventExpiry(event)
				return title, format("%s %s", COOLDOWN_REMAINING, Altoholic:GetTimeString(expiresIn))
			end,
	},
	[SHARED_CD_LINE] = {
		GetReadyNowWarning = function(self, event)
				return format(L["%s is now ready (%s on %s)"], event.source, event.char, event.realm)
			end,
		GetReadySoonWarning = function(self, event, minutes)
				return format(L["%s will be ready in %d minutes (%s on %s)"], event.source, minutes, event.char, event.realm)
			end,
		GetInfo = function(self, event)
				local expiresIn = GetEventExpiry(event)
				return event.source, format("%s %s", COOLDOWN_REMAINING, Altoholic:GetTimeString(expiresIn))
			end,
	},
}

local function ShowExpiryWarning(index, minutes)
	local event = Altoholic.Calendar.Events:Get(index)
	local CalendarEvent = CalendarEventTypes[event.eventType]
	
	local warning
	if minutes == 0 then
		warning = CalendarEvent:GetReadyNowWarning(event)
	else
		warning = CalendarEvent:GetReadySoonWarning(event, minutes)
	end
	if not warning then return end
	
	-- print instead of dialog box if player is in combat
	if Altoholic.Options:Get("WarningDialogBox") == 1 and not UnitAffectingCombat("player") then
		AltoMsgBox.ButtonHandler = Altoholic.Calendar.WarningButtonHandler
		AltoMsgBox_Text:SetText(format("%s\n%s", WHITE..warning, L["Do you want to open Altoholic's calendar for details ?"]))
		AltoMsgBox:Show()
	else
		Altoholic:Print(warning)
	end
end

local function ClearExpiredEvents()
	local DS = DataStore
	
	for realm in pairs(DS:GetRealms()) do
		for characterName, character in pairs(DS:GetCharacters(realm)) do

			-- Profession Cooldowns
			local professions = DS:GetProfessions(character)
			if professions then
				for professionName, profession in pairs(professions) do
					DS:ClearExpiredCooldowns(profession)
				end
			end
			
			local c = Altoholic:GetCharacterTable(characterName, realm)
			
			-- Saved Instances
			for k, v in pairs(c.SavedInstance) do
				local reset, lastCheck = strsplit("|", v)
				reset = tonumber(reset)
				lastCheck = tonumber(lastCheck)
				
				if reset - (time() - lastCheck) <= 0 then	-- has expired
					c.SavedInstance[k] = nil
				end
			end
			
			-- Other timers (like mysterious egg, etc..)
			for k, v in pairs(c.Timers) do
				local _, lastCheck, duration = strsplit("|", v)
				lastCheck = tonumber(lastCheck)
				duration = tonumber(duration)
				local expires = duration + lastCheck + GetClockDiff()
				if (expires - time()) <= 0 then
					c.Timers[k] = nil
				end
			end
		end
	end
end

local function IsNumberInString(number, str)
	-- ex: with str = "15|10|3" returns true if value is in this string
	for v in str:gmatch("(%d+)") do
		if tonumber(v) == number then
			return true
		end
	end
end

local WARNING_TYPE_PROFESSION_CD = 1
local WARNING_TYPE_DUNGEON_RESET = 2
local WARNING_TYPE_CALENDAR_EVENT = 3
local WARNING_TYPE_ITEM_TIMER = 4

local EventToWarningType = {
	[COOLDOWN_LINE] = WARNING_TYPE_PROFESSION_CD,
	[INSTANCE_LINE] = WARNING_TYPE_DUNGEON_RESET,
	[CALENDAR_LINE] = WARNING_TYPE_CALENDAR_EVENT,
	[CONNECTMMO_LINE] = WARNING_TYPE_CALENDAR_EVENT,
	[TIMER_LINE] = WARNING_TYPE_ITEM_TIMER,
	[SHARED_CD_LINE] = WARNING_TYPE_PROFESSION_CD,
}

function Altoholic.Calendar:CheckEvents(elapsed)
	if Altoholic.Options:Get("DisableWarnings") == 1 then	-- warnings disabled ? do nothing
		Altoholic.Tasks:Reschedule("EventWarning", 60)
		return true
	end

	-- called every 60 seconds
	local hasEventExpired
	
	for k, v in pairs(Altoholic.Calendar.Events.List) do
		local numMin = floor(GetEventExpiry(v) / 60)

		if numMin < 0 then
			hasEventExpired = true		-- at least one event has expired
		elseif numMin == 0 then
			ShowExpiryWarning(k, 0)
			hasEventExpired = true		-- at least one event has expired
		elseif numMin <= 30 then
			local warnings = Altoholic.Options:Get("WarningType"..EventToWarningType[v.eventType])		-- Gets something like "15|5|1"
			for _, threshold in pairs(TimerThresholds) do
				if threshold == numMin then			-- if snooze is allowed for this value
					if IsNumberInString(threshold, warnings) then
						ShowExpiryWarning(k, numMin)
					end
					break
				elseif threshold < numMin then		-- save some cpu cycles, exit if threshold too low
					break
				end
			end
		end
	end
	
	if hasEventExpired then		-- if at least one event has expired, rebuild the list & refresh
		ClearExpiredEvents()
		Altoholic.Calendar.Events:BuildList()
		Altoholic.Tabs.Summary:Refresh()
	end
	
	-- the task was executed right after the minute changed server side, so reschedule exactly 60 seconds later
	Altoholic.Tasks:Reschedule("EventWarning", 60)
	return true
end

function Altoholic.Calendar:WarningButtonHandler(button)
	AltoMsgBox.ButtonHandler = nil		-- prevent any other call to msgbox from coming back here
	if not button then return end

	Altoholic:ToggleUI()
	Altoholic.Tabs.Summary:MenuItem_OnClick(8)
end

function Altoholic.Calendar:SetFirstDayOfWeek(day)
	CALENDAR_FIRST_WEEKDAY = day
end

function Altoholic.Calendar:Update()
	-- taken from CalendarFrame_Update() in Blizzard_Calendar.lua, adjusted for my needs.

	local self = Altoholic.Calendar
	local presentWeekday, presentMonth, presentDay, presentYear = CalendarGetDate();
	local prevMonth, prevYear, prevNumDays = CalendarGetMonth(-1);
	local nextMonth, nextYear, nextNumDays = CalendarGetMonth(1);
	local month, year, numDays, firstWeekday = CalendarGetMonth();

	-- set title
	AltoholicFrameCalendar_MonthYear:SetText(CALENDAR_MONTH_NAMES[month] .. " ".. year)
	
	-- initialize weekdays
	for i = 1, 7 do
		_G["AltoholicFrameCalendarWeekday"..i.."Name"]:SetText(string.sub(CALENDAR_WEEKDAY_NAMES[GetWeekdayIndex(i)], 1, 3));
	end

	local buttonIndex = 1;
	local isDarkened = true
	local day;

	-- set the previous month's days before the first day of the week
	local viewablePrevMonthDays = mod((firstWeekday - CALENDAR_FIRST_WEEKDAY - 1) + 7, 7);
	day = prevNumDays - viewablePrevMonthDays;

	while ( GetWeekdayIndex(buttonIndex) ~= firstWeekday ) do
		self.Days:Update(buttonIndex, day, prevMonth, prevYear, isDarkened)
		day = day + 1;
		buttonIndex = buttonIndex + 1;
	end

	-- set the days of this month
	day = 1;
	isDarkened = false
	while ( day <= numDays ) do
		self.Days:Update(buttonIndex, day, month, year, isDarkened)
		day = day + 1;
		buttonIndex = buttonIndex + 1;
	end
	
	-- set the first days of the next month
	day = 1;
	isDarkened = true
	while ( buttonIndex <= CALENDAR_MAX_DAYS_PER_MONTH ) do
		self.Days:Update(buttonIndex, day, nextMonth, nextYear, isDarkened)

		day = day + 1;
		buttonIndex = buttonIndex + 1;
	end
	
	self.Events:Update()
end

function Altoholic.Calendar.Days:Init(index)
	local button = _G[DAY_BUTTON..index]
	button:SetID(index)
	
	-- set anchors
	button:ClearAllPoints();
	if ( index == 1 ) then
		button:SetPoint("TOPLEFT", AltoholicFrameCalendar, "TOPLEFT", 285, -1);
	elseif ( mod(index, 7) == 1 ) then
		button:SetPoint("TOPLEFT", _G[DAY_BUTTON..(index - 7)], "BOTTOMLEFT", 0, 0);
	else
		button:SetPoint("TOPLEFT", _G[DAY_BUTTON..(index - 1)], "TOPRIGHT", 0, 0);
	end

	-- set the normal texture to be the background
	local tex = button:GetNormalTexture();
	tex:SetDrawLayer("BACKGROUND");
	local texLeft = random(0,1) * CALENDAR_DAYBUTTON_NORMALIZED_TEX_WIDTH;
	local texRight = texLeft + CALENDAR_DAYBUTTON_NORMALIZED_TEX_WIDTH;
	local texTop = random(0,1) * CALENDAR_DAYBUTTON_NORMALIZED_TEX_HEIGHT;
	local texBottom = texTop + CALENDAR_DAYBUTTON_NORMALIZED_TEX_HEIGHT;
	tex:SetTexCoord(texLeft, texRight, texTop, texBottom);
	
	-- adjust the highlight texture layer
	tex = button:GetHighlightTexture();
	tex:SetAlpha(CALENDAR_DAYBUTTON_HIGHLIGHT_ALPHA);
end

function Altoholic.Calendar.Days:Update(index, day, month, year, isDarkened)
	local button = _G[DAY_BUTTON..index]
	local buttonName = button:GetName();
	
	button.day = day
	button.month = month
	button.year = year
	
	-- set date
	local dateLabel = _G[buttonName.."Date"];
	local tex = button:GetNormalTexture();

	dateLabel:SetText(day);
	if isDarkened then
		tex:SetVertexColor(0.4, 0.4, 0.4)
	else
		tex:SetVertexColor(1.0, 1.0, 1.0)
	end
	
	-- set count
	local countLabel = _G[buttonName.."Count"];
	local count = Altoholic.Calendar.Events:GetNum(year, month, day)
	
	if count == 0 then
		countLabel:Hide()
	else
		countLabel:SetText(count)
		countLabel:Show()
	end
end

function Altoholic.Calendar.Days:OnClick(self, button)
	local Events = Altoholic.Calendar.Events
	local count = Events:GetNum(self.year, self.month, self.day)
	if count == 0 then	-- no events on that day ? exit
		return
	end	
	
	local index = Events:GetIndex(self.year, self.month, self.day)
	if index then
		Events:SetOffset(index - 1)	-- if the date is the 4th line, offset is 3
		Events:Update()
	end
end

function Altoholic.Calendar.Days:OnEnter(self)

	local Events = Altoholic.Calendar.Events
	local count = Events:GetNum(self.year, self.month, self.day)
	if count == 0 then	-- no events on that day ? exit
		return
	end
	
	AltoTooltip:SetOwner(self, "ANCHOR_LEFT");
	AltoTooltip:ClearLines();
	local eventDate = format("%04d-%02d-%02d", self.year, self.month, self.day)
	local weekday = GetWeekdayIndex(mod(self:GetID(), 7)) 
	weekday = (weekday == 0) and 7 or weekday
	
	AltoTooltip:AddLine(TEAL..format(FULLDATE, GetFullDate(weekday, self.month, self.day, self.year)));

	for k, v in pairs(Events.List) do
		if v.eventDate == eventDate then
			local char, eventTime, title = Events:GetInfo(k)
			AltoTooltip:AddDoubleLine(format("%s %s", WHITE..eventTime, char), title);
		end
	end
	AltoTooltip:Show();
end

local EVENT_DATE = 1
local EVENT_INFO = 2

function Altoholic.Calendar.Events:InitialExpiryCheck()
	for index, event in pairs(self.List) do			-- browse all events
		local expiresIn = GetEventExpiry(event)
		if expiresIn < 0 then					-- if the event has expired
			event.markForDeletion = true		-- .. mark it for deletion (no table.remove in this pass, to avoid messing up indexes)
			ShowExpiryWarning(index, 0)		-- .. and do the appropriate warning
		end
	end
	
	for i = #self.List, 1, -1 do			-- browse the event table backwards
		if self.List[i].markForDeletion then	-- erase marked events
			table.remove(self.List, i)
		end
	end
	
	ClearExpiredEvents()	-- quick fix, it seems that for some reason, egg timers did not get cleared as they should without this call
	self.InitialExpiryCheck = nil		-- kill self, function won't be called again
end

function Altoholic.Calendar.Events:BuildView()
	self.view = self.view or {}
	wipe(self.view)
	
	-- the following list of events : 10/05, 10/05, 12/05, 14/05, 14/05
	-- turns into this view : 
	-- 	"10/05"
	--	event 1
	--	event 2
	--	"12/05"
	--	event 1
	-- 	"14/05"
	--	event 1
	--	event 2
	
	
	local eventDate = ""
	for k, v in pairs(self.List) do
		if eventDate ~= v.eventDate then
			table.insert(self.view, { linetype = EVENT_DATE, eventDate = v.eventDate })
			eventDate = v.eventDate
		end
		table.insert(self.view, { linetype = EVENT_INFO, parentID = k })
	end
end

function Altoholic.Calendar.Events:BuildList()
	self.List = self.List or {}
	wipe(self.List)

	local ClockDiff = GetClockDiff()
	local DS = DataStore
	
	for realm in pairs(DS:GetRealms()) do
		for characterName, character in pairs(DS:GetCharacters(realm)) do
			-- add all timers, even expired ones. Expiries will be handled elsewhere.

			-- Profession Cooldowns
			local professions = DS:GetProfessions(character)
			if professions then
				for professionName, profession in pairs(professions) do
					local supportsSharedCD
					if professionName == GetSpellInfo(2259) or			-- alchemy
						professionName == GetSpellInfo(3908) or 			-- tailoring
						professionName == GetSpellInfo(2575) then			-- mining
						supportsSharedCD = true		-- current profession supports shared cooldowns
					end
					
					if supportsSharedCD then
						local sharedCDFound		-- is there a shared cd for this profession ?
						for i = 1, DS:GetNumActiveCooldowns(profession) do
							local name, _, reset, lastCheck = DS:GetCraftCooldownInfo(profession, i)
							local expires = reset + lastCheck + ClockDiff

							if not sharedCDFound then
								sharedCDFound = true
								self:Add(SHARED_CD_LINE, date("%Y-%m-%d",expires), date("%H:%M",expires), characterName, realm, nil, professionName)
							end
						end
					else
						for i = 1, DS:GetNumActiveCooldowns(profession) do
							local name, _, reset, lastCheck = DS:GetCraftCooldownInfo(profession, i)
							local expires = reset + lastCheck + ClockDiff

							self:Add(COOLDOWN_LINE, date("%Y-%m-%d",expires), date("%H:%M",expires), characterName, realm, i, profession)
						end
					end
				end
			end
			
			local c = Altoholic:GetCharacterTable(characterName, realm)
			
			-- Saved Instances
			for k, v in pairs(c.SavedInstance) do
				local reset, lastCheck = strsplit("|", v)
				reset = tonumber(reset)
				lastCheck = tonumber(lastCheck)
				
				local expires = reset + lastCheck + ClockDiff
				self:Add(INSTANCE_LINE, date("%Y-%m-%d",expires), date("%H:%M",expires), characterName, realm, k)
			end
			
			-- Calendar Events
			for k, v in pairs(c.Calendar) do
				local eventDate, eventTime = strsplit("|", v)
				-- TODO: do not add declined invitations
				self:Add(CALENDAR_LINE, eventDate, eventTime, characterName, realm, k)
			end
			
			-- ConnectMMO events
			for k, v in pairs(c.ConnectMMO) do
				local eventDate, eventTime = strsplit("|", v)
				self:Add(CONNECTMMO_LINE, eventDate, eventTime, characterName, realm, k)
			end
			
			-- Other timers (like mysterious egg, etc..)
			for k, v in pairs(c.Timers) do
				local item, lastCheck, duration = strsplit("|", v)
				lastCheck = tonumber(lastCheck)
				duration = tonumber(duration)

				local expires = duration + lastCheck + ClockDiff
				self:Add(TIMER_LINE, date("%Y-%m-%d",expires), date("%H:%M",expires), characterName, realm, k)
			end
		end
	end
	
	-- sort by time
	self:Sort()
	self:BuildView()
end

local NUM_EVENTLINES = 14

function Altoholic.Calendar.Events:Update()
	local self = Altoholic.Calendar.Events
	
	local VisibleLines = NUM_EVENTLINES
	local frame = "AltoholicFrameCalendar"
	local entry = frame.."Entry"

	local offset = FauxScrollFrame_GetOffset( _G[ frame.."ScrollFrame" ] );

	for i=1, VisibleLines do
		local line = i + offset
		if line <= #self.view then
			local s = self.view[line]

			if s.linetype == EVENT_DATE then
				local year, month, day, weekday = GetDay(s.eventDate)
				_G[ entry..i.."Date" ]:SetText(format(FULLDATE, GetFullDate(weekday, month, day, year)))
				_G[ entry..i.."Date" ]:Show()
				
				_G[ entry..i.."Hour" ]:Hide()
				_G[ entry..i.."Character" ]:Hide()
				_G[ entry..i.."Title" ]:Hide()
				_G[ entry..i.."_Background"]:Show()
				
			elseif s.linetype == EVENT_INFO then
				local char, eventTime, title = self:GetInfo(s.parentID)

				_G[ entry..i.."Hour" ]:SetText(eventTime)
				_G[ entry..i.."Character" ]:SetText(char)
				_G[ entry..i.."Title" ]:SetText(title)
				
				_G[ entry..i.."Hour" ]:Show()
				_G[ entry..i.."Character" ]:Show()
				_G[ entry..i.."Title" ]:Show()

				_G[ entry..i.."Date" ]:Hide()
				_G[ entry..i.."_Background"]:Hide()
			end

			_G[ entry..i ]:SetID(line)
			_G[ entry..i ]:Show()
		else
			_G[ entry..i ]:Hide()
		end
	end
	
	local last = (#self.view < VisibleLines) and VisibleLines or #self.view
	FauxScrollFrame_Update( _G[ frame.."ScrollFrame" ], last, VisibleLines, 18);
end

function Altoholic.Calendar.Events:Get(index)
	return self.List[index]
end

function Altoholic.Calendar.Events:GetInfo(index)
	local event = self.List[index]		-- dereference event
	if not event then return end
	
	local DS = DataStore
	local character = DS:GetCharacter(event.char, event.realm)
	local char = DS:GetColoredCharacterName(character)
	
	if event.realm ~= GetRealmName() then	-- different realm ?
		char = format("%s %s(%s)", char, GREEN, event.realm)
	end
	
	local CalendarEvent = CalendarEventTypes[event.eventType]
	local title, desc = CalendarEvent:GetInfo(event)
				
	return char, event.eventTime, title, desc
end

function Altoholic.Calendar.Events:Sort()
	table.sort(self.List, function(a, b)
		if (a.eventDate ~= b.eventDate) then			-- sort by date first ..
			return a.eventDate < b.eventDate
		elseif (a.eventTime ~= b.eventTime) then		-- .. then by hour
			return a.eventTime < b.eventTime
		elseif (a.char ~= b.char) then					-- .. then by alt
			return a.char < b.char
		end
	end)
end

function Altoholic.Calendar.Events:Add(eventType, eventDate, eventTime, char, realm, index, externalTable)
	table.insert(self.List, {
		eventType = eventType, 
		eventDate = eventDate, 
		eventTime = eventTime, 
		char = char,
		realm = realm,
		parentID = index,
		source = externalTable})
end

function Altoholic.Calendar.Events:GetNum(year, month, day)
	local eventDate = format("%04d-%02d-%02d", year, month, day)
	local count = 0
	for k, v in pairs(self.List) do
		if v.eventDate == eventDate then
			count = count + 1
		end
	end
	return count
end

function Altoholic.Calendar.Events:OnEnter(frame)
	local self = Altoholic.Calendar.Events
	local s = self.view[frame:GetID()]
	if not s or s.linetype == EVENT_DATE then return end
	
	
	AltoTooltip:SetOwner(frame, "ANCHOR_RIGHT");
	AltoTooltip:ClearLines();
	-- local eventDate = format("%04d-%02d-%02d", self.year, self.month, self.day)
	-- local weekday = GetWeekdayIndex(mod(self:GetID(), 7))
	-- AltoTooltip:AddLine(TEAL..format(FULLDATE, GetFullDate(weekday, self.month, self.day, self.year)));
	
	local char, eventTime, title, desc = self:GetInfo(s.parentID)
	AltoTooltip:AddDoubleLine(format("%s %s", WHITE..eventTime, char), title);
	if desc then
		AltoTooltip:AddLine(" ")
		AltoTooltip:AddLine(desc)
	end
	AltoTooltip:Show();
end

function Altoholic.Calendar.Events:OnClick(frame, button)
	-- if an event is left-clicked, try to invite attendees. ConnectMMO events only
	
	local self = Altoholic.Calendar.Events
	local s = self.view[frame:GetID()]
	if not s or s.linetype == EVENT_DATE then return end		-- date line ? exit
	
	local e = self.List[s.parentID]		-- dereference event
	-- not a connectmmo event ? or wrong realm ? exit
	if not e or e.eventType ~= CONNECTMMO_LINE or e.realm ~= GetRealmName() then return end	
	
	local c = Altoholic:GetCharacterTable(e.char, e.realm)
	if not c then return end	-- invalid char table ? exit
	
	local _, _, _, _, _, attendees = strsplit("|", c.ConnectMMO[e.parentID])

	-- TODO, add support for raid groups
	for _, name in pairs({ strsplit(",", attendees) }) do
		if name ~= UnitName("player") then
			InviteUnit(name) 
		end
	end
end

function Altoholic.Calendar.Events:GetIndex(year, month, day)
	local eventDate = format("%04d-%02d-%02d", year, month, day)
	for k, v in pairs(self.view) do
		if v.linetype == EVENT_DATE and v.eventDate == eventDate then
			-- if the date line is found, return its index
			return k
		end
	end
end

function Altoholic.Calendar.Events:SetOffset(offset)
	-- if the view has less entries than can be displayed, don't change the offset
	if #self.view <= NUM_EVENTLINES then return end

	if offset <= 0 then
		offset = 0
	elseif offset > (#self.view - NUM_EVENTLINES) then
		offset = (#self.view - NUM_EVENTLINES)
	end
	FauxScrollFrame_SetOffset( AltoholicFrameCalendarScrollFrame, offset )
	AltoholicFrameCalendarScrollFrameScrollBar:SetValue(offset * 18)
end

function Altoholic.Calendar:Scan()
	if not CalendarFrame then
		-- The Calendar addon is LoD, and most functions return nil if the calendar is not loaded, so unless the CalendarFrame is valid, exit right away
		return
	end
	Altoholic:UnregisterEvent("CALENDAR_UPDATE_EVENT_LIST")	-- prevent CalendarSetAbsMonth from triggering a scan (= avoid infinite loop)
	
	local currentMonth, currentYear = CalendarGetMonth()		-- save the current month
	local _, thisMonth, thisDay, thisYear = CalendarGetDate();
	CalendarSetAbsMonth(thisMonth, thisYear);
	
	local c = Altoholic.ThisCharacter
	wipe(c.Calendar)
	
	-- save this month (from today) + 6 following months
	for monthOffset = 0, 6 do
		local month, year, numDays = CalendarGetMonth(monthOffset)
		local startDay = (monthOffset == 0) and thisDay or 1
		
		for day = startDay, numDays do
			for i = 1, CalendarGetNumDayEvents(monthOffset, day) do		-- number of events that day ..
				-- http://www.wowwiki.com/API_CalendarGetDayEvent
				local title, hour, minute, calendarType, _, eventType, _, _, inviteStatus = CalendarGetDayEvent(monthOffset, day, i)
				if calendarType ~= "HOLIDAY" and calendarType ~= "RAID_LOCKOUT" and 
					calendarType ~= "RAID_RESET" and inviteStatus ~= CALENDAR_INVITESTATUS_DECLINED then
					-- don't save holiday events, they're the same for all chars, and would be redundant..who wants to see 10 fishing contests every sundays ? =)

					table.insert(c.Calendar, format("%s|%s|%s|%d|%d",
						format("%04d-%02d-%02d", year, month, day), format("%02d:%02d", hour, minute),
						title, eventType, inviteStatus ))
				end
			end
		end
	end
	
	CalendarSetAbsMonth(currentMonth, currentYear);		-- restore current month
	Altoholic:RegisterEvent("CALENDAR_UPDATE_EVENT_LIST", Altoholic.Calendar.OnUpdate)
end

function Altoholic.Calendar:OnUpdate()
	local self = Altoholic.Calendar
	self:Scan()
	self.Events:BuildList()
	Altoholic.Tabs.Summary:Refresh()
end

local function ToggleWarningThreshold(self)
	local id = self.arg1
	local warnings = Altoholic.Options:Get("WarningType"..id)		-- Gets something like "15|5|1"
	
	local t = {}		-- create a temporary table to store checked values
	for v in warnings:gmatch("(%d+)") do
		v = tonumber(v)
		if v ~= self.value then		-- add all values except the one that was clicked
			table.insert(t, v)
		end
	end
	
	if not IsNumberInString(self.value, warnings) then		-- if number is not yet in the string, save it (we're checking it, otherwise we're unchecking)
		table.insert(t, self.value)
	end
	
	Altoholic.Options:Set("WarningType"..id, table.concat(t, "|"))		-- Sets something like "15|5|10|1"
end

function Altoholic.Calendar:WarningType_Initialize()
	local info = UIDropDownMenu_CreateInfo();
	local id = self:GetID()
	local warnings = Altoholic.Options:Get("WarningType"..id)		-- Gets something like "15|5|1"
	
	for _, threshold in pairs(TimerThresholds) do
		info.text = format(D_MINUTES, threshold)
		info.value = threshold
		info.func = ToggleWarningThreshold
		info.checked = IsNumberInString(threshold, warnings)
		info.arg1 = id		-- save the id of the current option
		UIDropDownMenu_AddButton(info, 1); 
	end
end
