--[[---------------------------------------------------------------------------

Copyright (c) 2008 by K. Scott Piel 
All Rights Reserved

E-mail: < kscottpiel@gmail.com >
Web:    < http://www.scottpiel.com >

This file is part of nUI.

    nUI is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    nUI is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with nUI.  Igf not, see <http://www.gnu.org/licenses/>.
	
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-------------------------------------------------------------------------------

nUI Skinning Options: Unit

class options =
{
	string name					-- a unique name for the unit frame
	string skinName				-- which unit skin to use for this frame
	string strata				-- the frame strata to use (BACKGROUND, LOW, HIGH, etc.) or nil to use the parent frame's strata
	string unit_id				-- the unit ID assigned to this frame
	int party_id				-- the party id (1-4) for a party frame, nil otherwise
	int raid_id					-- the raid id (1-40) for a raid frame, nil otherwise
	int level					-- the frame level to use for the aura icons (1-9) or nil to use the parent frame's level+1
	float scale         		-- adjusts the siz of the frame: 1 = normal (the actual size defined by the skin)
	boolean clickable			-- when false, disables click targeting, right-click menu, mouseover tooltips, etc.
	
	class popup					-- how and where to apply the unit frame popup menu -- required if clickable == true
	{
		string anchor_pt		-- anchor point on the popup frame to use
		string reltive_pt		-- anchor point on the unit frame to use
		float xOfs				-- horizontal distance between anchor_pt and relative_pt
		float yOfs				-- vertical distance between anchor_pt and relative_pt
		
		class color { r,g,b,a }	-- the background color to use for the popup menu
	};
	
	class border				-- gives the entire unit frame a border when not nil (hidden when the unit does not exist)
	{
		class backdrop			-- description of the backdrop itself
		{
			string bgFile		-- path to the frame background texture or nil for no background
			string edgeFile		-- path to the frame edge texture file or nil for no frame edge
			boolean tile		-- true if the background texture should be tiled to fill the frame
			float tileSize		-- size of each background tile
			float edgeSize		-- thickness of the edge texture around the frame
			
			class insets = 
			{	
				int left		-- inset of background texture from the outside edges of the frame
				int right
				int top
				int bottom
			};
		};
		
		class color
		{
			class border = { r, g, b, a }	-- color and transparency of the edge texture
			class backdrop = { r, g, b, a }	-- color and transparency of the background texture
		};
	};
	
	class background			-- if not nil, gives the entire unit frame a background that is visible even when the unit does not exist 
	{
		class backdrop			-- description of the backdrop itself
		{
			string bgFile		-- path to the frame background texture or nil for no background
			string edgeFile		-- path to the frame edge texture file or nil for no frame edge
			boolean tile		-- true if the background texture should be tiled to fill the frame
			float tileSize		-- size of each background tile
			float edgeSize		-- thickness of the edge texture around the frame
			
			class insets = 
			{	
				int left		-- inset of background texture from the outside edges of the frame
				int right
				int top
				int bottom
			};
		};
		
		class color
		{
			class border = { r, g, b, a }	-- color and transparency of the edge texture
			class backdrop = { r, g, b, a }	-- color and transparency of the background texture
		};
	};
}

--]]---------------------------------------------------------------------------
	
if not nUI_Unit then nUI_Unit = {}; end
if not nUI_UnitSkins then nUI_UnitSkins = {}; end
if not nUI_UnitOptions then nUI_UnitOptions = {}; end
if not nUI_DefaultConfig then nUI_DefaultConfig = {}; end

nUI_DefaultConfig.UnitOptions =
{
	click_cast = "yes",
};

nUI_Unit.frame_rate        = 1 / nUI_DEFAULT_FRAME_RATE;
nUI_UnitOptions.click_cast = strlower( nUI_UnitOptions.click_cast or nUI_DefaultConfig.UnitOptions.click_cast );

-------------------------------------------------------------------------------
-- create a unit frame element

local function CreateElement( parent, type, id, options, anchor )
	
	local clickable = parent.options.clickable;
	local unit_id   = parent.unit;
	local frame;

	if     type == "Aura"        then frame = nUI_Unit:createAuraFrame( parent, unit_id, id, options, clickable );
	elseif type == "Casting"     then frame = nUI_Unit:createCastingFrame( parent, unit_id, id, options, clickable );
	elseif type == "Class"       then frame = nUI_Unit:createClassFrame( parent, unit_id, id, options, clickable );
	elseif type == "Combat"      then frame = nUI_Unit:createCombatFrame( parent, unit_id, id, options, clickable );
	elseif type == "ComboPoints" then frame = nUI_Unit:createComboPointsFrame( parent, unit_id, id, options, clickable );
	elseif type == "Feedback"    then frame = nUI_Unit:createFeedbackFrame( parent, unit_id, id, options, clickable );
	elseif type == "Happiness"   then frame = nUI_Unit:createHappinessFrame( parent, unit_id, id, options, clickable );
	elseif type == "Health"      then frame = nUI_Unit:createHealthFrame( parent, unit_id, id, options, clickable );
	elseif type == "Label"       then frame = nUI_Unit:createLabelFrame( parent, unit_id, id, options, clickable );
	elseif type == "Level"       then frame = nUI_Unit:createLevelFrame( parent, unit_id, id, options, clickable );
	elseif type == "Portrait"    then frame = nUI_Unit:createPortraitFrame( parent, unit_id, id, options, clickable );
	elseif type == "Power"       then frame = nUI_Unit:createPowerFrame( parent, unit_id, id, options, clickable );
	elseif type == "PvP"         then frame = nUI_Unit:createPvPFrame( parent, unit_id, id, options, clickable );
	elseif type == "RaidGroup"   then frame = nUI_Unit:createRaidGroupFrame( parent, unit_id, id, options, clickable );
	elseif type == "RaidTarget"  then frame = nUI_Unit:createRaidTargetFrame( parent, unit_id, id, options, clickable );
	elseif type == "Range"       then frame = nUI_Unit:createRangeFrame( parent, unit_id, id, options, clickable );
	elseif type == "ReadyCheck"  then frame = nUI_Unit:createReadyCheckFrame( parent, unit_id, id, options, clickable );
	elseif type == "Resting"     then frame = nUI_Unit:createRestingFrame( parent, unit_id, id, options, clickable );
	elseif type == "Role"        then frame = nUI_Unit:createRoleFrame( parent, unit_id, id, options, clickable );
	elseif type == "Spec"        then frame = nUI_Unit:createSpecFrame( parent, unit_id, id, options, clickable );
	elseif type == "Status"      then frame = nUI_Unit:createStatusFrame( parent, unit_id, id, options, clickable );
	elseif type == "Frame"       then frame = nUI_Unit:createFrame( options.name, parent, unit_id, false ); frame.applyOptions( options );
	else
		DEFAULT_CHAT_FRAME:AddMessage( nUI_L["nUI_Unit: [%s] is not a valid unit frame element type!"]:format( type ), 1, 0.5, 0.5 );
	end

	local element =
	{
		type    = type,
		id      = id,
		options = options,
		anchor  = anchor,
		frame   = frame,
	};
	
	return frame and element or nil;
	
end

-------------------------------------------------------------------------------
-- apply a unit skin to a unit

local function ApplySkin( unit, skin )
	
	local skin = skin or unit.skin;
	
	if skin then
		
		local skin_elements   = skin.elements;
		local update_elements = {};

		unit.skin = skin;
		
		-- first step is to delete any unit frame elements that are no longer desired...
		-- this only happens when a user is editing a unit skin and disables an element
		
		for i,element in ipairs( unit.Elements ) do
			
			if not skin_elements[element.type] then
				
				nUI_Unit:deleteFrame( element.frame );
				nUI:TableRemoveByValue( unit.Elements, element );
				
			elseif element.id and not skin_elements[element.type][element.id] then
				
				nUI_Unit:deleteFrame( element.frame );
				nUI:TableRemoveByValue( unit.Elements, element );
				
			else
				
				if not update_elements[element.type] then 
					update_elements[element.type] = {}; 
				end
				
				update_elements[element.type..(element.id or "")] = element;
				
			end			
		end
		
		-- now span the skin and either create a new unit frame element that 
		-- is needed if it does not exist, or update the element's options if
		-- is does already exist
		
		for type in pairs( skin_elements ) do
			
			local element = skin_elements[type];
			
			-- is this an array of elements (such as buffs or frames?)
			
			if element[1] then
				
				-- create a new unit frame element for each index in the array
				
				for id=1, #element do
					
					local update = update_elements[type..id];
				
					-- if the element does not already exist, create it
					
					if not update then
						
						update = CreateElement( unit, type, id, element[id].options, element[id].anchor );
						
						if update then
							table.insert( unit.Elements, update );
						end
						
					-- otherwise, update the information about it and update the element's options
					
					else
						
						update.options = element[id].options;
						update.anchor  = element[id].anchor;
						
						update.frame.applyOptions( update.options );
						
					end
				end
				
			-- otherwise, there's no sub-array to parse, so use this leg
			
			else
				
				local update = update_elements[type];
			
				-- if the element does not already exist, create it
				
				if not update then
					
					update = CreateElement( unit, type, nil, element.options, element.anchor );
					
					if update then
						table.insert( unit.Elements, update );
					end
					
				-- otherwise, update the information about it and update the element's options
				
				else
					
					update.options = element.options;
					update.anchor  = element.anchor;
					
					update.frame.applyOptions( update.options );
					
				end
			end
		end
		
		-- now that we have created all of the frame elements, apply the anchors to them

		for i=1, #unit.Elements do			
			unit.Elements[i].frame.applyAnchor( unit.Elements[i].anchor );
		end
		
	end	
end

-------------------------------------------------------------------------------
-- create a new unit

function nUI_Unit:createUnit( name, parent, options )
	
	-- create the frame itself if it doesn't already exist
	
	local frame = _G[name] or nUI_Unit:createFrame( name, parent, options.unit_id, options.clickable );
	
	frame.Elements    = {}; 
	frame.Faders      = {};
	frame.parent      = parent;
	frame.enabled     = true;
	
	-- create a background frame
	
	frame.background = CreateFrame( "Frame", name.."_Background", parent );
	frame.background:SetAllPoints( frame );
	
	-- create a layer for the unit popup

	frame.popup = CreateFrame( "Frame", "$parent_Popup", frame, "UIDropDownMenuTemplate" );
	frame.popup.class = frame;

	frame.popup:SetFrameLevel( 9 );
	frame.popup.texture = frame.popup:CreateTexture( "$parentTexture" );
	frame.popup.texture:SetAllPoints( frame.popup );	
	frame.popup:Hide();

	-- prepare a unit frame popup menu for display
	
	frame.popup.onUnitPopupUpdate = function()
	
		local unit_info  = frame.unit_info;
		local unit_id    = frame.unit;
		local unit_popup = nil;
		local raid_id    = nil;
		local unit_name  = nil;
		
		-- select the popup menu to be displayed
	
		if unit_info then
				
			if unit_info.is_self then
				
				unit_popup = "SELF";
				
			elseif unit_info.is_pet then
				
				unit_popup = "PET";
		
			elseif unit_info.is_player then
				
				if unit_info.in_raid then
					
					unit_popup = "RAID_PLAYER";
					unit_name  = unit_info.unit_name;
					
				elseif unit_info.in_party then
					
					unit_popup = "PARTY";
					
				else
					
					unit_popup = "PLAYER";
					
				end
				
			else
				unit_popup = "RAID_TARGET_ICON";
				unit_name  = RAID_TARGET_ICON;
			end
			
			-- enable it
			
			UnitPopup_ShowMenu( frame.popup, unit_popup, unit_id, unit_name, unit_info.in_raid );
			
		end	
	end
	
	-- initialize the popup handler
	
	UIDropDownMenu_Initialize( frame.popup, frame.popup.onUnitPopupUpdate, "MENU" );	
	HideDropDownMenu( 1 );

	-- there's no point in doing all of the graphics updates on the frame's
	-- elements if the frame itself is hidden (such as a solo unit frame panel
	-- member like "player" trying to update the unit graphics when the party
	-- unit frame panel is the one that's being displayed. So, in order to
	-- reduce drag on the graphics engine and the frame rate, when this unit
	-- frame is hidden, we disabled all of the frame elements. When the frame
	-- is shown, we enabled its graphic elements if the frame itself is enabled
	
	frame:SetScript( "OnShow",
		function()
			if frame.enabled then
				for i,element in ipairs( frame.Elements ) do
					element.frame.setEnabled( true );
				end
			end
		end
	);
	
	frame:SetScript( "OnHide",
		function()
			if frame.enabled then
				for i,element in ipairs( frame.Elements ) do
					element.frame.setEnabled( false );
				end
			end
		end
	);
	
	-- called when the underlying GUID for the unit changes or when the
	-- content of the GUID is updated
	
	frame.newUnitInfo = function( list_unit, unit_info )
		
		frame.unit_info = unit_info;
		
	end
	
	-- setting enabled to false will prevent the frame from updating when new
	-- unit information is received (saves framerate). Setting enabled true will
	-- call the frame to immediately update if its content has changed since it
	-- was disabled
	
	frame.setEnabled = function( enabled )

		if enabled ~= frame.enabled then
				
			frame.enabled = enabled;
	
			for i,element in ipairs( frame.Elements ) do
				element.frame.setEnabled( enabled );
			end
		end		
	end
	
	-- used to change the scale of the frame... rather than the Bliz widget frame:SetScale()
	-- this method actually recalculates the size of the frame and uses frame:SetHeight()
	-- and frame:SetWidth() to reflect the actual size of the frame.
	
	frame.applyScale = function( scale )

		local anchor    = scale and frame.anchor or nil;
		local scale     = scale or frame.scale or 1;
		local height    = frame.skin.height * scale * nUI.scale;
		local width     = frame.skin.width * scale * nUI.scale;
		
		frame.scale  = scale;
		
		if frame.height         ~= height
		or frame.width          ~= width
		then

			scale = scale * nUI.scale;
			
			frame.height         = height;
			frame.width          = width;
			
			frame:SetHeight( height );
			frame:SetWidth( width );
			
			frame.popup:ClearAllPoints();
			frame.popup:SetPoint( "BOTTOMLEFT", frame, "CENTER", 0, 0 );
--[[			
			if frame.options.popup then
				
				local anchor = frame.options.popup or {};
				
				frame.popup:ClearAllPoints();
				frame.popup:SetPoint( anchor.anchor_pt or "CENTER", frame, anchor.relative_pt or "CENTER", (anchor.xOfs or 0) * scale, (anchor.yOfs or 0) * scale );
				
			end
]]--
		end
		
		if anchor then frame.applyAnchor(); end
		
	end

	-- change the scale of the unit frame and all its children
	
	frame.changeScale = function( scale )
		
		frame.applyScale( scale );
		
		for i,element in ipairs( frame.Elements ) do
			element.frame.applyScale( scale );
		end
		
	end

	-- this method allows for multiple unit modules to manage frame fading
	
	frame.applyFrameFader = function( owner, fade_alpha )
	
		local alpha         = 1;		
		frame.Faders[owner] = fade_alpha;
		
		for owner in pairs( frame.Faders ) do
			
			if frame.Faders[owner]
			and frame.Faders[owner] < alpha
			then alpha = frame.Faders[owner];
			end
			
		end
		
		if frame.alpha ~= alpha then
			frame.alpah = alpha;
			frame:SetAlpha( alpha );
		end
	end
				
	-- this method applies the anchor point of the frame. As with all else, the
	-- frame's anchor is only moved if the point defined is different than the
	-- point that is already known
	
	frame.applyAnchor = function( anchor )

		local anchor      = anchor or frame.anchor or {};
		local scale       = nUI.scale;
		local anchor_pt   = anchor.anchor_pt or "CENTER";
		local relative_to = anchor.relative_to or frame.parent:GetName() or "nUI_Dashboard";
		local relative_pt = anchor.relative_pt or "CENTER";
		local xOfs        = (anchor.xOfs or 0) * scale;
		local yOfs        = (anchor.yOfs or 0) * scale;

		frame.anchor = anchor;
		
		if frame.anchor_pt   ~= anchor_pt
		or frame.relative_to ~= relative_to
		or frame.relative_pt ~= relative_pt
		or frame.xOfs        ~= xOfs
		or frame.yOfs        ~= yOfs
		then
			
			frame.anchor_pt   = anchor_pt;
			frame.relative_to = relative_to;
			frame.relative_pt = relative_pt;
			frame.xOfs        = xOfs;
			frame.yOfs        = yOfs;
			
			if not anchor.relative_to then
				DEFAULT_CHAT_FRAME:AddMessage( nUI_L["nUI: Warning.. anchoring %s to %s -- anchor point has a <nil> value."]:format( (frame:GetName() or nUI_L["<unnamed frame>"] ), relative_to ) );
			else
				frame:ClearAllPoints();
				frame:SetPoint( anchor_pt, relative_to:gsub( "$parent", (frame.parent:GetName() or "nUI_Dashboard") ), relative_pt, xOfs, yOfs );
			end	
		end
	end
	
	-- applies the set of frame options to this frame. Typically called when the frame 
	-- is first created or when the user changes options via config.
	
	frame.applyOptions = function( options )
	
		local name   = options.skinName or nUI_UNITSKIN_SOLOPLAYER;
		local skin   = nUI_UnitSkins[name];
		
		if not skin then
			
			DEFAULT_CHAT_FRAME:AddMessage( nUI_L["nUI_Unit: [%s] is not a known unit skin name!"]:format( name ), 1, 0.5, 0.5 );
			
		else
	
			frame.options  = options;	
			frame.skinName = name;
			frame.skin     = skin;
			
			-- set the raid/party id if required
		
			frame.id = options.raid_id or options.party_id;
			
			if frame.id then frame:SetID( frame.id ); end
		
			-- set up frame layering
			
			frame:SetFrameStrata( options.strata or frame.parent:GetFrameStrata() );
			frame:SetFrameLevel( options.level or (frame.parent:GetFrameLevel()+1) );
			frame.background:SetFrameLevel( (options.level or frame:GetFrameLevel())-1 );
			
			-- if there's a border, set it
			
			if options.border then
					
				local border_color = options.border.color.border;
				local backdrop_color = options.border.color.backdrop;
				
				frame:SetBackdrop( options.border.backdrop );
				frame:SetBackdropBorderColor( border_color.r, border_color.g, border_color.b, border_color.a );
				frame:SetBackdropColor( backdrop_color.r, backdrop_color.g, backdrop_color.b, backdrop_color.a );
		
			else 
				
				frame:SetBackdrop( nil );
				
			end
			
			-- if there's a background, set it
			
			if options.background then
					
				local border_color = options.background.color.border;
				local backdrop_color = options.background.color.backdrop;
				
				frame.background:SetAlpha( 1 );
				frame.background:SetBackdrop( options.background.backdrop );
				frame.background:SetBackdropBorderColor( border_color.r, border_color.g, border_color.b, border_color.a );
				frame.background:SetBackdropColor( backdrop_color.r, backdrop_color.g, backdrop_color.b, backdrop_color.a );
		
			else 
				
				frame.background:SetAlpha( 0 );
				frame.background:SetBackdrop( nil );
				
			end
	
			-- set up the popup menu
			
			if options.popup then
	
				local color = options.popup.color or {};
				
				frame.popup.texture:SetTexture( color.r or 0, color.g or 0, color.b or 0, color.a or 0.75 );
				
			end
			
			-- size and anchor the frame
			
			frame.applyScale( options.scale or frame.scale or 1 );
			
		end		
	end
	
	-- apply a skin to the frame, or refresh the current skin
	
	frame.applySkin = function( skinName )

		local name   = skinName or frame.skinName or nUI_UNITSKIN_SOLOPLAYER;
		local skin   = nUI_UnitSkins[name];
		
		if not skin then
			
			DEFAULT_CHAT_FRAME:AddMessage( nUI_L["nUI_Unit: [%s] is not a known unit skin name!"]:format( name ), 1, 0.5, 0.5 );
			
		else

			ApplySkin( frame, skin );
			
		end
	end
	
	-- if this frame is using the named skin then refresh it
	
	frame.refreshSkin = function( skinName )
		
		if frame.skinName == skinName then frame.applySkin(); end
		
	end
	
	-- initiate the frame
	
	frame.unit_info = nUI_Unit:registerUnitChangeCallback( frame.unit, frame );
	
	frame.applyOptions( options );
	frame.applySkin();
	
	-- register the frame for scaling
	
	nUI:registerScalableFrame( frame );
	
	-- register the unit frame with the game engine
	
	RegisterUnitWatch( frame );
	
	return frame;
	
end

-------------------------------------------------------------------------------
-- delete an existing unit

function nUI_Unit:deleteUnit( frame )

	if frame.Elements then
		
		for i,element in ipairs( frame.Elements ) do
			nUI_Unit:deleteFrame( element.frame );
		end
		
		frame.Elements = {};
		
	end	
	
	UnregisterUnitWatch( frame );
	
	nUI:unregisterScalableFrame( frame );
	nUI_Unit:unregisterUnitChangeCallback( frame.unit, frame );
	nUI_Unit:deleteUnit( frame );
	
end
