-----------------------------------------------------------------------------------------
--
-- Author: AurÃ©lien Defossez
-- (c) 2014 Tabemasu Games (www.tabemasu.com)
--
-- Sprite.lua
--
-- An abstract layer over the Corona sprites, used to simplify how it is handled by
-- Corona.
-- It features:
--   Easy loading and unloading of spritesheets
--   Easy handling of sprites
--   The spritePause and spriteChangeSpeed events
--   The possibility to add attachments
--   A better sprite event than the native one
--
-- The sprite will throw ecussonSprite events, which will have the same parameters
-- as the native event, except the phase:
--  phase = began when the animation starts
--  phase = next for each animation step
--  phase = loop when the animation starts again (if looping)
--  phase = ended when the animation ends (not on the last frame, but after it)
--
-----------------------------------------------------------------------------------------

local utils = require("lib.ecusson.Utils")
local vec2 = require("lib.ecusson.math.vec2")
local EcussonDisplayObject = require("lib.ecusson.internal.EcussonDisplayObject")

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

local Super = EcussonDisplayObject
local Class = utils.extend(Super)
local Sprite = Class

-----------------------------------------------------------------------------------------
-- Class attributes
-----------------------------------------------------------------------------------------

local random = math.random

-- Setup variables
local imagePath
local datapath
local animationData
local attachments

-- The different loaded spritesheets
local spritesheets = {}

-- The different sets of sprites (e.g. the zombie set contains all animations concerning the zombie)
local sets = {}

-- The different attachments
local attachments = {}

-- The timescale (1 mean normal speed, 0.5 half-speed and 2 double speed)
local timeScale = 1

-----------------------------------------------------------------------------------------
-- Class methods
-----------------------------------------------------------------------------------------

-- Setup sprite library
--
-- Parameters:
--  imagePath: The path to the sprite textures
--  datapath: The path to the generated sprite sheet data files
--  animationData: The user-defined animation data,
--  attachments: The attachments definition (optional)
function Class.setup(options)
	imagePath = options.imagePath
	datapath = options.datapath
	animationData = options.animationData
	attachments = options.attachments
end

-- Load a single sheet
--
-- Parameters:
--  sheetName: The sheet to load
function Class.loadSheet(sheetName)
	if not spritesheets[sheetName] then
		print("[Sprite] Load spriteSheet   "..sheetName)
		-- Load sprite sheet data (either with getSheet method or getSpriteSheetData)
		local dataFile = require(datapath..sheetName)
		local sheetInfo = dataFile.getSheet and dataFile:getSheet() or dataFile.getSpriteSheetData()

		-- Create image sheet
		local imageSheet = graphics.newImageSheet(imagePath..sheetName..".png", sheetInfo)
		assert(imageSheet, "imageSheet not found ("..imagePath..sheetName..".png"..")")

		-- Frame indices (name => index)
		local indexer
		if dataFile.getFrameIndex then
			-- For spritesheets generated with TexturePacker
			indexer = dataFile
		else
			-- For spritesheets generated by a tweaked Zwoptex
			local spriteSheetIndex = {}
			for key, value in pairs(data.frames) do
				spriteSheetIndex[value.name] = key
			end

			-- Create fake class for depecrated Zwoptex format
			indexer = {
				spriteSheetIndex = spriteSheetIndex,

				getFrameIndex = function(self, spriteName)
					return self.spriteSheetIndex[spriteName..".png"]
				end
			}
		end

		-- Load sprite sets
		for setId, sprites in pairs(animationData[sheetName]) do
			local animations = {}
			local animationOptions = {}
			local animationCount = 1

			if sets[setId] then
				utils.softError("[Warning] Error while loading spritesheet "..sheetName..": "..
					"SpriteSet "..setId.." already defined.")
			end

			for animationName, animation in pairs(sprites) do
				local variations = animation.variations or {{ suffix = "" }}

				-- Load sprite variations (if any)
				for _, variation in pairs(variations) do
					local suffix = variation.suffix

					-- Frames
					local frames = {}
					if animation.frames then
						for j = 1, #animation.frames do
							local frameIndex = animation.frames[j]
							local zeroedIndex = frameIndex < 10 and "0"..frameIndex or frameIndex
							frames[j] = indexer:getFrameIndex(setId.."_"..animationName..suffix.."_"..zeroedIndex)
						end
					else
						local frameCount = animation.frameCount or 1

						-- Rename sprite as the first sprite of the animation if it is one
						if frameCount == 1 then
							frames = {
								indexer:getFrameIndex(setId.."_"..animationName..suffix)
							}
						else
							for j = 1, frameCount do
								local zeroedIndex = j < 10 and "0"..j or j
								frames[j] = indexer:getFrameIndex(setId.."_"..animationName..suffix.."_"..zeroedIndex)
							end
						end
					end

					-- If this animation is timed, we need to add a second frame to make it work
					if #frames == 1 and animation.period then
						frames[2] = frames[1]
					end

					-- Add the animation to the animations
					animations[animationCount] = {
						name = animationName..suffix,
						frames = frames,
						-- Set the period to 1ms if no period is defined (= steady frame)
						time = animation.period and animation.period * 1000 or 1,
						loopCount = 0,
						loopDirection = "forward"
					}

					animationCount = animationCount + 1
				end

				-- Add the Ecusson specific options
				animationOptions[animationName] = {
					-- Set the loopCount parameter to 0 (loop indefinitely) by default
					loopCount = animation.loopCount or 0,
					-- Set the randomStartFrame parameter to false (not random) by default
					randomStartFrame = animation.randomStartFrame or false,
					variations = animation.variations,
					frames = animations[animationCount - 1].frames,
					attachments = animation.attachments or {}
				}
			end

			sets[setId] = {
				imageSheet = imageSheet,
				sheetInfo = sheetInfo,
				animations = animations,
				animationOptions = animationOptions
			}
		end

		spritesheets[sheetName] = true
	end
end

-- Load a bunch of spritesheets
--
-- Parameters:
--  sheetNames: An array with the names of the spritesheets to load
function Class.loadSheets(sheetNames)
	if sheetNames then
		for i = 1, #sheetNames do
			Sprite.loadSheet(sheetNames[i])
		end
	end
end

-- Unload every loaded spritesheet
function Class:tearDown()
	for sheetName, sheet in pairs(spritesheets) do
		Sprite.unloadSheet(sheetName)
	end
end

-- Unload a spritesheet previously loaded with loadSheet
--
-- Parameters:
--  sheetName: The sheet to unload
function Class.unloadSheet(sheetName)
	if spritesheets[sheetName] then
		print("[Sprite] Unload spriteSheet "..sheetName)

		for setId, sprites in pairs(animationData[sheetName]) do
			sets[setId] = nil
		end

		spritesheets[sheetName] = false
	end
end

-- Unload a bunch of spritesheets previously loaded with loadSheet
--
-- Parameters:
--  sheetNames: An array with the names of the spritesheets to unload
function Class.unloadSheets(sheetNames)
	if sheetNames then
		for i = 1, #sheetNames do
			Sprite.unloadSheet(sheetNames[i])
		end
	end
end

-- Sets the animation time scale for all the sprites.
-- A time scale of 1.0 runs the animation at normal speed.
-- A time scale of 2.0 runs the animation twice as fast.
-- A time scale of 0.5 runs the animation at half speed.
--
-- Parameters:
--  newTimeScale: The time scale
function Class.setTimeScale(newTimeScale)
	timeScale = newTimeScale

	Runtime:dispatchEvent{
		name = "spriteChangeSpeed",
		timeScale = newTimeScale
	}
end

-- Return the sprite sets
function Class.getSets()
	return sets
end

-----------------------------------------------------------------------------------------
-- Initialization and Destruction
-----------------------------------------------------------------------------------------

-- Create the sprite
--
-- Parameters:
--  spriteSet: The spriteSet name
--  group: The display group to add the sprite to (optional)
--  anchor: The anchor point, either a string (tl, tc, tr, cl, c, cr, bl, bc, br)
--    or a vec2 between vec2(0, 0) and vec2(1, 1) (default is centered, vec2(0.5, 0.5))
--  position: The position (default is vec2(0, 0))
--  animation: The animation to play (optional, default is the first animation)
--  rotation: The sprite rotation (default is 0)
--  scale: The uniform scale on both x and y axis (default is 1)
--  opacity: The opacity value, in [0 ; 1] (default is 1)
--  visible: If false, the object will not appear on screen (default is true)
--  toBack: If true, creates the object on back of the group (behind other objects already in this group)
--  color: The sprite tint, as an array of color components (r, g, b, a (optional)) (default is 255, 255, 255, 255)
--  timescale: The time scale of the sprite, 0.5 plays twice as slow while 2.0 plays twice as fast (default is 1)
--  mask: The mask to apply to the sprite
--   image: The mask image
--   position: The mask position (default is vec2(0, 0))
--   xScale: The mask x scale (default is 1)
--   yScale: The mask y scale (default is 1)
--   rotation: The mask rotation (default is 0)
--   opacity: The opacity value, in [0 ; 1] (default is 1)
--  autoHide: If true, hide the sprite when the animation ends (default is false)
--  autoDestroy: If true, destroys the sprite when the animation ends (default is false)
--  hitTestable: If true, forces the display object to be hitTestable (cf Corona documentation) (default is false)
function Class.create(options)
	return utils.extend(Class):super(options)
end

function Class:super(options)
	-- Initialize attributes
	self.spriteSet = options.spriteSet
	self.autoHide = options.autoHide or false
	self.autoDestroy = options.autoDestroy or false
	self.loopCount = 0
	self.timescale = options.timescale or 1.0
	self.attachments = {}
	self.attachmentsCount = 0

	-- Create group
	self.group = display.newGroup()
	if options.group then
		options.group:insert(self.group)
	end

	-- Create groups for attachments
	self.backAttachmentsGroup = display.newGroup()
	self.attachmentsGroup = display.newGroup()

	-- Insert back group before the sprite so it's below it
	self.group:insert(self.backAttachmentsGroup)

	-- Create Corona sprite
	local spriteSetDefinition = sets[options.spriteSet]
	if not spriteSetDefinition then
		local found = false
		for sheetName, sheetData in pairs(animationData) do
			for setId, sprites in pairs(sheetData) do
				if not found and options.spriteSet == setId then
					Class.loadSheet(sheetName)
					found = true
					break
				end
			end
		end

		spriteSetDefinition = sets[options.spriteSet]
		if not spriteSetDefinition then
			error("Undefined spriteSet: "..options.spriteSet)
		end
	end
	
	self.spriteSet = options.spriteSet
	self.animationOptions = spriteSetDefinition.animationOptions
	self._displayObject = display.newSprite(spriteSetDefinition.imageSheet, spriteSetDefinition.animations)

	-- Prepare sprite
	self._displayObject.timeScale = timeScale

	Super.super(self, options)

	-- Apply mask
	if options.mask then
		self:setMask(options.mask)
	end

	-- Create factor vector to position attachments relative to the sprite reference point
	self.attachmentPositionFactor = self.anchor or vec2(0.5, 0.5)

	-- Attachments group is inserted after the sprite to make it on front (or reposition it to front directly)
	self.group:insert(self.attachmentsGroup)

	if options.toBack then
		self.group:toBack()
	end

	-- Auto play animation
	if options.animation then
		self:play(options.animation)
	end

	-- Listen to events
	Runtime:addEventListener("spritePause", self)
	Runtime:addEventListener("spriteChangeSpeed", self)
	self._displayObject:addEventListener("sprite", self)

	return self
end

-- Destroy the sprite
function Class:destroy()
	self._displayObject:removeEventListener("sprite", self)
	Runtime:removeEventListener("spritePause", self)
	Runtime:removeEventListener("spriteChangeSpeed", self)

	for _, attachment in pairs(self.attachments) do
		attachment:destroy()
	end

	Super.destroy(self)

	self._displayObject:removeSelf()
	self.attachmentsGroup:removeSelf()
	self.backAttachmentsGroup:removeSelf()
	self.group:removeSelf()

	utils.deleteObject(self)
end

-----------------------------------------------------------------------------------------
-- Methods
-----------------------------------------------------------------------------------------

-- Play the given animation
--
-- Parameters:
--  animationName: The animation to play
--  frame: The frame to start the animation on (default is 1)
--  autoDestroy: If true, destroys the sprite when the animation ends (default is false)
function Class:play(animationName, options)
	options = options or {}
	self.animation = animationName
	self:resume()

	if animationName then
		local currentAnimationOptions = self.animationOptions[animationName]

		if not currentAnimationOptions then
			utils.softError("Undefined sprite animation: "..animationName.." of "..self.spriteSet)
		else
			self.currentAnimationOptions = currentAnimationOptions

			-- Pick random variation
			if self.currentAnimationOptions.variations and #self.currentAnimationOptions.variations > 0 then
				local variations = self.currentAnimationOptions.variations
				local suffix = variations[random(#variations)].suffix
				animationName = animationName..suffix
			end

			-- Prepare sequence (sprite set animation)
			self._displayObject:setSequence(animationName)

			-- Start at a certain frame
			if options.frame then
				self:setFrame(options.frame)
			elseif self.currentAnimationOptions.randomStartFrame then
				self:setFrame(random(self._displayObject.numFrames))
			end
		end
	end

	self._displayObject:play()
	self.isAnimating = true
	self.loopCount = 0

	-- Save attributes
	self.width = self._displayObject.width
	self.height = self._displayObject.height
	self.autoDestroy = options.autoDestroy or self.autoDestroy
	self.autoHideOnce = options.autoHide

	-- Reposition attachments
	self:repositionAttachments()
end

-- Pause the sprite animation
function Class:pause()
	if self._displayObject.pause then
		self.isAnimating = self._displayObject.isPlaying
		self._displayObject:pause()
	else
		utils.softError("[Ecusson:Sprite] Impossible to pause sprite "
			..self.spriteSet..(self.animation and ":"..self.animation or "")..
			", display object does not exists")
	end
end

-- Resume the sprite animation
function Class:resume()
	if self.isAnimating and self._displayObject.play then
		self._displayObject:play()
	end
end

-- Set the sprite position
--
-- Parameters:
--  position: The position
function Class:setPosition(position)
	Super.setPosition(self, position)

	if self.attachmentsCount > 0 then
		self:repositionAttachmentsGroup()
	end

	if self.fixedMask then
		self:setMaskPosition(-self.position - self.maskOffsetPosition)
	end
end

-- Set the sprite opacity
--
-- Parameters:
--  opacity: The new opacity
function Class:setOpacity(opacity)
	self.backAttachmentsGroup.opacity = opacity
	self.attachmentsGroup.opacity = opacity

	Super.setOpacity(self, opacity)
end

-- Set the sprite visibility
--
-- Parameters:
--  visible: if true, the object will be visible
function Class:setVisible(visible)
	self.backAttachmentsGroup.isVisible = visible
	self.attachmentsGroup.isVisible = visible

	Super.setVisible(self, visible)
end

-- Apply a mask on the sprite
--
-- Parameters:
--  mask: The mask to apply to the sprite
--   image: The mask image
--   position: The mask position (default is vec2(0, 0))
--   xScale: The mask x scale (default is 1)
--   yScale: The mask y scale (default is 1)
--   rotation: The mask rotation (default is 0)
--   opacity: The opacity value, in [0 ; 1] (default is 1)
function Class:setMask(options)
	self._displayObject:setMask(options.image)

	self._displayObject.maskScaleX = 0.25
	self._displayObject.maskScaleY = 0.25

	self:setMaskPosition(options.position or vec2(0, 0))
	self:setMaskRotation(options.rotation or 0)

	self.fixedMask = options.fixed
	if self.fixedMask then
		self.maskOffsetPosition = (options.position or vec2(0, 0)) - self.position
		self.maskOffsetRotation = (options.rotation or 0) - self.rotation
	end
end

-- Remove the mask from the sprite
function Class:removeMask()
	self._displayObject:setMask(nil)
	self.fixedMask = false
end

-- Move the sprite mask to a given position
--
-- Parameters:
--  position: The position
function Class:setMaskPosition(position)
	self._displayObject.maskX = position.x
	self._displayObject.maskY = position.y
end

-- Rotate the sprite mask
--
-- Parameters:
--  rotation: the rotation
function Class:setMaskRotation(rotation)
	self._displayObject.maskRotation = rotation
end

-- Set the current frame
--
-- parameters:
--  frame: the frame number, in [ 1 ; nbFrames ]
function Class:setFrame(frame)
	self._displayObject:setFrame(frame)
end

-- Return the current frame number
--
-- Returns:
--  The current frame number, in [ 1 ; nbFrames ]
function Class:getFrame()
	return self._displayObject.frame
end

-- Attach an attachment to the sprite
--
-- Parameters:
--  attachmentName: The attachment to attach
--
-- Returns the attached sprite
function Class:attach(attachmentName)
	if not self.attachments[attachmentName] then
		local attachmentDefinition = attachments[attachmentName]

		if not attachmentDefinition then
			utils.softError("No definition for attachment "..attachmentName)
		else
			local animationDefinition = self.currentAnimationOptions.attachments[attachmentName]
			local attachment = Sprite.create{
				spriteSet = attachmentDefinition.spriteSet,
				group = attachmentDefinition.toBack and self.backAttachmentsGroup or self.attachmentsGroup,
				anchor = attachmentDefinition.anchor
			}

			if animationDefinition and animationDefinition.animation then
				attachment:play(animationDefinition.animation)
			end

			self.attachments[attachmentName] = attachment
			self.attachmentsCount = self.attachmentsCount + 1

			self:repositionAttachmentsGroup()
			self:repositionAttachment(attachmentName)

			return attachment
		end
	else
		return self.attachments[attachmentName]
	end
end

-- Detach an attachment previously attached
--
-- Parameters:
--  attachmentName: The attachment to detach
function Class:detach(attachmentName)
	if self.attachments[attachmentName] then
		self.attachments[attachmentName]:destroy()
		self.attachments[attachmentName] = nil
		self.attachmentsCount = self.attachmentsCount - 1
	end
end

-- Return a specific attachment
--
-- Parameters:
--  attachmentName: The attachment to get
function Class:getAttachment(attachmentName)
	return self.attachments[attachmentName]
end

function Class:repositionAttachmentsGroup()
	if self.attachmentPositionFactor then
		local attachmentsPosition = self.position - vec2(self.width, self.height) * self.attachmentPositionFactor

		self.attachmentsGroup.x = attachmentsPosition.x
		self.attachmentsGroup.y = attachmentsPosition.y

		self.backAttachmentsGroup.x = attachmentsPosition.x
		self.backAttachmentsGroup.y = attachmentsPosition.y
	end
end

-- Reposition all attachments depending on sprite animation
function Class:repositionAttachments()
	if self.attachmentsCount > 0 then
		for attachmentName, attachment in pairs(self.attachments) do
			self:repositionAttachment(attachmentName)
		end
	end
end

-- Reposition a specific attachment depending on sprite animation
--
-- Parameters:
--  attachmentName: The attachment to reposition
function Class:repositionAttachment(attachmentName)
	local attachment = self.attachments[attachmentName]
	local attachmentOptions = self.currentAnimationOptions.attachments[attachmentName]

	if attachmentOptions then
		local frame = self:getFrame()
		
		attachment:setVisible(true)

		-- For the case we added a false second frame to create a steady timed animation
		local position = attachmentOptions.positions[frame]
		if not position then
			position = attachmentOptions.positions[1]
		end

		attachment:setPosition(position)

		-- Rotate attachment
		local rotation
		if attachmentOptions.rotations then
			rotation = attachmentOptions.rotations[frame]
			if not rotation then
				rotation = attachmentOptions.rotations[1]
			end

			attachment:setRotation(rotation)
		end
	else
		attachment:hide()
	end
end

function Class:setRotation(rotation)
	Super.setRotation(self, rotation)

	self:repositionAttachments()
end

-- Set the color of the display object
--
-- Parameters:
--  color: The four components of a color, in this order: R, G, B, Alpha (optional)
function Class:setColor(color)
	Super.setColor(self, color)

	for attachmentName, attachment in pairs(self.attachments) do
		attachment:setColor(self.color)
	end
end

function Class:setTimeScale(timescale)
	self._displayObject.timeScale = timescale
end

-----------------------------------------------------------------------------------------
-- Event listeners
-----------------------------------------------------------------------------------------

-- Native sprite event handler
--
-- Parameters:
--  event: The event thrown
function Class:sprite(event)
	local currentFrame = self._displayObject.frame
	local loopDetected = false

	self.width = self._displayObject.width
	self.height = self._displayObject.height

	local ecussonPhase
	if event.phase == "next" then
		if currentFrame <= self.lastFrame then
			-- Detect loops not sent by Corona
			loopDetected = true
		else
			-- Dispatch next event
			ecussonPhase = "next"
		end
	elseif event.phase == "began" then
		-- Dispatch began event
		ecussonPhase = "began"
	end

	if event.phase == "loop" or loopDetected then
		-- Count loops and throw either a loop event, if the animation is not finished, or an ended event otherwise
		self.loopCount = self.loopCount + 1

		if self.currentAnimationOptions.loopCount == 0 or self.loopCount < self.currentAnimationOptions.loopCount then
			ecussonPhase = "loop"
		else
			-- Auto-destroy the sprite if ordered so
			if self.autoDestroy then
				self:destroy()
			elseif self.autoHide or self.autoHideOnce then
				self:hide()
				self.autoHideOnce = false
			else
				ecussonPhase = "ended"
				self:setFrame(self.lastFrame)
				self:pause()
			end
		end
	end

	self.lastFrame = currentFrame

	-- Dispatch event
	if ecussonPhase then
		self._displayObject:dispatchEvent{
			name = "ecussonSprite",
			phase = ecussonPhase,
			sprite = self,
			frame = self._displayObject.frame
		}

		-- Move attachments according to sprite animation
		if ecussonPhase == "next" or ecussonPhase == "loop" then
			self:repositionAttachments()
		end
	end
end

-- Pause the sprite animation
--
-- Parameters:
--  event: The tile event, with these values:
--   status: If true, then pauses the animation, otherwise resumes it
function Class:spritePause(event)
	if event.status then
		self:pause()
	else
		self:resume()
	end
end

-- Change the sprite animation speed
--
-- Parameters:
--  event: The event, with these values:
--   timeScale: The new time scale
function Class:spriteChangeSpeed(event)
	self._displayObject.timeScale = self.timescale * event.timeScale
end

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

return Class
