------------------------------------------------------------------------------
-- Shadowshift
-- 
-- This Addon is specifically for shadow-priests who have the "Shadowform"
-- talent, and acts to simulate this as an actual shape-shift ability by
-- placing it on the shapeshift action bar (like rogue stealth and druid
-- forms).
-- 
-- Written by Cirk of Doomhammer, November 2005
------------------------------------------------------------------------------

------------------------------------------------------------------------------
-- AddOn name and version
------------------------------------------------------------------------------
SHADOWSHIFT_NAME = "Cirk's Shadowshift"
SHADOWSHIFT_VERSION = "1.10.0"


------------------------------------------------------------------------------
-- Globals
------------------------------------------------------------------------------
ShadowshiftState = {};					-- Will be overridden when loaded


------------------------------------------------------------------------------
-- Localization (English default)
------------------------------------------------------------------------------
-- Spellname data for localization (must match the spellbook name)
SHADOWSHIFT_SPELLNAMES = {
	SHADOWFORM = "Shadowform",
}

-- Text color emphasis enable and disable (format is |cff<rr><gg><bb<)
SHADOWSHIFT_EM_ON = "|cffffff00"
SHADOWSHIFT_EM_OFF = "|r"

-- Miscellaneous text commands
SHADOWSHIFT_TEXT = {
	DEBUG = SHADOWSHIFT_EM_ON.."Shadowshift: "..SHADOWSHIFT_EM_OFF;
};

-- Slash commands and responses
SHADOWSHIFT_COMMANDS = {
	-- Slash commands
	COMMAND_HELP = "help",
	COMMAND_ON = "on",
	COMMAND_ENABLE = "enable",
	COMMAND_OFF = "off",
	COMMAND_DISABLE = "disable",
	COMMAND_STATUS = "status",
	COMMAND_DEBUGON = "debugon",
	COMMAND_DEBUGOFF = "debugoff",

	-- Slash command responses
	COMMAND_ENABLE_CONFIRM = SHADOWSHIFT_EM_ON.."Shadowshift is enabled"..SHADOWSHIFT_EM_OFF,
	COMMAND_DISABLE_CONFIRM = SHADOWSHIFT_EM_ON.."Shadowshift is disabled"..SHADOWSHIFT_EM_OFF,
	COMMAND_ENABLE_FAILED = SHADOWSHIFT_EM_ON.."Shadowshift cannot be enabled for your class"..SHADOWSHIFT_EM_OFF,
	COMMAND_ENABLED_STATUS = SHADOWSHIFT_EM_ON.."Shadowshift is currently enabled"..SHADOWSHIFT_EM_OFF,
	COMMAND_DISABLED_STATUS = SHADOWSHIFT_EM_ON.."Shadowshift is currently disabled"..SHADOWSHIFT_EM_OFF,
	COMMAND_UNABLE_STATUS = SHADOWSHIFT_EM_ON.."Shadowshift is not enabled for your class"..SHADOWSHIFT_EM_OFF,
	COMMAND_DEBUGON_CONFIRM = "Shadowshift debug is enabled",
	COMMAND_DEBUGOFF_CONFIRM = "Shadowshift debug is disabled",
};

-- Help text
SHADOWSHIFT_HELP = {
	SHADOWSHIFT_EM_ON..SHADOWSHIFT_NAME.." v"..SHADOWSHIFT_VERSION..SHADOWSHIFT_EM_OFF,
	SHADOWSHIFT_EM_ON.."Places the priest \"Shadowform\" talent on the shapeshift bar."..SHADOWSHIFT_EM_OFF,
	"   /shadowshift "..SHADOWSHIFT_COMMANDS.COMMAND_HELP..SHADOWSHIFT_EM_ON.." shows this help message"..SHADOWSHIFT_EM_OFF,
	"   /shadowshift "..SHADOWSHIFT_COMMANDS.COMMAND_ON..SHADOWSHIFT_EM_ON.." enables Shadowshift"..SHADOWSHIFT_EM_OFF,
	"   /shadowshift "..SHADOWSHIFT_COMMANDS.COMMAND_OFF..SHADOWSHIFT_EM_ON.." disables Shadowshift"..SHADOWSHIFT_EM_OFF,
	"   /shadowshift "..SHADOWSHIFT_COMMANDS.COMMAND_STATUS..SHADOWSHIFT_EM_ON.." shows the status of Shadowshift"..SHADOWSHIFT_EM_OFF,
};


------------------------------------------------------------------------------
-- Local constants
------------------------------------------------------------------------------
local SHADOWSHIFT_TEXTURES = {
	-- These override the ones from the spellbook (only ACTIVE really needed)
	NORMAL = "Interface\\Icons\\Spell_Shadow_Shadowform",
	ACTIVE = "Interface\\Icons\\Spell_Shadow_ChillTouch",
};


------------------------------------------------------------------------------
-- Local variables
------------------------------------------------------------------------------
local _thisFrame = nil;					-- The frame pointer
local _serverName = nil;				-- set to current realm when loaded
local _playerName = nil;				-- set to current playername when known
local _classEnabled = nil;				-- set to 1 if the player is of a valid class
local _enabled = 0;						-- whether the AddOn is enabled or not
local _lastUnitMana = nil;				-- because shapeshiftbar doesn't handle UNIT_MANA events properly
local _originalGetNumShapeshiftForms = nil;		-- original GetNumShapeshiftForms function
local _originalGetShapeshiftFormInfo = nil;		-- original GetShapeshiftFormInfo function
local _originalGetShapeshiftFormCooldown = nil;	-- original GetShapeshiftFormCooldown function
local _originalCastShapeshiftForm = nil;		-- original CastShapeshiftForm function
local _originalGameTooltipSetShapeshift = nil;	-- original GameTooltip's SetShapeshift function

local _debugFrame = nil;				-- debug output chat frame


------------------------------------------------------------------------------
-- Data tables
------------------------------------------------------------------------------
local _spellData = nil;					-- will contain information on the Shadowform spell (nil if not present)


------------------------------------------------------------------------------
-- Local functions
------------------------------------------------------------------------------
local function Shadowshift_Register()
	-- Register for events and hook the shapeshift functions.  Note that we
	-- have to process the UNIT_MANA event here in order to tell the
	-- ShapeshiftBar when to update itself, since it has a bug in it and
	-- doesn't actually listen to this event itself (although it should).
	_thisFrame:RegisterEvent("SPELLS_CHANGED");
	_thisFrame:RegisterEvent("UNIT_MANA");
	if (not _originalGetNumShapeshiftForms) then
		_originalGetNumShapeshiftForms = GetNumShapeshiftForms;
		GetNumShapeshiftForms = Shadowshift_GetNumShapeshiftForms;
	end
	if (not _originalGetShapeshiftFormInfo) then
		_originalGetShapeshiftFormInfo = GetShapeshiftFormInfo;
		GetShapeshiftFormInfo = Shadowshift_GetShapeshiftFormInfo;
	end
	if (not _originalGetShapeshiftFormCooldown) then
		_originalGetShapeshiftFormCooldown = GetShapeshiftFormCooldown;
		GetShapeshiftFormCooldown = Shadowshift_GetShapeshiftFormCooldown;
	end
	if (not _originalCastShapeshiftForm) then
		_originalCastShapeshiftForm = CastShapeshiftForm;
		CastShapeshiftForm = Shadowshift_CastShapeshiftForm;
	end
	if (not _originalGameTooltipSetShapeshift) then
		_originalGameTooltipSetShapeshift = GameTooltip.SetShapeshift;
		GameTooltip.SetShapeshift = Shadowshift_GameTooltipSetShapeshift;
	end
end


local function Shadowshift_Unregister()
	-- Unregister for events (but don't unhook the functions though)
	_thisFrame:UnregisterEvent("SPELLS_CHANGED");
end


local function Shadowshift_GetNextParam(text)
	-- Extracts the next parameter out of the passed text, and returns it and
	-- the rest of the string
	if (text) then
		for param, remain in string.gfind(text, "(%w+) +(.*)") do
			return param, remain;
		end
	end
	return text;
end


local function Shadowshift_CheckLoadedVariables()
	-- Make sure that all loaded variables have valid values and performs any
	-- fixups required for version to version changes of Shadowshift
end


------------------------------------------------------------------------------
-- Spellbook and action bar parsing functions
------------------------------------------------------------------------------
local function Shadowshift_CheckSpellbook()
	-- Parses the player's spellbook, looking for the Shadowform spell.  If it
	-- is found, we then register for the UNIT_MANA event so that we can tell
	-- the ShapeshiftBar when to update itself, since it has a bug in it and
	-- doesn't actually listen to this event itself (although it should).
	if (_debugFrame) then
		_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."Shadowshift_CheckSpellbook()");
	end
	_spellData = nil;
	for tab = 1, GetNumSpellTabs(), 1 do
		local name, texture, offset, numSpells = GetSpellTabInfo(tab);
		for index = 1, numSpells do
			local spellName, spellRank = GetSpellName(offset + index, BOOKTYPE_SPELL);
			if (spellName == SHADOWSHIFT_SPELLNAMES.SHADOWFORM) then
				if (_debugFrame) then
					_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."--> found "..spellName.." at index "..(offset + index));
				end
				_spellData = {};
				_spellData.name = spellName;
				_spellData.spellID = offset + index;
				ShadowshiftTooltip:SetOwner(_thisFrame, "ANCHOR_NONE");
				ShadowshiftTooltip:ClearLines();
				ShadowshiftTooltip:SetSpell(_spellData.spellID, BOOKTYPE_SPELL);
				local manaText = Shadowshift_GetNextParam(ShadowshiftTooltipTextLeft2:GetText());
				ShadowshiftTooltip:Hide();
				_spellData.mana = tonumber(manaText) or 0;
				break;
			end
		end
	end
	if (_spellData) then
		_thisFrame:RegisterEvent("UNIT_MANA");	
	else
		_thisFrame:UnregisterEvent("UNIT_MANA");
	end
end


------------------------------------------------------------------------------
-- Hooked functions (public)
------------------------------------------------------------------------------
function Shadowshift_GetNumShapeshiftForms()
	-- Returns the number of shapeshift forms the character has (normally zero
	-- for a priest)
	if (_debugFrame) then
		_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."Shadowshift_GetNumShapeshiftForms()");
	end
	local numForms = _originalGetNumShapeshiftForms();
	if ((_enabled == 1) and (numForms == 0) and _spellData) then
--		if (_debugFrame) then
--			_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."--> overriding shapeshift forms count");
--		end
		numForms = 1;
	end
--	if (_debugFrame) then
--		if (numForms) then
--			_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."--> "..numForms.." forms");
--		else
--			_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."--> nil forms");
--		end
--	end
	return numForms;
end


function Shadowshift_GetShapeshiftFormInfo(index)
	-- Returns information on each of the shapeshift forms the character has
	-- (normally not called for a priest, since forms count is zero).  Here we
	-- use the information from the spellbook functions to determine the
	-- correct return values, where the normal and active textures are filled
	-- in from the SHADOWSHIFT_TEXTURES table (since the spellbook only
	-- returns the "normal" texture).
	if (_debugFrame) then
		_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."Shadowshift_GetShapeshiftFormInfo("..index..")");
	end
	local texture, name, active, castable = _originalGetShapeshiftFormInfo(index);
	if ((_enabled == 1) and not texture and _spellData) then
--		if (_debugFrame) then
--			_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."--> overriding shapeshift information");
--		end
		local mana = UnitMana("player");
		name = _spellData.name;
		active = IsCurrentCast(_spellData.spellID, BOOKTYPE_SPELL);
		if (active) then
			texture = SHADOWSHIFT_TEXTURES.ACTIVE;
			castable = 1;
		else
			texture = SHADOWSHIFT_TEXTURES.NORMAL;
			if (mana < _spellData.mana) then
				castable = nil;
			else
				castable = 1;
			end
		end
	end
--	if (_debugFrame) then
--		if (texture) then
--			_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."--> texture is "..texture);
--		else
--			_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."--> texture is nil");
--		end
--		if (name) then
--			_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."--> name is "..name);
--		else
--			_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."--> name is nil");
--		end
--		if (active) then
--			_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."--> active is "..active);
--		else
--			_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."--> active is nil");
--		end
--		if (castable) then
--			_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."--> castable is "..castable);
--		else
--			_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."--> castable is nil");
--		end
--	end
	return texture, name, active, castable;
end


function Shadowshift_GetShapeshiftFormCooldown(index)
	-- Returns information on the current cooldown for each of the shapeshift
	-- forms the character has (normally not called for a priest, since forms
	-- count is zero).  Here we just use the cooldown information from the
	-- spellbook to return in place of the actual shapeshift information.
	if (_debugFrame) then
		_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."Shadowshift_GetShapeshiftFormCooldown("..index..")");
	end
	local start, duration, enable = _originalGetShapeshiftFormCooldown(index);
	if ((_enabled == 1) and (start == 0) and (duration == 0) and _spellData) then
--		if (_debugFrame) then
--			_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."--> overriding cooldown information");
--		end
		start, duration, enable = GetSpellCooldown(_spellData.spellID, BOOKTYPE_SPELL);	
	end
--	if (_debugFrame) then
--		if (start and duration and enable) then
--			_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."--> cooldown is "..start..", "..duration..", "..enable);
--		else
--			_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."--> no cooldown");
--		end
--	end
	return start, duration, enable;
end


function Shadowshift_CastShapeshiftForm(index)
	-- Casts the shapeshift spell.  For our replacement we simply cast the
	-- appropriate spell from the spellbook.
	if (_debugFrame) then
		_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."Shadowshift_CastShapeshiftForm("..index..")");
	end
	if ((_enabled == 1) and (_originalGetNumShapeshiftForms() == 0) and _spellData) then
--		if (_debugFrame) then
--			_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."--> casting Shadowform");
--		end
		CastSpell(_spellData.spellID, BOOKTYPE_SPELL);
	else
		_originalCastShapeshiftForm(index);
	end
end


function Shadowshift_GameTooltipSetShapeshift(self, index)
	-- This function hooks the GameTooltip's SetShapeshift method, since we
	-- need to replace this call with SetSpell for the appropriate spellID.
	-- Note that we make use of the fact that :method(...) is syntactic sugar
	-- for .method(self, ...) to do this.
	if (_debugFrame) then
		_debugFrame:AddMessage(SHADOWSHIFT_TEXT.DEBUG.."Shadowshift_GameTooltipSetShapeshift(self, "..index..")");
	end
	if ((_enabled == 1) and (_originalGetNumShapeshiftForms() == 0) and _spellData) then
		GameTooltip.SetSpell(self, _spellData.spellID, BOOKTYPE_SPELL);
	else
		_originalGameTooltipSetShapeshift(self, index);
	end
end


------------------------------------------------------------------------------
-- Initialization functions
------------------------------------------------------------------------------
local function Shadowshift_VariablesLoaded()
	-- Called for the VARIABLES_LOADED event, this function retrieves the
	-- current settings, sets the current realm name, and unregisters the
	-- event (we don't need any more notifications for this)
	if (not _serverName) then
		if (not ShadowshiftState) then
			ShadowshiftState = {};
		end
		if (not ShadowshiftState.Servers) then
			ShadowshiftState.Servers = {};
		end
		_serverName = GetCVar("realmName");
		if (not ShadowshiftState.Servers[_serverName]) then
			ShadowshiftState.Servers[_serverName] = {};
		end
		_thisFrame:UnregisterEvent("VARIABLES_LOADED");
	end
end


local function Shadowshift_PlayerLoaded()
	-- This function is called when the player enters the world and checks to
	-- see if the playername is known so the player's per character settings
	-- can be retrieved, and other initializations performed.  Once this is
	-- done, the startup events are unregistered.
	if (_serverName and not _playerName) then
		local playername = UnitName("player");
		if ((not playername) or (playername == UNKNOWNOBJECT) or (playername == UKNOWNBEING)) then
			-- Player not yet known, just return
			return;
		end
		_playerName = playername;
		local _, classname = UnitClass("player");
		if (classname == "PRIEST") then
			_classEnabled = 1;
		end

		_thisFrame:UnregisterEvent("PLAYER_ENTERING_WORLD");
		_thisFrame:UnregisterEvent("UNIT_NAME_UPDATE");

		if (_classEnabled == 1) then
			-- Make sure basic saved variable structure is present
			if (not ShadowshiftState.Servers[_serverName].Characters) then
				ShadowshiftState.Servers[_serverName].Characters = {};
			end
			if (not ShadowshiftState.Servers[_serverName].Characters[_playerName]) then
				ShadowshiftState.Servers[_serverName].Characters[_playerName] = {};
			end
			if (not ShadowshiftState.Servers[_serverName].Characters[_playerName].Enabled) then
				ShadowshiftState.Servers[_serverName].Characters[_playerName].Enabled = 1;
			end
			_enabled = ShadowshiftState.Servers[_serverName].Characters[_playerName].Enabled;

			-- Process the loaded variables (and do past version cleanups)
			Shadowshift_CheckLoadedVariables();

			-- If enabled, start it now
			if (_enabled == 1) then
				Shadowshift_Register();
				Shadowshift_CheckSpellbook();
				ShapeshiftBar_Update();
			end
		else
			_enabled = 0;
			if (ShadowshiftState.Servers[_serverName].Characters) then
				if (ShadowshiftState.Servers[_serverName].Characters[_playerName]) then
					ShadowshiftState.Servers[_serverName].Characters[_playerName] = nil;
				end
			end
		end
	end
end


------------------------------------------------------------------------------
-- OnEvent handler
------------------------------------------------------------------------------
function Shadowshift_OnEvent(event)
	if (event == "SPELLS_CHANGED") then
		Shadowshift_CheckSpellbook();
		ShapeshiftBar_Update();
		return;
	end
	if ((event == "UNIT_MANA") and _spellData) then
		local unitMana = UnitMana("player");
		if (_lastUnitMana) then
			if ((_lastUnitMana < _spellData.mana) and (unitMana >= _spellData.mana)) then
				ShapeshiftBar_UpdateState();
			elseif ((unitMana < _spellData.mana) and (_lastUnitMana >= _spellData.mana)) then
				ShapeshiftBar_UpdateState();
			end
		else
			ShapeshiftBar_UpdateState();
		end
		_lastUnitMana = unitMana;
	end
	if (event == "VARIABLES_LOADED") then
		Shadowshift_VariablesLoaded();
		return;
	end
	if ((event == "PLAYER_ENTERING_WORLD") or
		((event == "UNIT_NAME_UPDATE") and (arg1 == "player"))) then
		Shadowshift_PlayerLoaded();
		return;
	end
end


------------------------------------------------------------------------------
-- OnLoad function
------------------------------------------------------------------------------
function Shadowshift_OnLoad()
	-- Record our frame pointer for later
	_thisFrame = this;

	-- Register for player events
	_thisFrame:RegisterEvent("VARIABLES_LOADED");
	_thisFrame:RegisterEvent("PLAYER_ENTERING_WORLD");
	_thisFrame:RegisterEvent("UNIT_NAME_UPDATE");

	-- Register slash command handler
	SLASH_SHADOWSHIFT1 = "/shadowshift";
	SlashCmdList["SHADOWSHIFT"] = function(text)
		Shadowshift_SlashCommand(text);
	end

	-- Announce ourselves
	if (DEFAULT_CHAT_FRAME) then
		DEFAULT_CHAT_FRAME:AddMessage(SHADOWSHIFT_NAME.." v"..SHADOWSHIFT_VERSION.." loaded");
	end
end


------------------------------------------------------------------------------
-- Slash command function
------------------------------------------------------------------------------
function Shadowshift_SlashCommand(text)
	if (text) then
		local command, params = Shadowshift_GetNextParam(string.lower(text));
		if ((command == SHADOWSHIFT_COMMANDS.COMMAND_ON) or (command == SHADOWSHIFT_COMMANDS.COMMAND_ENABLE)) then
			if (_classEnabled == 1) then
				ShadowshiftState.Servers[_serverName].Characters[_playerName].Enabled = 1;
				_enabled = 1;
				Shadowshift_Register();
				Shadowshift_CheckSpellbook();
				ShapeshiftBar_Update();
				if (DEFAULT_CHAT_FRAME) then
					DEFAULT_CHAT_FRAME:AddMessage(SHADOWSHIFT_COMMANDS.COMMAND_ENABLE_CONFIRM);
				end
			else
				if (DEFAULT_CHAT_FRAME) then
					DEFAULT_CHAT_FRAME:AddMessage(SHADOWSHIFT_COMMANDS.COMMAND_ENABLE_FAILED);
				end
			end
		elseif ((command == SHADOWSHIFT_COMMANDS.COMMAND_OFF) or (command == SHADOWSHIFT_COMMANDS.COMMAND_DISABLE)) then
			if (_classEnabled == 1) then
				ShadowshiftState.Servers[_serverName].Characters[_playerName].Enabled = 0;
				_enabled = 0;
				Shadowshift_Unregister();
				ShapeshiftBar_Update();
				if (DEFAULT_CHAT_FRAME) then
					DEFAULT_CHAT_FRAME:AddMessage(SHADOWSHIFT_COMMANDS.COMMAND_DISABLE_CONFIRM);
				end
			else
				if (DEFAULT_CHAT_FRAME) then
					DEFAULT_CHAT_FRAME:AddMessage(SHADOWSHIFT_COMMANDS.COMMAND_UNABLE_STATUS);
				end
			end
		elseif (command == SHADOWSHIFT_COMMANDS.COMMAND_STATUS) then
			if (DEFAULT_CHAT_FRAME) then
				if (_enabled == 1) then
					DEFAULT_CHAT_FRAME:AddMessage(SHADOWSHIFT_COMMANDS.COMMAND_ENABLED_STATUS);
				else
					if (_classEnabled == 1) then
						DEFAULT_CHAT_FRAME:AddMessage(SHADOWSHIFT_COMMANDS.COMMAND_DISABLED_STATUS);
					else
						DEFAULT_CHAT_FRAME:AddMessage(SHADOWSHIFT_COMMANDS.COMMAND_UNABLE_STATUS);
					end
				end
			end
		elseif (command == SHADOWSHIFT_COMMANDS.COMMAND_DEBUGON) then
			_debugFrame = DEFAULT_CHAT_FRAME;
			if (DEFAULT_CHAT_FRAME) then
				DEFAULT_CHAT_FRAME:AddMessage(SHADOWSHIFT_COMMANDS.COMMAND_DEBUGON_CONFIRM);
			end
		elseif (command == SHADOWSHIFT_COMMANDS.COMMAND_DEBUGOFF) then
			_debugFrame = nil;
			if (DEFAULT_CHAT_FRAME) then
				DEFAULT_CHAT_FRAME:AddMessage(SHADOWSHIFT_COMMANDS.COMMAND_DEBUGOFF_CONFIRM);
			end
		else
			if (DEFAULT_CHAT_FRAME) then
				for _, string in SHADOWSHIFT_HELP do
					DEFAULT_CHAT_FRAME:AddMessage(string);
				end
			end
		end
	end
end


