'OHRRPGCE - Animation system
'(C) Copyright 1997-2025 James Paige, Ralph Versteegen, and the OHRRPGCE Developers
'Dual licensed under the GNU GPL v2+ and MIT Licenses. Read LICENSE.txt for terms and disclaimer of liability.

#include "config.bi"
#include "util.bi"
#include "const.bi"
#include "uiconst.bi"
#include "common.bi"
#include "allmodex.bi"
#include "reload.bi"
#include "slices.bi"
#include "loading.bi"
#include "sliceedit.bi"
#include "slice_propnames.bi"

using Reload


dim shared ms_per_frame as integer = 55

' A single global document for all Animation.opsnode RELOAD nodes (they are orphaned).
' Access via get_anim_doc().
dim shared _anim_doc as DocPtr

' Map from property names (interned strings: zstring ptr, does not own a copy) to
' SlicePropertyNumber index.
dim shared slice_property_numbers as HashTable

' Number of loops/non-forwards branches that can occur in an animation without a
' wait before it's considered to be stuck in an infinite loop.
const ANIMATION_LOOPLIMIT = 10

' Short names used for listing an animation
redim anim_op_names(animOpLAST) as string
anim_op_names(animOpWait) =      "wait"
anim_op_names(animOpWaitMS) =    "wait"
anim_op_names(animOpFrame) =     "frame"
anim_op_names(animOpRepeat) =    "repeat"
anim_op_names(animOpSetOffset) = "set offset"
anim_op_names(animOpRelOffset) = "add offset"
anim_op_names(animOpPlayFrameGroup) = "frame group"
anim_op_names(animOpSetProp) =   "set"
anim_op_names(animOpSwitchAnim) ="switch to"
anim_op_names(animOpTween) =     "tween"

 ' Short names used for RELOAD serialisation
redim anim_op_node_names(animOpLAST) as string
anim_op_node_names(animOpWait) =      "wait"
anim_op_node_names(animOpWaitMS) =    "waitms"
anim_op_node_names(animOpFrame) =     "frame"
anim_op_node_names(animOpRepeat) =    "repeat"
anim_op_node_names(animOpSetOffset) = "setoffset"
anim_op_node_names(animOpRelOffset) = "addoffset"
anim_op_node_names(animOpPlayFrameGroup) = "framegroup"
anim_op_node_names(animOpSetProp) =   "setprop"
anim_op_node_names(animOpSwitchAnim) ="switchanim"
anim_op_node_names(animOpTween) =     "tween"

' Descriptive captions
redim anim_op_fullnames(animOpLAST) as string
anim_op_fullnames(animOpWait) =      "Wait (num frames)"
anim_op_fullnames(animOpWaitMS) =    "Wait (seconds)"
anim_op_fullnames(animOpFrame) =     "Set frame"
anim_op_fullnames(animOpRepeat) =    "Repeat animation"
anim_op_fullnames(animOpSetOffset) = "Move to offset (unimp)"
anim_op_fullnames(animOpRelOffset) = "Add to offset (unimp)"
anim_op_fullnames(animOpPlayFrameGroup) = "Play frame group"
anim_op_fullnames(animOpSetProp) =   "Set"
anim_op_fullnames(animOpSwitchAnim) ="Switch to animation"
anim_op_fullnames(animOpTween) =     "Tween (blend keyframes)"

' Built-in variants have names starting with :, which are not allowed for user names
dim shared builtin_variants_table(4) as IntStrPair
'  => { _
'   (varUp,       "up"), _
'   (varRight,    "right"), _
'   (varDown,     "down"), _
'   (varLeft,     "left"), _
'   (varHurt,     "hurt") _
' }
'   '(varSelected, "selected"), _
'   '(varHover,    "hover"), _
'   '(varDisabled, "disabled"), _

dim builtin_animations(...) as AnimVariantInfo => { _
	(@"idle",        acAny,        @"Default when no other animation running, e.g. standing still"), _
	(@"walk",        acActor,      @"Normal/default movement"), _
	(@"retreat",     acBattler,    @"Walking back to start after attacking"), _
	(@"flee",        acBattler,    @"Fleeing from battle"), _
	(@"victory",     acBatHero,    @"Victory dance after a battle"), _
	(@"walkinplace", acWalkabout,  @"NPCs with Walk-in-Place movetype"), _
	(@"talk",        acWalkabout,  @"For heroes and NPCs, when an NPC shows a textbox"), _
	(@"attack",      acBattler,    @"Default attack animation"), _
	(@"cast",        acBattler,    @"`Cast' attack animation"), _
	(@"useitem",     acBatHero,    @"Doing an attack linked to an item"), _
	/'(@"jump",        acBattler,   @"`Jump' attack animation"), '/ _
	/'(@"land",        acBattler,   @"`Land' attack animation"), '/ _
	(@"hurt",        acBattler,    @"When hit by an attack"), _
	/'(@"healed",      acBattler,   @"When healed in a battle"), '/ _
	/' die disabled; need to figure out how to handle or supersede enemy dissolve '/ _
	/'(@"die",         acBattler,    @"Used instead of `hurt' when dying"), (happens before enemy dissolve) '/ _
	(@"dead",        acBatHero,    @"Heroes with 0 HP"), _
	/'(@"revive",      acBatHero,   @"When HP increases above 0") '/ _
	(@"selected",    acAny,        @"Selected in a menu"), _
	(NULL, 0, NULL) _
}

dim builtin_anim_variants(...) as AnimVariantInfo => { _
	(@"(none)",      acAny,        @"Used if there's no more specific animation"), _
	(@"up",          acActor,      @"Facing direction"), _
	(@"right",       acActor,      @"Facing direction"), _
	(@"down",        acActor,      @"Facing direction"), _
	(@"left",        acActor,      @"Facing direction"), _
	(@"weak",        acBattler,    @"HP less than the Weak threshold (see Battle System Options)"), _
	/'(@"missed",      acBattler,   @"Performing or hit by an attack that missed"), '/ _
	/'(@"resisted",    acBattler,   @"Performing or hit by an attack that failed"), '/ _
	(NULL, 0, NULL) _
}


function get_anim_doc() as DocPtr
	if _anim_doc = NULL then _anim_doc = CreateDocument()
	return _anim_doc
end function

' Creates the default global animations for a sprite type in an existing AnimationSet.
' TODO: very unfinished. These could be defined in a RELOAD data file instead of by code.
sub spriteset_default_global_animations(byref animset as AnimationSet, sprtype as SpriteType)
	debuginfo "Using default global animations for " & sprite_sizes(sprtype).name

	dim n as NodePtr
	#define op(opname) n = .append(opname)
	#define arg(argname, argvalue) AppendChildNode(n, argname, argvalue)

	animset.delete_all_animations()
	if sprtype = sprTypeHero then
		with *animset.new_animation("idle")
			op(animOpFrame)
			arg("frame", frameSTAND)    '0
		end with
		with *animset.new_animation("walk")
			op(animOpPlayFrameGroup)
			arg("framegroup", 0)  '(frameSTAND + frameSTEP)
			arg("ms", 110)
			op(animOpRepeat)
		end with
		with *animset.new_animation("attack")
			op(animOpPlayFrameGroup)
			op(animOpPlayFrameGroup)
			arg("framegroup", 1)  '(frameATTACKA + frameATTACKB)
			arg("ms", 110)
			op(animOpFrame)
			arg("frame", frameSTAND)    '0
		end with
	end if
	if sprtype = sprTypeWalkabout then
		with *animset.new_animation("walk", "up")
			op(animOpPlayFrameGroup)
			arg("framegroup", 0)
			arg("ms", 110)
			op(animOpRepeat)
		end with
		with *animset.new_animation("walk", "right")
			op(animOpPlayFrameGroup)
			arg("framegroup", 1)
			arg("ms", 110)
			op(animOpRepeat)
		end with
		with *animset.new_animation("walk", "down")
			op(animOpPlayFrameGroup)
			arg("framegroup", 2)
			arg("ms", 110)
			op(animOpRepeat)
		end with
		with *animset.new_animation("walk", "left")
			op(animOpPlayFrameGroup)
			arg("framegroup", 3)
			arg("ms", 110)
			op(animOpRepeat)
		end with
		with *animset.new_animation("selected")
			op(animOpSwitchAnim)
			arg("anim", "walk")
			arg("variant", "down")
		end with
	end if
end sub

sub set_animation_framerate(ms as integer)
	' We bound to 5-200 because set_speedcontrol does the same thing
	ms_per_frame = bound(ms, 5, 200)
end sub

function get_animation_framerate() as integer
	return ms_per_frame
end function

function ms_to_frames(ms as integer) as integer
	if ms <= 0 then return 0
	return large(1, CINT(ms / ms_per_frame))  'Round to nearest
end function

function frames_to_ms(frames as integer) as integer
	return frames * ms_per_frame
end function


'==========================================================================================
'                                    Slice properties
'==========================================================================================

declare sub set_slice_blend_property(sl as Slice ptr, propnum as SlicePropertyNumber, drawopts as DrawOptions, datnode as NodePtr)

local sub setup_slice_property_numbers()
	slice_property_numbers.construct(31)
	slice_property_numbers.key_is_opaque_ptr = YES

	for idx as integer = 0 to ubound(slice_property_names)
		dim prop as zstring ptr = intern_string(slice_property_names(idx))

		slice_property_numbers.set(prop, idx)
	next
end sub

' Sets a property (such as "w") or pseudo-property (such as "screen_x") of a slice to
' value(s) taken from datnode.
' Rather than just raw writes to slice data, this should aim to provide a higher-level
' interface comparable to script commands such as "set opacity" or "move slice to" which
' modify multiple data at once.
' There can be multiple properties for the same thing (e.g. "frame" and "frameid").
' datnode could contain more than one value, but there's no examples of that yet.
' If a property name isn't matched, we do nothing, and don't detect that case.
sub set_slice_property(sl as Slice ptr, prop as zstring ptr, datnode as NodePtr)
	if slice_property_numbers.constructed = NO then
		setup_slice_property_numbers
	end if
	dim propnum as SlicePropertyNumber
	propnum = slice_property_numbers.get_int(intern_string(prop), _INVALID_PROP)

	dim intval as integer = datnode..int
	dim boolval as bool = intval <> 0  ' == datnode..bool

	' When possible, if there are slice types with comparable properties,
	' they should accept the same property names.

	' Animation properties are the same as the node names used in .slices with the following exceptions:
	' Ellipse: use "col" and "bgcol" instead of "bordercol" and "fillcol", to match Rect, Line and Text slices
	' Rectangle: use "col" and "bgcol" instead of "fg" and "bg", ditto
	' Rectangle: use "translucent" instead of "trans" to not match Sprite "trans" transparency setting
	' Layout: use "padding" instead of "padding0" to match Panel "padding"

	if propnum < _LAST_GENERIC_PROP then
		' Properties applicable to all slice types

		select case as const propnum
			case _x
				sl->X = intval
			case _y
				sl->Y = intval
			case _screen_x
				' TODO: It would be nice if you could instead set the screen position
				' without changing X/Y, like Fill Parent does
				RefreshSliceScreenPos sl
				sl->X += intval - (sl->ScreenX + SliceXAnchor(sl))
			case _screen_y
				RefreshSliceScreenPos sl
				sl->Y += intval - (sl->ScreenY + SliceYAnchor(sl))
			case _w
				' "set slice width" allows negative width, so we will too.
				' Text slices are resizable horizontally if and only if they wrap
				if SlicePossiblyResizable(sl) then
					sl->Width = intval
					' Unlike "set slice width", this succeeds on slices set
					' to Fill Parent or Cover Children by turning those off.
					slice_edit_updates sl, @sl->Width
				end if
			case _h
				' Text slices are never resizable vertically. (This is unlike "set slice height",
				' which momentarily allows, before it's reset.)
				if SlicePossiblyResizable(sl) andalso sl->SliceType <> slText then
					sl->Height = intval
					slice_edit_updates sl, @sl->Height
				end if
			case _cover
				if in_bound(intval, 0, coverLAST) then
					sl->CoverChildren = intval
					slice_edit_updates sl, @sl->CoverChildren
				end if
			case _fill
				sl->Fill = boolval
				slice_edit_updates sl, @sl->Fill
			case _fillmode
				if in_bound(intval, 0, sliceFillLAST) then
					sl->FillMode = intval
					slice_edit_updates sl, @sl->FillMode
				end if
			case _vis
				sl->Visible = boolval
			case _paused
				' Doesn't make sense for an animation to pause itself, but
				' set_slice_property might get called elsewhere.
				sl->Paused = boolval
			case _clip
				sl->Clip = boolval
			case _alignh
				if in_bound(intval, alignLeft, alignRight) then
					sl->AlignHoriz = intval
				end if
			case _alignv
				if in_bound(intval, alignLeft, alignRight) then
					sl->AlignVert = intval
				end if
			case _anchorh
				if in_bound(intval, alignLeft, alignRight) then
					sl->AnchorHoriz = intval
				end if
			case _anchorv
				if in_bound(intval, alignLeft, alignRight) then
					sl->AnchorVert = intval
				end if
			case _clamph
				if in_bound(intval, alignLeft, alignBoth) then
					sl->ClampHoriz = intval
				end if
			case _clampv
				if in_bound(intval, alignLeft, alignBoth) then
					sl->ClampVert = intval
				end if
			case _clamptoscreen
				sl->ClampToScreen = boolval
			case _padt
				sl->PaddingTop = intval
			case _padr
				sl->PaddingRight = intval
			case _padb
				sl->PaddingBottom = intval
			case _padl
				sl->PaddingLeft = intval
			case _autosort
				if in_bound(intval, 0, slAutoSortLAST) then
					sl->AutoSort = intval
				end if
			case _sort
				sl->Sorter = intval
			case _lookup
				' TODO: allow other lookups in some cases
				if sl->Lookup >= 0 andalso intval >= 0 then
					sl->Lookup = intval
				end if
			case _vtickx
				sl->VelTicks.X = bound(intval, -1, 999999)
				sl->TargTicks = 0
			case _vx
				sl->Velocity.X = bound(intval, -999999, 999999)
				if sl->VelTicks.X = 0 then
					sl->VelTicks.X = -1
					sl->TargTicks = 0
				end if
			case _vticky
				sl->VelTicks.Y = bound(intval, -1, 999999)
				sl->TargTicks = 0
			case _vy
				sl->Velocity.Y = bound(intval, -999999, 999999)
				if sl->VelTicks.Y = 0 then
					sl->VelTicks.Y = -1
					sl->TargTicks = 0
				end if
			case _ttick
				sl->TargTicks = bound(intval, 0, 999999)
				sl->VelTicks = 0
			case _tx
				sl->Targ.X = intval
				sl->VelTicks = 0
			case _ty
				sl->Targ.Y = intval
				sl->VelTicks = 0
		end select

	else
		' Properties which apply to only some types (usually only one)

		select case sl->SliceType

			case slMap
				dim dat as MapSliceData ptr = sl->MapData
				set_slice_blend_property(sl, propnum, dat->drawopts, datnode)

			case slSprite
				dim dat as SpriteSliceData ptr = sl->SpriteData

				' Various sprite data that's not editable by users is missing

				select case as const propnum
					case _frame
						dat->set_frame(sl, intval)
					case _frameid
						dat->set_frameid(sl, intval)
					case _sprtype
						if in_bound(intval, 0, sprTypeLastPickable) then
							ChangeSpriteSlice sl, intval
						end if
					case _rec
						'if in_bound(intval, 0, sprite_sizes(dat->spritetype).lastrec) then
						' Handles bounds checking
						ChangeSpriteSlice sl, , intval
					case _pal
						if in_bound(intval, -1, gen(genMaxPal)) then
							ChangeSpriteSlice sl, , , intval
						end if
					case _trans
						dat->trans = boolval
					case _fliph
						dat->flipHoriz = boolval
					case _flipv
						dat->flipVert = boolval
					case _dissolving
						dat->dissolving = boolval
					case _d_type
						if in_bound(intval, 0, dissolveTypeMax) then
							dat->d_type = intval
							dat->dissolving = YES
						end if
					case _d_time
						if in_bound(intval, -1, 999999) then
							dat->d_time = intval
						end if
					case _d_tick
						if in_bound(intval, 0, 999999) then
							dat->d_tick = intval
							dat->dissolving = YES
						end if
					case _d_back
						dat->d_back = boolval
					case else
						set_slice_blend_property(sl, propnum, dat->drawopts, datnode)
				end select

			case slRectangle
				dim dat as RectangleSliceData ptr = sl->RectData
				select case as const propnum
					case _style
						if in_bound(intval, -1, 14) then
							ChangeRectangleSlice sl, intval
						end if
					case _bgcol  'bg
						if in_bound(intval, LowColorCode(), 255) then
							ChangeRectangleSlice sl, , intval
						end if
					case _col  'fg
						if in_bound(intval, LowColorCode(), 255) then
							ChangeRectangleSlice sl, , , intval
						end if
					case _border
						if in_bound(intval, -2, 14) then
							ChangeRectangleSlice sl, , , , intval
						end if
					case _raw_box_border
						if in_bound(intval, 0, gen(genMaxBoxBorder)) then
							ChangeRectangleSlice sl, , , , , , , intval
						end if
					case _translucent
						if in_bound(intval, 0, transLAST) then
							dat->translucent = intval
						end if
					case _fuzzfactor
						if in_bound(intval, 0, 99) then
							dat->fuzzfactor = intval
						end if
					case _fz_zoom
						dat->fuzz_zoom = bound(intval, 1, 10000)
					case _fz_stationary
						dat->fuzz_stationary = boolval
				end select

			case slLine
				dim dat as LineSliceData ptr = sl->LineData
				select case propnum
					case _col
						if in_bound(intval, LowColorCode(), 255) then
							dat->col = intval
						end if
				end select

			case slText
				dim dat as TextSliceData Ptr = sl->TextData
				select case propnum
					case _s
						ChangeTextSlice sl, datnode..str
					case _col
						if in_bound(intval, LowColorCode(), 255) then
							dat->col = intval
						end if
					case _bgcol
						if in_bound(intval, LowColorCode(), 255) then
							dat->bgcol = intval
						end if
					case _outline
						'ChangeTextSlice sl, , , boolval
						dat->outline = boolval
					case _wrap
						'ChangeTextSlice sl, , , , boolval
						dat->wrap = boolval
					case _row_align
						'ChangeTextSlice sl, , , , , , boolval
						if in_bound(intval, alignLeft, alignRight) then
							dat->row_alignment = intval
						end if
				end select

			case slGrid
				dim dat as GridSliceData Ptr = sl->GridData
				select case propnum
					case _rows
						if in_bound(intval, 0, 99) then
							dat->rows = intval
						end if
					case _cols
						if in_bound(intval, 0, 99) then
							dat->cols = intval
						end if
					case _show
						dat->show = boolval
				end select

			case slEllipse
				dim dat as EllipseSliceData Ptr = sl->EllipseData
				select case propnum
					case _col  'bordercol
						if in_bound(intval, LowColorCode(), 255) then
							dat->bordercol = intval
						end if
					case _bgcol  'fillcol
						if in_bound(intval, LowColorCode(), 255) then
							dat->fillcol = intval
						end if
				end select

			case slScroll
				dim dat as ScrollSliceData Ptr = sl->ScrollData
				select case propnum
					case _style
						' Note that Rectangle style allows -1 for "None"
						if in_bound(intval, 0, 14) then
							dat->style = intval
						end if
					case _check_depth
						dat->check_depth = large(0, intval)
				end select

			case slSelect
				dim dat as SelectSliceData Ptr = sl->SelectData
				select case propnum
					case _index
						dat->index = large(0, intval)
				end select

			case slPanel
				dim dat as PanelSliceData Ptr = sl->PanelData
				select case propnum
					case _vertical
						dat->vertical = boolval
					case _primary
						if in_bound(intval, 0, 1) then
							dat->primary = intval
						end if
					case _percent
						if in_bound(datnode..float, 0.0, 1.0) then
							dat->percent = intval
						end if
					case _pixels
						dat->pixels = large(intval, 0)
					case _padding
						dat->padding = large(intval, 0)
				end select

			case slLayout
				dim dat as LayoutSliceData Ptr = sl->LayoutData
				select case propnum
					case _dir0
						if in_bound(intval, 0, 3) then
							dat->primary_dir = intval
						end if
					case _dir1
						if in_bound(intval, 0, 3) then
							dat->secondary_dir = intval
						end if
					case _justified
						dat->justified = boolval
					case _last_row_justified
						dat->last_row_justified = boolval
					case _skip_hidden
						dat->skip_hidden = boolval
					case _row_align
						if in_bound(intval, alignLeft, alignRight) then
							dat->row_alignment = intval
						end if
					case _cell_align
						if in_bound(intval, alignLeft, alignRight) then
							dat->cell_alignment = intval
						end if
					case _padding  'padding0
						dat->primary_padding = intval
					case _padding1
						dat->secondary_padding = intval
					case _min_breadth
						dat->min_row_breadth = large(0, intval)
				end select
		end select
	end if
end sub

sub set_slice_blend_property(sl as Slice ptr, propnum as SlicePropertyNumber, drawopts as DrawOptions, datnode as NodePtr)
	dim intval as integer = datnode..int

	select case propnum
		case _blending
			drawopts.with_blending = datnode..bool
		case _opacity
			drawopts.opacity = bound(datnode..float, 0.0, 1.0)
			if drawopts.opacity < 1.0 then drawopts.with_blending = YES
		case _blend_mode
			if in_bound(intval, 0, blendModeLAST) then
				drawopts.blend_mode = intval
				drawopts.with_blending = YES
			end if
		case _mod_r
			drawopts.argbModifier.R = bound(intval, 0, 255)
			drawopts.with_blending = YES
		case _mod_g
			drawopts.argbModifier.G = bound(intval, 0, 255)
			drawopts.with_blending = YES
		case _mod_b
			drawopts.argbModifier.B = bound(intval, 0, 255)
			drawopts.with_blending = YES
	end select
end sub

' Set a slice property as the interpolation of two value nodes.
' x is a value from 0.0 (to use value0) to 1.0 (to use value1)
sub interpolate_slice_property(sl as Slice ptr, prop as zstring ptr, x as double, value0 as NodePtr, value1 as NodePtr)
	dim as NodePtr intervalue = CreateNode(value0, "value")
	select case NodeType(value0)
		case rltInt
			dim vals(1) as integer
			vals(0) = value0..int
			vals(1) = value1..int
			SetContent intervalue, vals(0) + x * (vals(1) - vals(0))
		case rltFloat
			dim vals(1) as double
			vals(0) = value0..float
			vals(1) = value1..float
			SetContent intervalue, vals(0) + x * (vals(1) - vals(0))
		case rltString, rltInternString
			' Cute text interpolation: first erase the text character by characer
			' down to the common prefix, then add the value1 suffix.
			dim vals(1) as string
			vals(0) = value0..str
			vals(1) = value1..str
			dim matchlen as integer = length_matching(vals(0), vals(1))
			dim edit_dist as integer = len(vals(0)) + len(vals(1)) - 2 * matchlen + 1
			dim curchar as integer = edit_dist * x
			dim prefix0len as integer = len(vals(0)) - curchar
			' TODO: will need reimplementation to support text markup
			if prefix0len >= matchlen then
				SetContent intervalue, left(vals(0), prefix0len)
			else
				SetContent intervalue, left(vals(1), 2 * matchlen - prefix0len)
			end if
	end select
	set_slice_property sl, prop, intervalue
	FreeNode intervalue
end sub

'==========================================================================================
'                                         Animation
'==========================================================================================


' constructor Animation()
' 	reference()
' 	opsnode = CreateNode(get_anim_doc)
' end constructor

constructor Animation(name as string, variant as string = "")
	this.name = name
	this.variant = variant
	reference()
	this.opsnode = CreateNode(get_anim_doc, "ops")
end constructor

destructor Animation()
	'? " Animation.destructor(" & hex(@this) & " " & name & " " & variant & ")"
	FreeNode(this.opsnode)
end destructor

function Animation.reference() as Animation ptr
	refcount += 1
	'? "Animation.reference(" & hex(@this) & " " & name & " " & variant & "): refc=" & refcount
	return @this
end function

sub Animation.dereference()
	refcount -= 1
	'? "Animation.dereference(" & hex(@this) & " " & name & " " & variant & "): refc=" & refcount
	BUG_IF(refcount < 0, "Too many Animation.dereference()")
	if refcount = 0 then
		delete @this
	end if
end sub

' Deep copy
function Animation.duplicate() as Animation ptr
	'? "Animation.duplicate(" & hex(@this) & " " & name & " " & variant & "):"
	dim ret as Animation ptr = new Animation(name, variant)
	ret->refcount = 1
	FreeNode ret->opsnode
	ret->opsnode = CloneNodeTree(opsnode, get_anim_doc)
	return ret
end function

sub Animation.replace_ops(copy_from as NodePtr)
	FreeNode(opsnode)
	opsnode = CloneNodeTree(copy_from, get_anim_doc)
end sub

function Animation.append(optype as AnimOpType) as NodePtr
	BUG_IF(opsnode = 0, "opsnode null", 0)
	return AppendChildNode(opsnode, anim_op_node_names(optype), optype)
end function

' Changes the type (operator) of an op while leaving its children unmodified, which
' is useful if the new type takes the same args.
sub Animation.mutate_op(op as NodePtr, optype as AnimOpType)
	SetContent(op, optype)
	RenameNode(op, anim_op_node_names(optype))
end sub


'==========================================================================================
'                                      AnimationSet
'==========================================================================================


' Create deep copies of animations; shared_set/fallback_set are shared.
' The result has refcount 1 (comparable to reference()).
' When this is called on a SpriteSet you get an AnimationSet. You can't duplicate a SpriteSet
' (well, it could be implemented with frame_duplicate)
function AnimationSet.duplicate() as AnimationSet ptr
	dim ret as AnimationSet ptr = new AnimationSet
	ret->refcount = 1
	ret->slice_specific = slice_specific
	ret->name = name
	if shared_set then
		ret->shared_set = shared_set->reference()
	end if
	if fallback_set then
		ret->fallback_set = fallback_set->reference()
	end if
	v_new ret->animations
	for idx as integer = 0 to v_len(animations) - 1
		v_append ret->animations, animations[idx]->duplicate()
	next
	return ret
end function

destructor AnimationSet()
	'If deleting an AnimationSet that's a SpriteSet, noone should still be playing its animations!
	'(The Animations can remain referenced, but the Frames might be gone)
	delete_all_animations(YES)  'check_no_references = YES

	unload_shared_animsets
end destructor

sub AnimationSet.unload_shared_animsets()
	animset_unload @shared_set
	animset_unload @fallback_set
end sub

' Increment refcount.
function AnimationSet.reference() as AnimationSet ptr
	refcount += 1
	DEBUG_ANIM_CACHE(? "AnimationSet.reference(" & hex(@this) & " " & name & "): refc=" & refcount)
	return @this
end function

' Recommended to call the animset_unload() wrapper instead
sub AnimationSet.dereference()
	refcount -= 1
	DEBUG_ANIM_CACHE(? "AnimationSet.dereference(" & hex(@this) & " " & name & "): refc=" & refcount)
	BUG_IF(refcount < 0, "Negative refc")
	if refcount = 0 then
		delete @this
	end if
end sub

' Decrement refcount and delete on zero
sub animset_unload(pp as AnimationSet ptr ptr)
	if *pp then
		(*pp)->dereference()
		*pp = NULL
	end if
end sub

sub AnimationSet.delete_all_animations(check_no_references as bool = NO)
	for idx as integer = 0 to v_len(animations) - 1
		if check_no_references then
			BUG_IF(animations[idx]->refcount <> 1, "Leaked reference to animation")
		end if
		animations[idx]->dereference()
		animations[idx] = 0
	next
	v_free animations
end sub

' animvariant can contain a trailing space
sub split_animvariant(animvariant as string, byref animname as string, byref variant as string)
	dim spacepos as integer = instr(animvariant, " ")
	if spacepos then
		animname = left(animvariant, spacepos - 1)
		variant = mid(animvariant, spacepos + 1)
	else
		animname = animvariant
		variant = ""
	end if
end sub

' Searches for an animation with a certain name, or NULL if there's no match.
' If exact=YES, the variant must match exactly, otherwise looks for best match.
' If recurse=YES, go through the chain of shared_sets and then the chain of
'   fallback_sets as if they were all concatenated.  But we don't recurse from a
'   shared_set to its fallback_set or vice versa. This is because fallback_set
'   is intended for spriteset-specific animations and shared_set for looking at
'   some ancestor slice for shared defaults, but we don't want to use defaults
'   from ancestor's spriteset.
' animvariant is either just the name of the animation, or the
' name plus an optional variant separated by a space, e.g. "walk upleft", "walk ", "walk".
' The nearest match is picked amongst animations which match the name:
'  - prefer variant as specified
'  - then prefer an animation with blank variant
'  - then prefer the first animation (with that name)
function AnimationSet.find_animation(animvariant as string, exact as bool = NO, recurse as bool = YES) as Animation ptr
	dim as string animname, variant
	split_animvariant animvariant, animname, variant

	return find_animation_recurse(animname, variant, exact, recurse, recurse, 0)
end function

function AnimationSet.find_animation_recurse(animname as string, variant as string, exact as bool, recurse1 as bool, recurse2 as bool, byref _best_score as integer) as Animation ptr
	dim as Animation ptr best_match, anim, best_fallback
	for idx as integer = 0 to v_len(animations) - 1
		anim = animations[idx]
		if anim->name = animname then
			' Right name, check how good the match of variants is
			dim score as integer
			if anim->variant = variant then
				score = 1000
			elseif exact then
				score = 0
			elseif len(anim->variant) = 0 then
				'Prefer nonvariant animations
				score = 10
			else
				'Otherwise, default to the first variant
				score = 1
			end if
			if _best_score < score then
				best_match = anim
				_best_score = score
			end if
		end if
	next
	if recurse1 andalso shared_set then
		' When called recursively, returns null unless a better match
		' than _best_score was found, which is updated.
		best_fallback = shared_set->find_animation_recurse(animname, variant, exact, YES, NO, _best_score)
		if best_fallback then best_match = best_fallback
	end if
	if recurse2 andalso fallback_set then
		best_fallback = fallback_set->find_animation_recurse(animname, variant, exact, NO, YES, _best_score)
		if best_fallback then best_match = best_fallback
	end if

	return best_match
end function

' Get exactly this animvariant and do not recurse
function AnimationSet.get_animation(animvariant as string) as Animation ptr
	return find_animation(animvariant, YES, NO)
end function

' Append a new blank animation and return pointer
function AnimationSet.new_animation(name as string = "", variant as string = "") as Animation ptr
	dim ret as Animation ptr = new Animation(name, variant)
	if animations = NULL then
		v_new animations
	end if
	v_append animations, ret
	return ret
end function

sub AnimationSet.delete_animation(anim as Animation ptr)
	if anim then
		anim->dereference()
		v_remove animations, anim
	end if
end sub


'==========================================================================================
'                                       AnimationState
'==========================================================================================


constructor AnimationState(rhs as AnimationState)
	memcpy(@this, @rhs, sizeof(this))
	if anim then anim->reference()
end constructor

constructor AnimationState(sl as Slice ptr)
	this.sl = sl
	sl->GetAnimations()  'Ensure nonnull
	' Maybe we need to keep our own reference to sl->Animations in case it's replaced?
end constructor

destructor AnimationState()
	set_anim(NULL)  'Dec refcount
end destructor

' Lookup an animation and start it. See AnimationSet.find_animation() for documentation
' of animvariant (animation name plus optional variant).
' Normally an animation specifies how many times it loops (unimplemented), or ends in Repeat
' to loop forever. loopcount <> 0 overrides this, giving a fixed number of
' times to play, or < 0 to repeat forever
function AnimationState.start_animation(animvariant as string, loopcount as integer = 0) as Animation ptr
	start_animation(sl->Animations->find_animation(animvariant), loopcount)
	return anim
end function

' OK for anim to be NULL
function AnimationState.start_animation(anim as Animation ptr, loopcount as integer = 0) as Animation ptr
	anim_wait = 0
	anim_advanced = YES
	anim_loop = loopcount
	anim_looplimit = ANIMATION_LOOPLIMIT
	set_anim(anim)
	if anim then
		curop = FirstChild(anim->opsnode)
	else
		curop = NULL
	end if
	return anim
end function

' Start an animation if it's not already playing. If it is, change the loopcount (if not -1).
' Stop if the animation doesn't exist.
function AnimationState.switch_animation(animvariant as string, loopcount as integer = -1) as bool
	return switch_animation(sl->Animations->find_animation(animvariant), loopcount)
end function

' OK for anim to be NULL
function AnimationState.switch_animation(to_anim as Animation ptr, loopcount as integer = -1) as bool
	if to_anim <> anim then
		start_animation(to_anim, loopcount)
		return YES
	elseif loopcount > -1 then
		anim_loop = loopcount
	end if
	return NO
end function

' Doesn't reset the sprite.
sub AnimationState.stop_animation()
	set_anim(NULL)
	anim_wait = 0
	curop = NULL
end sub

sub AnimationState.set_anim(newanim as Animation ptr)
	if anim then
		anim->dereference()
	end if
	if newanim then
		newanim->reference()
	end if
	anim = newanim
end sub

' Resets everything that an animation might change, but doesn't stop it
' TODO: requires keeping an undo log... which could be shared with the slice editor!
sub AnimationState.reset()
	/'
	sl->Pos = 0
	if sl->SliceType = slSprite then
		sl->SpriteData->frame = 0
	end if
	'/
end sub

' Advance time until the next wait, skipping the current one, and returns number of ms that the wait was for.
' Returns -1 and does nothing if not waiting, -2 on error.
' The return value ought to be independent of ms_per_frame
' Note: any time already spent on the current wait is ignored.
function AnimationState.skip_wait() as integer
	if anim = NULL orelse curop = NULL then return -2
	' Look at the current op instead of anim_wait, because it might be a wait
	' which we haven't looked at yet.
	declare as NodePtr curop
	select case curop..int
		case animOpWait, animOpWaitMS, animOpPlayFrameGroup
			dim ret as integer = curop."ms"
			anim_wait = ms_to_frames(ret)
			if animate() = NO then ret = -2  ' Until next wait
			return ret
		case animOpTween
			' TODO: don't know yet how to handle this for gifs
			'var lastfr = LastChild(curop, "keyframe")
			'if lastfr = NULL then return -1
		case else
			return -1
	end select
end function

' Calculate and apply the current state of a tween.
' curop is a animOpTween node with "key" and "keyframe" children, which are assumed to be sorted by time.
' curtime is in ms.
' Returns true if curtime is before the end of the tween.
function tween_keyframes(sl as Slice ptr, curop as NodePtr, curtime as integer) as bool
	dim as NodePtr prevframe, nextframe
	prevframe = FirstChild(curop, "keyframe")
	nextframe = prevframe

	if prevframe = NULL then return NO
	nextframe = NextSibling(prevframe, "keyframe")

	if nextframe = NULL orelse curtime < prevframe..int then
		set_slice_property sl, curop."key".zstring, prevframe."value".ptr
		return NO
	end if

	' Find the interval containing curtime
	dim times(1) as integer
	while nextframe
		times(0) = prevframe..int
		times(1) = nextframe..int
		if curtime >= times(0) andalso curtime <= times(1) then
			var diff = times(1) - times(0)
			dim x as double = 0
			if diff then
				x = (curtime - times(0)) / diff
			end if
			' The first keyframe might have a time > 0ms. Don't extrapolate
			if x < 0 then x = 0

			interpolate_slice_property sl, curop."key".zstring, x, prevframe."value".ptr, nextframe."value".ptr

			return YES
		end if

		prevframe = nextframe
		nextframe = NextSibling(nextframe, "keyframe")
	wend

	return NO
end function

' Advance the animation by one op.
' Returns true on success or finished animation, false on error.
' Sets anim = NULL on error or finished animation.
' Does not check for infinite loops; caller must do that.
function AnimationState.animate_step() as AnimationState.StepResult
	declare as NodePtr curop
	'? "animate_step: curop", curop, curop..name, curop..int

	if anim = NULL then return stepError

	' This happens only if the animation doesn't end with Repeat
	if curop = NULL then
		anim_looplimit -= 1
		' anim_loop = 0 means default number of loops
		' Also refuse to loop if empty.
		if anim_loop = 0 or anim_loop = 1 orelse NumChildren(anim->opsnode) = 0 then
			return stepEnd
		end if
		if anim_loop > 0 then anim_loop -= 1
		curop = FirstChild(anim->opsnode)
	end if

	declare as NodePtr curop

	select case curop..int
		case animOpWait, animOpWaitMS
			' These two opcodes are identical, differing only in how ' they are treated by the editor
			anim_wait += 1
			if anim_wait > ms_to_frames(curop."ms") then
				anim_wait = 0
			else
				return stepWait
			end if
		case animOpFrame
			if sl->SliceType <> slSprite then exit select  'Skip op
			sl->SpriteData->set_frame(sl, curop."frame")

		case animOpSetProp
			set_slice_property sl, curop."key".zstring, curop."value".ptr

		case animOpRepeat
			' If a loop count was specified when playing the animation,
			' then only loop that many times, otherwise repeat forever
			if anim_loop > 0 then
				anim_loop -= 1
				if anim_loop = 0 then
					return stepEnd
				end if
			end if
			curop = FirstChild(anim->opsnode)
			anim_looplimit -= 1
			return stepNext
		case animOpSetOffset
			/'
			offset.x = curop."x"
			offset.y = curop."y"
			'/
		case animOpRelOffset
			/'
			offset.x += curop."x"
			offset.y += curop."y"
			'/
		case animOpPlayFrameGroup
			if sl->SliceType <> slSprite then exit select  'Skip op

			if anim_advanced then
				' Set initial frame
				if sl->SpriteData->set_frameid(sl, curop."framegroup" * 100, YES) = -1 then
					'debug "frame group missing"
					exit select
				end if
			end if

			anim_wait += 1
			if anim_wait > ms_to_frames(curop."ms") then
				'? "animOpPlayFrameGroup: wait done"

				' Next frame
				dim as integer frameid, frameidx
				frameid = sl->SpriteData->get_frameid(sl) + 1
				frameidx = sl->SpriteData->set_frameid(sl, frameid, YES)  'exact=YES
				'? "animOpPlayFrameGroup: advance to id=" & frameid & " idx=" & frameidx

				if frameidx = -1 then
					anim_wait = 0  'Done
				else
					anim_wait = 1  'Start wait
					return stepWait
				end if
			else
				'? "animOpPlayFrameGroup: waiting"
				return stepWait
			end if
		case animOpSwitchAnim
			' TODO: should have a way to play an animation in shared_set/fallback_set
			' switch_animation resets the looplimit, but we instead decrement it
			dim looplimit as integer = anim_looplimit - 1
			if switch_animation(curop."anim".str & " " & curop."variant".str) then
				anim_looplimit = looplimit
			end if

		case animOpTween
			if tween_keyframes(sl, curop, frames_to_ms(anim_wait)) = NO then
				anim_wait = 0  'Done
			else
				anim_wait += 1
				return stepWait
			end if

		case animOpUnknown
			' An opcode from a future version not recognised by load_animation_node,
			' which already printed an error. Skip it.
		case else
			' Shouldn't happen
			showbug "bad animation opcode " & curop..name & " " & curop..int & " in '" & anim->name & "'"
			return stepError
	end select

	curop = NextSibling(curop)  'NULL if finished
	return stepNext
end function

' Advance time by one tick. True on success or finished (anim is now NULL!), false on an error/infinite loop
function AnimationState.animate() as bool
	if anim = NULL then return NO

	while anim_looplimit > 0
		select case animate_step()
			case stepError
				stop_animation()
				return NO
			case stepWait
				anim_looplimit = ANIMATION_LOOPLIMIT  'Reset
				anim_advanced = NO
				return YES
			case stepNext
				' animate_step advanced curop
				anim_advanced = YES
			case stepEnd
				stop_animation()
				return YES
		end select
	wend

	' Exceeded the loop limit
	debug "animation '" & anim->name & "' got stuck in an infinite loop"
	stop_animation()
	return NO
end function
