------------------------------------------------
--                 CT_BarMod                  --
--                                            --
-- Intuitive yet powerful action bar addon,   --
-- featuring per-button positioning as well   --
-- as scaling while retaining the concept of  --
-- grouped buttons and action bars.           --
-- Please do not modify or otherwise          --
-- redistribute this without the consent of   --
-- the CTMod Team. Thank you.                 --
------------------------------------------------

--------------------------------------------
-- Initialization

local _G = getfenv(0);
local module = _G.CT_BarMod;

-- Options
local displayBindings = true;
local displayCount = true;
local colorLack = 1;
local buttonLock = false;
local hideGrid = false;

--------------------------------------------
-- Local Copies

local rangeIndicator = RANGE_INDICATOR;
local GetTime = GetTime;
local ceil = ceil;
local next = next;

local HasAction = HasAction;
local ActionHasRange = ActionHasRange;

local GetActionRange = GetActionRange;
local GetActionTexture = GetActionTexture;
local GetActionCooldown = GetActionCooldown;

local IsUsableAction = IsUsableAction;
local IsAttackAction = IsAttackAction;
local IsCurrentAction = IsCurrentAction;
local IsConsumableAction = IsConsumableAction;
local IsAutoRepeatAction = IsAutoRepeatAction;

--------------------------------------------
-- Cooldown Handler

local cooldownList, cooldownUpdater;
local function updateCooldown(count, time)
	if ( time > 3540 ) then
		-- Hours
		count:SetText(ceil(time/3600).."h");
	elseif ( time > 60 ) then
		-- Minutes
		count:SetText(ceil(time/60).."m");
	elseif ( time > 1 ) then
		-- Seconds
		count:SetText(ceil(time));
	else
		count:SetText("");
	end
end

local function dropCooldownFromQueue(object)
	if ( cooldownList ) then
		cooldownList[object] = nil;
		if ( not next(cooldownList) ) then
			module:unschedule(cooldownUpdaterRef, true);
		end
	end
end

cooldownUpdater = function()
	if ( cooldownList ) then
		local currTime, start, duration, enable = GetTime();
		for key, value in pairs(cooldownList) do
			start, duration, enable = GetActionCooldown(key.id);
			if ( start > 0 and duration > 0 and enable > 0 ) then
				updateCooldown(value, duration - (currTime-start));
			else
				dropCooldownFromQueue(key);
			end
		end
	end
end

local function stopCooldown(cooldown)
	local count = cooldown.count;
	if ( count ) then
		count:Hide();
		dropCooldownFromQueue(cooldown.object);
	end
end

local function startCooldown(cooldown, start, duration)
	if ( duration < 2 ) then
		stopCooldown(cooldown);
		return;
	end
	
	local count = cooldown.count;
	if ( not count ) then
		count = cooldown:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge");
		count:SetPoint("CENTER", cooldown);
		cooldown.count = count;
	end
	
	if ( not cooldownList ) then
		cooldownList = { [cooldown.object] = count };
		module:schedule(0.5, true, cooldownUpdater);
	else
		if ( not next(cooldownList) ) then
			module:schedule(0.5, true, cooldownUpdater);
		end
		cooldownList[cooldown.object] = count;
	end
	
	count:Show();
	updateCooldown(count, duration - (GetTime()-start));
end

--------------------------------------------
-- Use Button Class

local useButton = { };
local actionButton = module.actionButtonClass;
local actionButtonList = module.actionButtonList;

setmetatable(useButton, { __index = actionButton });
module.useButtonClass = useButton;

-- Constructor
function useButton:constructor(...)
	actionButton.constructor(self, ...);
	
	-- Do stuff
	local button = self.button;
	button:SetAttribute("action", self.id);
	button:SetAttribute("checkselfcast", true);
	button.border:SetVertexColor(0, 1, 0, 0.35);
end

-- Destructor
function useButton:destructor(...)
	-- Do stuff
	self.hasAction = nil;
	self.hasRange = nil;
	self.checked = nil;
	
	if ( self.flashing ) then
		self:stopFlash();
	end
	
	actionButton.destructor(self, ...);
end

-- Update everything
function useButton:update()
	local id = self.id;
	local hasAction = HasAction(id);
	local button = self.button;
	
	self.hasAction = hasAction;
	self.hasRange = ActionHasRange(id);
	
	self:updateCount();
	self:updateBinding();
	self:updateTexture();
	if ( hasAction ) then
		self:updateState();
		self:updateUsable();
		self:updateFlash();
		self:updateCooldown();
		self:updateLock();
		button:Show();
	else
		button.cooldown:Hide();
		if ( hideGrid and not self.gridShown ) then
			button:Hide();
		else
			button:Show();
		end
	end
	
	-- Textures
	if ( hasAction ) then
		local icon = button.icon;
		icon:SetTexture(GetActionTexture(id));
		icon:Show();
		button.normalTexture:SetTexture("Interface\\Buttons\\UI-Quickslot2");
	else
		button.icon:Hide();
		button.normalTexture:SetTexture("Interface\\Buttons\\UI-Quickslot");
	end
	
	-- Equip
	if ( IsEquippedAction(id) ) then
		button.border:Show();
	else
		button.border:Hide();
	end
	
	-- Macro Name
	button.name:SetText(GetActionText(id));
end

-- Check button lock state to disable shift-click
function useButton:updateLock()
	if ( buttonLock ) then
		self.button:SetAttribute("shift-type*", ATTRIBUTE_NOOP);
	else
		self.button:SetAttribute("shift-type*", nil);
	end
end

-- Update Usable
function useButton:updateUsable()
	local isUsable, notEnoughMana = IsUsableAction(self.id);
	local button = self.button;
	
	if ( colorLack and self.outOfRange ) then
		if ( colorLack == 2 ) then
			button.icon:SetVertexColor(0.5, 0.5, 0.5);
		else
			button.icon:SetVertexColor(0.8, 0.4, 0.4);
		end
		
	elseif ( isUsable ) then
		button.icon:SetVertexColor(1, 1, 1);
		
	elseif ( notEnoughMana ) then
		button.icon:SetVertexColor(0.5, 0.5, 1);
		
	else
		button.icon:SetVertexColor(0.4, 0.4, 0.4);
	end
end

-- Update Cooldown
function useButton:updateCooldown()
	local start, duration, enable = GetActionCooldown(self.id);
	if ( start > 0 and duration > 0 and enable > 0 ) then
		local cooldown = self.button.cooldown;
		cooldown:SetCooldown(start, duration);
		cooldown:Show();
		
		if ( displayCount ) then
			startCooldown(cooldown, start, duration);
		else
			stopCooldown(cooldown);
		end
	else
		local cooldown = self.button.cooldown;
		stopCooldown(cooldown);
		cooldown:Hide();
	end
end

-- Update State
function useButton:updateState()
	local id = self.id;
	
	if ( IsCurrentAction(id) or IsAutoRepeatAction(id) ) then
		self.checked = true;
		self.button:SetChecked(true);
	else
		self.checked = nil;
		self.button:SetChecked(false);
	end
end

-- Update Binding
function useButton:updateBinding()
	local id = self.id;
	if ( displayBindings ) then
		local text = self:getBinding();
		if ( text == "" ) then
			if ( not self.hasAction or not ActionHasRange(id) ) then
				self.button.hotkey:SetText("");
				self.hasRange = nil;
				return;
			else
				self.button.hotkey:SetText(rangeIndicator);
				self:updateUsable();
			end
		else
			self.button.hotkey:SetText(text);
		end
		self.hasRange = true;
	else
		self.hasRange = self.hasAction and ActionHasRange(id);
		self.button.hotkey:SetText("");
	end
end

-- Update Range
function useButton:updateRange()
	if ( IsActionInRange(self.id) == 0 ) then
		local button = self.button;
		self.outOfRange = true;
		button.hotkey:SetVertexColor(1.0, 0.1, 0.1);
	else
		self.outOfRange = nil;
		self.button.hotkey:SetVertexColor(0.6, 0.6, 0.6);
	end
	if ( colorLack ) then
		self:updateUsable();
	end
end

-- Update Count
function useButton:updateCount()
	local id = self.id;
	if ( IsConsumableAction(id) ) then
		self.button.count:SetText(GetActionCount(id));
	else
		self.button.count:SetText("");
	end
end

-- Update Flash
function useButton:updateFlash()
	local id = self.id;
	if ( ( IsAttackAction(id) and IsCurrentAction(id) ) or IsAutoRepeatAction(id) ) then
		self:startFlash();
	elseif ( self.flashing ) then
		self:stopFlash();
	end
	self:updateState();
end

-- Show Grid
function useButton:showGrid()
	self.gridShown = true;
	
	local button = self.button;
	button:Show();
	button.normalTexture:SetVertexColor(1, 1, 1, 0.5);
end

-- Hide Grid
function useButton:hideGrid()
	self.gridShown = nil;
	self:updateUsable();
	if ( hideGrid and not self.hasAction ) then
		self.button:Hide();
	end
end

-- Get Binding
function useButton:getBinding()
	local text = module:getOption("BINDING-"..self.id);
	if ( not text ) then
		return;
	end
	
	text = text:gsub("(.-)SHIFT%-(.+)", "%1S-%2");
	text = text:gsub("(.-)CTRL%-(.+)", "%1C-%2");
	text = text:gsub("(.-)ALT%-(.+)", "%1A-%2");
	return text;
end

------------------------
-- Button Handlers

-- PostClick
function useButton:postclick()
	if ( not self.checked ) then
		self.button:SetChecked(false);
	end
end

-- OnDragStart
function useButton:ondragstart()
	if ( not module:getOption("buttonLock") or IsShiftKeyDown() ) then
		PickupAction(self.id);
	end
end

-- OnReceiveDrag
function useButton:onreceivedrag()
	PlaceAction(self.id);
end

------------------------
-- Flash Handling

local flashingButtons;

-- Toggles flashing on a button
function toggleFlash(object, enable)
	local flash = object.button.flash;
	
	if ( enable ~= nil ) then
		if ( enable ) then
			flash:Show();
		else
			flash:Hide();
		end
	else
		if ( not flash:IsShown() ) then
			flash:Show();
		else
			flash:Hide();
		end
	end
end

-- Periodic flash updater
local function flashUpdater()
	if ( flashingButtons ) then
		for key, value in pairs(flashingButtons) do
			toggleFlash(key);
		end
	end
end

-- Start Flashing
function useButton:startFlash()
	if ( not flashingButtons ) then
		flashingButtons = { };
	end
	
	self.flashing = true;
	toggleFlash(self, true);
	flashingButtons[self] = true;
	
	module:unschedule(flashUpdater, true);
	module:schedule(0.5, true, flashUpdater);
end

-- Stop Flashing
function useButton:stopFlash()
	if ( flashingButtons and self.flashing ) then
		self.flashing = nil;
		flashingButtons[self] = nil;
		toggleFlash(self, false);
		if ( not next(flashingButtons) ) then
			module:unschedule(flashUpdater, true);
		end
	end
end

--------------------------------------------
-- Event Handlers

local function eventHandler_UpdateAll(event, unit)
	if ( event ~= "UNIT_INVENTORY_CHANGED" or unit == "player" ) then
		actionButtonList:update();
	end
end

local function eventHandler_HideGrid()
	actionButtonList:hideGrid();
end

local function eventHandler_ShowGrid(event, button)
	actionButtonList:showGrid();
end

local function eventHandler_UpdateState(event, button)
	actionButtonList:updateState();
end

local function eventHandler_UpdateUsable()
	actionButtonList:updateUsable();
	actionButtonList:updateCooldown();
end

local function eventHandler_UpdateBindings()
	actionButtonList:updateBinding();
end

local function eventHandler_CheckRepeat()
	actionButtonList:updateFlash();
end

-- Range checker
local function rangeUpdater()
	actionButtonList:updateRange();
end

--------------------------------------------
-- Preset Groups

module.setupPresetGroups = function(self)
	local object;
	for i = 1, 5, 1 do
		for y = 1, 12, 1 do
			object = useButton:new(i*12 + y, i);
			self:addObjectToGroup(object, object.group);
		end
	end
end

--------------------------------------------
-- Default-Bar additions

-- Out of Range timers
	local oldActionButton_OnUpdate = ActionButton_OnUpdate;
	function ActionButton_OnUpdate(...)
		local rangeTimer = this.rangeTimer;
		if ( rangeTimer and rangeTimer - (...) <= 0 ) then
			if ( colorLack and IsActionInRange(ActionButton_GetPagedID(this)) == 0 ) then
				local icon = getglobal(this:GetName().."Icon");
				local normalTexture = getglobal(this:GetName().."NormalTexture");
				
				if ( colorLack == 2 ) then
					icon:SetVertexColor(0.5, 0.5, 0.5);
				else
					icon:SetVertexColor(0.8, 0.4, 0.4);
				end
			else
				ActionButton_UpdateUsable();
			end
		end
		oldActionButton_OnUpdate(...);
	end

	local oldActionButton_UpdateUsable = ActionButton_UpdateUsable;
	function ActionButton_UpdateUsable(...)
		if ( colorLack and IsActionInRange(ActionButton_GetPagedID(this)) == 0 ) then
			local icon = getglobal(this:GetName().."Icon");
			local normalTexture = getglobal(this:GetName().."NormalTexture");

			if ( colorLack == 2 ) then
				icon:SetVertexColor(0.5, 0.5, 0.5);
			else
				icon:SetVertexColor(0.8, 0.4, 0.4);
			end
			return;
		end

		oldActionButton_UpdateUsable(...);
	end
	
-- Cooldown Count
	local oldActionButton_UpdateCooldown = ActionButton_UpdateCooldown;
	function ActionButton_UpdateCooldown(...)
		oldActionButton_UpdateCooldown(...);
		local id = ActionButton_GetPagedID(this);
		local start, duration, enable = GetActionCooldown(id);
		if ( start > 0 and duration > 0 and enable > 0 ) then
			local cooldown = getglobal(this:GetName().."Cooldown");

			-- Set up variables we need in our cooldown handler
			if ( not this.id ) then
				cooldown.object = this;
				this.id = id;
			end
			startCooldown(cooldown, start, duration);
		end
	end
	
-- Hotkeys
	local oldActionButton_UpdateHotkeys = ActionButton_UpdateHotkeys;
	function ActionButton_UpdateHotkeys(...)
		oldActionButton_UpdateHotkeys(...)
		if ( not displayBindings ) then
			getglobal(this:GetName().."HotKey"):SetText("");
		end
	end

--------------------------------------------
-- Update Initialization

local useButtonMeta = { __index = useButton };
module.useButtonMeta = useButtonMeta;
module.useEnable = function(self)
	self:regEvent("PLAYER_ENTERING_WORLD", eventHandler_UpdateAll);
	self:regEvent("UNIT_INVENTORY_CHANGED", eventHandler_UpdateAll);
	self:regEvent("ACTIONBAR_HIDEGRID", eventHandler_HideGrid);
	self:regEvent("ACTIONBAR_SHOWGRID", eventHandler_ShowGrid);
	self:regEvent("ACTIONBAR_UPDATE_STATE", eventHandler_UpdateState);
	self:regEvent("ACTIONBAR_UPDATE_COOLDOWN", eventHandler_UpdateUsable);
	self:regEvent("ACTIONBAR_UPDATE_USABLE", eventHandler_UpdateUsable);
	self:regEvent("UPDATE_INVENTORY_ALERTS", eventHandler_UpdateUsable);
	self:regEvent("CRAFT_SHOW", eventHandler_UpdateState);
	self:regEvent("CRAFT_CLOSE", eventHandler_UpdateState);
	self:regEvent("TRADE_SKILL_SHOW", eventHandler_UpdateState);
	self:regEvent("TRADE_SKILL_CLOSE", eventHandler_UpdateState);
	self:regEvent("UPDATE_BINDINGS", eventHandler_UpdateBindings);
	self:regEvent("PLAYER_ENTER_COMBAT", eventHandler_CheckRepeat);
	self:regEvent("PLAYER_LEAVE_COMBAT", eventHandler_CheckRepeat);
	self:regEvent("STOP_AUTOREPEAT_SPELL", eventHandler_CheckRepeat);
	self:regEvent("START_AUTOREPEAT_SPELL", eventHandler_CheckRepeat);
	self:regEvent("PLAYER_TARGET_CHANGED", rangeUpdater);
	
	self:schedule(0.3, true, rangeUpdater);
end
module.useDisable = function(self)

	self:unregEvent("PLAYER_ENTERING_WORLD", eventHandler_UpdateAll);
	self:unregEvent("UNIT_INVENTORY_CHANGED", eventHandler_UpdateAll);
	self:unregEvent("ACTIONBAR_HIDEGRID", eventHandler_HideGrid);
	self:unregEvent("ACTIONBAR_SHOWGRID", eventHandler_ShowGrid);
	self:unregEvent("ACTIONBAR_UPDATE_COOLDOWN", eventHandler_UpdateCooldown);
	self:unregEvent("ACTIONBAR_UPDATE_STATE", eventHandler_UpdateState);
	self:unregEvent("ACTIONBAR_UPDATE_USABLE", eventHandler_UpdateUsable);
	self:unregEvent("UPDATE_INVENTORY_ALERTS", eventHandler_UpdateUsable);
	self:unregEvent("CRAFT_SHOW", eventHandler_UpdateState);
	self:unregEvent("CRAFT_CLOSE", eventHandler_UpdateState);
	self:unregEvent("TRADE_SKILL_SHOW", eventHandler_UpdateState);
	self:unregEvent("TRADE_SKILL_CLOSE", eventHandler_UpdateState);
	self:unregEvent("UPDATE_BINDINGS", eventHandler_UpdateBindings);
	self:unregEvent("PLAYER_ENTER_COMBAT", eventHandler_CheckRepeat);
	self:unregEvent("PLAYER_LEAVE_COMBAT", eventHandler_CheckRepeat);
	self:unregEvent("STOP_AUTOREPEAT_SPELL", eventHandler_CheckRepeat);
	self:unregEvent("START_AUTOREPEAT_SPELL", eventHandler_CheckRepeat);
	self:unregEvent("PLAYER_TARGET_CHANGED", rangeUpdater);
	
	self:unschedule(rangeUpdater, true);
end

module.useUpdate = function(self, type, value)
	if ( type == "colorLack" ) then
		if ( value == 3 ) then
			value = false;
		end
		colorLack = value;
		actionButtonList:updateUsable();
		
	elseif ( type == "displayBindings" ) then
		displayBindings = value;
		actionButtonList:updateBinding();
		
	elseif ( type == "displayCount" ) then
		displayCount = value;
		actionButtonList:updateCooldown();
		
	elseif ( type == "buttonLock" ) then
		buttonLock = value;
		actionButtonList:updateLock();
		
	elseif ( type == "hideGrid" ) then
		hideGrid = value;
		actionButtonList:update();
	
	elseif ( type == "init" ) then
		colorLack = self:getOption("colorLack") or 1;
		if ( colorLack == 3 ) then colorLack = false; end
		displayBindings = self:getOption("displayBindings") ~= false;
		displayCount = self:getOption("displayCount") ~= false;
		buttonLock = self:getOption("buttonLock");
		hideGrid = self:getOption("hideGrid");
	end
end