-- vim: set foldmethod=marker ts=4 sw=4 :
-- TODO rewrite variables: armor_model, model
--- Initial definitions ---
-- player model backwards compatibility
MODEL_NAME="player_model"
model=models[MODEL_NAME]
ping=pings
-- Texture dimensions --
TEXTURE_WIDTH = 256
TEXTURE_HEIGHT = 256

local optrequire
do
	local fallback=setmetatable({}, {__index=function() return function() end end})
	function optrequire(...)
		local status, req=pcall(require, ...)
		if status then return req end
		return fallback
	end
end

util = require("nulllib.util")
logging = optrequire("nulllib.logging")
timers=require("nulllib.timers")
nmath=require("nulllib.math")
PartsManager=require("nulllib.PartsManager")
UVManager=require("nulllib.UVManager")
sharedstate=require("nulllib.sharedstate")
sharedconfig=require("nulllib.sharedconfig")
statemonitor=require("nulllib.statemonitor")
KattArmor=require("kattarmor.KattArmor")

---Set optimal settings for random player sounds
---@param sound Sound
---@return Sound
function sound_settings(sound)
	return sound:volume(1):pitch(1):pos(player:getPos())
end

-- shortcuts for /figura run so i don't have to type so much
C={}

-- math functions
lerp=math.lerp -- this is implemented in figura now
wave=nmath.wave

-- for global state tracking, post syncState era
-- this isn't entirely necessary but it's good to know what has and hasn't been
-- migrated yet. I should probably rewrite stuff that uses it, but most of it
-- isn't nearly as bad as the old syncState/setLocalState/etc. It's currently
-- only used for a somewhat scattered color check funciton.
STATE={
	["current"]={},
	["old"]={}
}

-- (the last remnants of) syncState {{{
do
	local pm_refresh=false
	function pmRefresh()
		logging.debug([[part refresh queued
		]], util.traceback())
		pm_refresh=true
	end

	function doPmRefresh()
		if pm_refresh then
			PartsManager.refreshAll()
			pm_refresh=false
		end
	end
end
-- }}}

-- Master configuration -- {{{

-- master state variables and configuration (do not access within pings) --
do
	local is_host=host:isHost()
	local defaults={
		["armor_enabled"]=true,
		["vanilla_enabled"]=false,
		["snore_enabled"]=true,
		["snore_augh"]=false,
		["print_settings"]=false,
		["vanilla_partial"]=false,
		["tail_enabled"]=true,
		["aquatic_enabled"]=true,
		["aquatic_override"]=false,
		["is_cat"]=true
	}
	local function armor_refresh_callback()
		if player:isLoaded() then pmRefresh() end
	end
	local callbacks={
		["armor_enabled"]=armor_refresh_callback
	}
	sharedconfig.load_defaults(defaults, callbacks)
end

local function printSettings()
	print("Settings:")
	printTable(sharedconfig.load())
end
if sharedconfig.load("print_settings") then
	printSettings()
end

--- Convenience, manipulate settings
---@param key? string Key to access
---@param value? any Value to set
function C.set(key, value)
	if value ~= nil and key ~= nil then
		sharedconfig.save(key, value)
	elseif key ~= nil then
		print(sharedconfig.load(key))
	else
		printSettings()
	end
end
-- }}}

-- Parts, groups, other constants -- {{{
HEAD=model.Head.Head
FACE=model.Head.Face
SHATTER=model.Head.Shatter
TAIL=model.Body_Tail.Tail_L1
VANILLA_PARTIAL={}
VANILLA_GROUPS={
	["HEAD"]={vanilla_model.HEAD, vanilla_model.HAT},
	["BODY"]={vanilla_model.BODY, vanilla_model.JACKET},
	["LEFT_ARM"]={vanilla_model.LEFT_ARM, vanilla_model.LEFT_SLEEVE},
	["RIGHT_ARM"]={vanilla_model.RIGHT_ARM, vanilla_model.RIGHT_SLEEVE},
	["LEFT_LEG"]={vanilla_model.LEFT_LEG, vanilla_model.LEFT_PANTS},
	["RIGHT_LEG"]={vanilla_model.RIGHT_LEG, vanilla_model.RIGHT_PANTS},
	["OUTER"]={ vanilla_model.HAT, vanilla_model.JACKET, vanilla_model.LEFT_SLEEVE, vanilla_model.RIGHT_SLEEVE, vanilla_model.LEFT_PANTS, vanilla_model.RIGHT_PANTS },
	["INNER"]={ vanilla_model.HEAD, vanilla_model.BODY, vanilla_model.LEFT_ARM, vanilla_model.RIGHT_ARM, vanilla_model.LEFT_LEG, vanilla_model.RIGHT_LEG },
	["ALL"]={ vanilla_model.HEAD, vanilla_model.BODY, vanilla_model.LEFT_ARM, vanilla_model.RIGHT_ARM, vanilla_model.LEFT_LEG, vanilla_model.RIGHT_LEG, vanilla_model.HAT, vanilla_model.JACKET, vanilla_model.LEFT_SLEEVE, vanilla_model.RIGHT_SLEEVE, vanilla_model.LEFT_PANTS, vanilla_model.RIGHT_PANTS },
	["ARMOR"]={vanilla_model.LEGGINGS, vanilla_model.BOOTS, vanilla_model.CHESTPLATE, vanilla_model.HELMET}
}

-- these are inefficient, redundancy is better in this case
-- for _, v in pairs(VANILLA_GROUPS.INNER) do table.insert(VANILLA_GROUPS.ALL,v) end
-- for _, v in pairs(VANILLA_GROUPS.OUTER) do table.insert(VANILLA_GROUPS.ALL,v) end
-- for _, v in pairs(armor_model) do table.insert(VANILLA_GROUPS.ARMOR, v) end

MAIN_GROUPS={model.Head, model.RightArm, model.LeftArm, model.RightLeg, model.LeftLeg, model.Body } -- RightArm LeftArm RightLeg LeftLeg Body Head

TAIL_LEGGINGS={
	model.Body.LeggingsTop,
	model.Body.LeggingsTopTrimF,
	model.Body.LeggingsTopTrimB,
	model.Body.MTail1.Leggings,
	model.Body.MTail1.LeggingsTrim,
	model.Body.MTail1.MTail2.LeggingsBottom
}
TAIL_LEGGINGS_COLOR={
	model.Body.LeggingsTopTrimF,
	model.Body.LeggingsTopTrimB,
	model.Body.MTail1.Leggings,
	model.Body.MTail1.LeggingsTrim,
	model.Body.MTail1.MTail2.LeggingsBottom
}
TAIL_BOOTS={
	model.Body.MTail1.MTail2.MTail3.Boot,
	model.Body.MTail1.MTail2.MTail3.LeatherBoot
}
TAIL_BONES={
	model.Body.MTail1,
	model.Body.MTail1.MTail2,
	model.Body.MTail1.MTail2.MTail3,
	model.Body.MTail1.MTail2.MTail3.MTail4
}
REG_TAIL_BONES={
	TAIL,
	TAIL.Tail_L2,
	TAIL.Tail_L2.Tail_L3,
	TAIL.Tail_L2.Tail_L3.fin
}
BODY_EMISSIVES={
	model.Body.MTail1.MTailDots1,
	model.Body.MTail1.MTail2.MTailDots2,
	model.Body.MTail1.MTail2.MTail3.MTailDots3,
	model.Body.MTail1.MTail2.MTail3.MTail4.MTailDots4,
	TAIL.TailDots1,
	TAIL.Tail_L2.TailDots2,
	TAIL.Tail_L2.Tail_L3.TailDots3,
	TAIL.Tail_L2.Tail_L3.fin.TailDots4,
	model.Head.EmDots,
	model.LeftArm.LeftArmEm,
	model.RightArm.RightArmEm,
	model.LeftLeg.LeftLegEm,
	model.RightLeg.RightLegEm
}
FACE_EMISSIVES={
	model.Head.Face
}
EMISSIVES=util.mergeTable(BODY_EMISSIVES, FACE_EMISSIVES)
COLORS={}
COLORS.neutral=vec(127/255,127/255,255/255)
COLORS.hurt=   vec(1, 0, 63/255)
COLORS.lava=   vec(1, 128/255, 64/255)
-- prev 255 160 192
COLORS.owo=    vec(1, 128/255, 160/255)
COLORS["end"]="end"
for _, v in pairs(EMISSIVES) do
	v:setColor(COLORS.neutral)
end

local gay_idiot_uuid="3dad78e8-6979-404f-820e-952ce20964a0" -- boy fren
--gay_idiot_uuid="468554f1-27cd-4ea1-9308-3dd14a9b1a12" -- alt account (testing)

-- }}}

--  PartsManager rules {{{
-- Vanilla rules

do
	-- TODO
	function getVanillaVisible()
		return (not avatar:canEditVanillaModel()) or vanilla_model.PLAYER:getVisible()
	end

	local function vanillaPartial()
		if sharedconfig.load("vanilla_enabled") then
			return false
		end
		return sharedconfig.load("vanilla_partial")
	end

	local function forceVanilla()
		print(vanilla_model.PLAYER:getVisible())
		return not avatar:canEditVanillaModel() or sharedconfig.load("vanilla_enabled") or vanilla_model.PLAYER:getVisible()
	end

	-- eventually replace this with an instance once PartsManager becomes a class
	local PM=PartsManager


	--- Vanilla state
	-- no cape if tail enabled (it clips)
	PM.addPartFunction(vanilla_model.CAPE, function(last) logging.trace("pm tail enabled func", sharedconfig.load("tail_enabled")) return last and not sharedconfig.load("tail_enabled") end)

	--- Custom state
	-- local tail_parts=util.mergeTable({model.Body.TailBase}, TAIL_BONES)
	local tail_parts={model.Body.MTail1, model.Body.TailBase}
	
	-- TODO: old vanilla_partial groups, use these for texture swap
	-- local vanilla_partial_disabled=util.mergeTable(MAIN_GROUPS, {model.Body.Body, model.Body.BodyLayer})
	-- local vanilla_partial_enabled={model.Head, model.Body}

	-- Show shattered only at low health
	PM.addPartFunction(SHATTER, function(last) return last and sharedstate.get("health") <= 5 end)

	-- Enable tail setting
	PM.addPartFunction(TAIL, function(last) return last and sharedconfig.load("tail_enabled") end)
	-- no legs, regular tail in water if tail enabled
	local mtail_mutually_exclusive={model.LeftLeg, model.RightLeg, TAIL, vanilla_model.LEGGINGS, vanilla_model.BOOTS}
	PM.addPartListFunction(mtail_mutually_exclusive, function(last) return last and not aquaticTailVisible() end)
	-- aquatic tail in water
	PM.addPartListFunction(tail_parts, function(last) return last and aquaticTailVisible() end)

	--- Armor state
	local all_armor=util.reduce(util.mergeTable, {VANILLA_GROUPS.ARMOR, TAIL_LEGGINGS, TAIL_BOOTS})
	PM.addPartListFunction(all_armor, function(last) return last and sharedconfig.load("armor_enabled") end)
	-- Only show armor if equipped
	PM.addPartFunction(model.Body.MTail1.MTail2.MTail3.Boot, function(last) return last and armor_state.boots end)
	PM.addPartFunction(model.Body.MTail1.MTail2.MTail3.LeatherBoot, function(last) return last and armor_state.leather_boots end)
	PM.addPartListFunction(TAIL_LEGGINGS, function(last) return last and armor_state.leggings end)


	-- Disable when vanilla_enabled
	PM.addPartListFunction(MAIN_GROUPS, function(last) return last and not getVanillaVisible() end)
end

SNORES={"snore-1", "snore-2", "snore-3"}
-- }}}

-- Expression change -- {{{
do
	local expressions={}
	expressions.neutral=vec(0,0)
	expressions["end"]=expressions.neutral
	expressions.hurt=vec(0,1)
	expressions.owo=vec(0,2)
	local expruvm=UVManager.new(vec(8, 8), nil, expressions, FACE)
	current_expression="neutral"

	-- color/expression rules
	function getBestColor()
		if current_expression=="owo" then
			return COLORS.owo
		elseif player:isInLava() or player:getDimensionName()=="minecraft:the_nether" then
			return COLORS.lava
		else
			return COLORS.neutral
		end
	end
	function getBestExpression()
		return "neutral"
	end
	function setColor(col)
		if not lock_color then
			col=(col~=nil) and col or getBestColor()
			for _, v in pairs(EMISSIVES) do
				v:setColor(col)
				-- TODO
				-- v:setShader("None")
			end
		end
	end

	-- Expression change code
	function setExpression(expression)
		current_expression=expression
		expruvm:setUV(current_expression)
		-- This expression sticks, so do not set color explicitly
		setColor()
	end
	function changeExpression(expression, ticks)
		expruvm:setUV(expression)
		-- This one is for more explicit "flashes" such as player hurt
		-- animations, get color explicitly
		setColor(COLORS[expression])
		timers.namedWait(ticks, resetExpression, "resetExpression")
	end
	function resetExpression()
		lock_color=false
		expruvm:setUV(current_expression)
		setColor()
	end

	function hurt()
		if sharedconfig.load("is_cat") then
			sound_settings(sounds["entity.cat.hurt"]):play() end
		lock_color=false
		changeExpression("hurt", 10)
		lock_color=true
		PartsManager.refreshPart(SHATTER)
	end
end
-- }}}

-- Action Wheel & Pings -- {{{
-- TODO
do
	wheel={}
	local wheel_index=1
	function wheelScroll(change)
		wheel_index=((wheel_index-1)+change)%#wheel+1
		action_wheel:setPage(wheel[wheel_index])
		print("page: " .. wheel_index)
	end

	wheel[1]=action_wheel:newPage()

	action_wheel:setPage(wheel[1])
	action_wheel.scroll=wheelScroll
end



wheel[1]:newAction():title('test expression'):onLeftClick(function() ping.expressionTest() end)
function ping.expressionTest()
	logging.debug("ping.expressionTest")
	changeExpression("hurt", 10)
end
wheel[1]:newAction():title('log health'):onLeftClick(function() print(player:getHealth()) end)
wheel[1]:newAction():title('Toggle Armor'):onLeftClick(function() setArmor() end)
wheel[1]:newAction():title('T-Pose'):onLeftClick(function() ping.tPose() end)
wheel[1]:newAction():title('UwU'):onLeftClick(function() ping.expr("owo") end)
-- action_wheel.SLOT_8.setTitle('sssss...')
-- action_wheel.SLOT_8.setItem("minecraft:creeper_head")
-- action_wheel.SLOT_8.setFunction(function() switch_model('misc/Creeper') end)

-- Pings --
--- Damage function --
function ping.expr(expr)
	logging.debug("ping.expr")
	local val=(expr==current_expression) and "neutral" or expr
	setExpression(val)
end


function ping.oof(health) -- This is a replacement for onDamage, that function doesn't sync for some reason
	logging.debug("ping.oof")
	hurt()
end

--- Toggle Armor ---
function setArmor(state)
	sharedconfig.save("armor_enabled", state)
end

do
	local purr_sound

	---@param state boolean
	function purr(state)
		if state and not purr_sound then
			purr_sound=sound_settings(sounds["entity.cat.purr"]):loop(true):play()
		elseif not state and purr_sound then
			purr_sound:stop()
			purr_sound=nil
		end
		return purr_sound
	end
end

local snore
do
	local snores={sounds["sounds.snore-1"], sounds["sounds.snore-2"], sounds["sounds.snore-3"]}
	
	local snore_index=1
	local is_snoring=false

	local function state_not_sleeping()
		-- return not player:getPose() ~= "SLEEPING"
		return player:getPose() ~= "SLEEPING"
	end

	local function snore_purr()
		purr(true)
		statemonitor.register("snore", state_not_sleeping, function() purr(false) end, 5, true)
	end

	local function snore_augh()
		if sharedconfig.load("snore_enabled") then
			if timers.cooldown(20*4, "snore") then
				sound_settings(snores[snore_index]):stop():play()
				snore_index=snore_index%#snores+1
				print(snore_index)
			end
		end
	end
	function snore()
		if sharedconfig.load("snore_augh") then
			snore_augh()
		else
			snore_purr()
		end
	end

end

-- meow
function pings.meow()
	sound_settings(sounds["entity.cat.ambient"]):play()
end
events.CHAT_SEND_MESSAGE:register(function(msg)
		if sharedconfig.load("is_cat") and string.match(msg, '^/') == nil then
			pings.meow() end
		return msg end,
	"chat_meow")

--- Toggle Vanilla ---
function setVanilla(state)
	sharedconfig.save("vanilla_enabled", state)
end


function ping.tPose()
	logging.debug("ping.tPose")
	-- TODO
	-- local_state.emote_vector=player:getPos()
	-- animation.tpose.start()
end
-- }}}

-- Tail stuff {{{
local tail_cooldown
function aquaticTailVisible()
	tail_cooldown=tail_cooldown or 0
	return (sharedconfig.load("aquatic_enabled") and (player:isInWater() or player:isInLava()) or sharedconfig.load("aquatic_override") or tail_cooldown>0) and not getVanillaVisible()
end

local function updateTailVisibility()
	local anim=player:getPose()
	local water=player:isInWater()
	local lava=player:isInLava()
	tail_cooldown=(tail_cooldown and tail_cooldown > 0) and tail_cooldown-1 or 0
	if aquaticTailVisible() and (anim=="SLEEPING" or anim=="SPIN_ATTACK" or anim=="FALL_FLYING" or water or lava) then
		tail_cooldown=anim=="SPIN_ATTACK" and 60 or (tail_cooldown >= 10 and tail_cooldown or 10)
	end
	if STATE.old.aquatic_tail_visible ~= aquaticTailVisible() then pmRefresh() end
	STATE.old.aquatic_tail_visible=aquaticTailVisible()
	renderer.forcePaperdoll=aquaticTailVisible()
end

-- armor {{{
armor_color={}
armor_color['leather'] = {131 /255 , 84  /255 , 50  /255}
armor_glint={}
armor_state={}
armor_state['leggings']=false
armor_state['boots']=false
armor_state['leather_boots']=false

do
	local positions={}
	positions['leather']=vec(0, 0)
	positions['iron']=vec(0, 1)
	positions['chainmail']=vec(0, 2)
	positions['golden']=vec(0, 3)
	positions['diamond']=vec(0, 4)
	positions['netherite']=vec(0, 5)
	tailuvm=UVManager.new(vec(0, 19), nil, positions)
end

-- TODO fix code after optimization in prewrite
-- function armor()
-- 	if true then return nil end
-- 	-- ^ hacky way to disable a function without uncommenting the entire thing to not break git vcs
-- 
-- 	-- Get equipped armor, extract name from item ID
-- 	local leggings_item = player.getEquipmentItem(4)
-- 	local boots_item    = player.getEquipmentItem(3)
-- 	local leggings     = string.sub(leggings_item.getType(), 11, -10)
-- 	local boots        = string.sub(boots_item.getType(),    11, -7)
-- 
-- 	if local_state.armor_enabled then
-- 		if old_state.leggings ~= leggings or old_state.armor_enabled ~= local_state.armor_enabled  then
-- 			-- leggings
-- 			armor_glint.leggings=leggings_item.hasGlint()
-- 			local leggings_color=colorArmor(leggings_item) or armor_color[leggings]
-- 			local uv=tailuvm:getUV(leggings)
-- 			if uv ~= nil then
-- 				armor_state.leggings=true
-- 				for k, v in pairs(TAIL_LEGGINGS) do
-- 					v.setUV(uv)
-- 				end
-- 				if leggings=="leather" then
-- 					for k, v in pairs(TAIL_LEGGINGS_COLOR) do
-- 						v.setColor(leggings_color)
-- 					end
-- 				else
-- 					for k, v in pairs(TAIL_LEGGINGS) do
-- 						v.setColor({1, 1, 1})
-- 					end
-- 				end
-- 			else
-- 				armor_state.leggings=false
-- 			end
-- 			pmRefresh()
-- 		end
-- 
-- 		if old_state.boots ~= boots or old_state.armor_enabled ~= local_state.armor_enabled  then
-- 			-- boots
-- 			armor_glint.boots=boots_item.hasGlint()
-- 			local boots_color=colorArmor(boots_item) or armor_color[boots]
-- 			local uv_boots=tailuvm:getUV(boots)
-- 			if uv_boots ~= nil then
-- 				armor_state.boots=true
-- 				for k, v in pairs(TAIL_BOOTS) do
-- 					v.setUV(uv_boots)
-- 				end
-- 				if boots=="leather" then
-- 					model.Body.MTail1.MTail2.MTail3.Boot.setColor(boots_color)
-- 					armor_state.leather_boots=true
-- 				else
-- 					model.Body.MTail1.MTail2.MTail3.Boot.setColor({1, 1, 1})
-- 					armor_state.leather_boots=false
-- 				end
-- 			else
-- 				armor_state.boots=false
-- 			end
-- 			pmRefresh()
-- 		end
-- 	else
-- 		armor_glint.leggings=false
-- 		armor_glint.boots=false
-- 	end
-- 
-- 	if armor_glint.leggings then
-- 		for _, v in pairs(TAIL_LEGGINGS) do
-- 			v.setShader("Glint")
-- 		end
-- 	else
-- 		for _, v in pairs(TAIL_LEGGINGS) do
-- 			v.setShader("None")
-- 		end
-- 	end
-- 	if armor_glint.boots then
-- 		for _, v in pairs(TAIL_BOOTS) do
-- 			v.setShader("Glint")
-- 		end
-- 	else
-- 		for _, v in pairs(TAIL_BOOTS) do
-- 			v.setShader("None")
-- 		end
-- 	end
-- 
-- 	old_state.boots=boots
-- 	old_state.leggings=leggings
-- 	old_state.armor_enabled=local_state.armor_enabled
-- end

function colorArmor(item)
	local tag = item.tag
	if tag ~= nil and tag.display ~= nil and tag.display.color ~= nil then
		return vectors.intToRGB(tag.display.color)
	end
end
-- }}}

function resetAngles(part)
	part:setRot(vec(0,0,0))
end

--- Get the vertical distance between a position and the block hitbox directly below it
-- @param block BlockState
-- @param pos Vec3
function getVerticalBlockOffset(block, pos)
	local relative_pos=pos-block:getPos()
	local absolute_offset=0
	-- print(relative_pos)
	-- iterates over collision shapes in block
	for k, shape_corners in ipairs(block:getCollisionShape()) do
		-- printTable(shape_corners)
		local above=true
		for _, axis in ipairs({"x", "z"}) do
			if not (relative_pos[axis] >= shape_corners[1][axis] and relative_pos[axis] <= shape_corners[2][axis]) then
				above=false
			end
		end
		-- print("above for shape " .. tostring(k) .. " is " .. tostring(above))
		absolute_offset=(above and shape_corners[2]["y"] >= absolute_offset) and shape_corners[2]["y"] or absolute_offset
	end
	return relative_pos.y-absolute_offset
end

function curveMTail(delta)
	local max_curve=-28
	local block_offset=0.6 -- extra length of tail compared to vanilla model
	local pos=player:getPos(delta)
	local block=world.getBlockState(pos)
	-- if block has a "level" property (this applies to fluids only)
	if block:getProperties().level or block:isAir() then
		block=world.getBlockState(pos+vec(0,-1,0))
	end
	local block_offset_lerp=1-(math.min(1, math.max(0, getVerticalBlockOffset(block,pos)/block_offset)))
	return math.lerp(0, max_curve, block_offset_lerp)

end

function animateMTail(val, delta)
	local chest_rot = 3
	local period=3*math.pi
	local amplitude_multiplier=1
	local curve=0
	-- TODO vanilla model manipulation broke, add chestplate model
	-- armor_model.CHESTPLATE:setRot(vec( -wave(val, period, math.rad(3)), 0, 0 ))
	-- this makes it work with partial vanilla
	-- vanilla_model.BODY:setRot(vec( -wave(val, period, math.rad(3)), 0, 0 ))
	-- vanilla_model.JACKET:setRot(vec( -wave(val, period, math.rad(3)), 0, 0 ))

	local vehicle = player:getVehicle()
	if vehicle then
		if vehicle:getType() ~= "create:seat" then
			curve=22
		end
		period=4*math.pi
		amplitude_multiplier=0.33
		TAIL_BONES[1]:setRot(vec(80,0,0))
	else
		local pose=player:getPose()
		if pose ~= "SWIMMING" and pose ~= "SLEEPING" then
			curve=curveMTail(delta)
		end
		if pose == "SLEEPING" then
			period=6*math.pi
			amplitude_multiplier=0.3
		end
		resetAngles(model.Body)
		model.Body:setRot(vec( wave(val, period, 3*amplitude_multiplier), 0, 0 ))
		model.Body.LeggingsTopTrimF:setRot(vec( wave(val-1, period, 4*amplitude_multiplier), 0, 0 ))
		model.Body.LeggingsTopTrimB:setRot(vec( wave(val-1, period, 4*amplitude_multiplier), 0, 0 ))
		TAIL_BONES[1]:setRot(vec( wave(val-1, period, 7*amplitude_multiplier) + curve, 0, 0 ))
	end

	TAIL_BONES[2]:setRot(vec( wave(val-2, period,  8*amplitude_multiplier) + curve, 0, 0 ))
	TAIL_BONES[3]:setRot(vec( wave(val-3, period, 12*amplitude_multiplier) + curve, 0, 0 ))
	TAIL_BONES[4]:setRot(vec( wave(val-4, period, 15*amplitude_multiplier) + curve, 0, 0 ))
end
tail_original_rot={}
for k, v in ipairs(REG_TAIL_BONES) do
	tail_original_rot[k]=v:getRot()
end
function animateTail(val)
	local per_y=20*4
	local per_x=20*6
	for k, _ in ipairs(REG_TAIL_BONES) do
		local cascade=(k-1)*12
		REG_TAIL_BONES[k]:setRot(vec( tail_original_rot[k].x + wave(val-cascade, per_x, 3), wave(val-cascade, per_y, 12), tail_original_rot[k].z ))
	end
end

STATE.current.anim_tick=0
STATE.current.anim_cycle=0
STATE.old.anim_cycle=0

local function animateTick()
	STATE.current.anim_tick = STATE.current.anim_tick + 1
	if aquaticTailVisible() then
		local velocity = player:getVelocity()

		if aquaticTailVisible() then
			STATE.old.anim_cycle=STATE.current.anim_cycle
			local player_speed = math.sqrt(velocity.x^2 + velocity.y^2 + velocity.z^2)
			local animation=player:getPose()
			local factor=(not player:isInWater() and (animation=="FALL_FLYING" or animation=="SPIN_ATTACK")) and 0.5 or 5
			STATE.current.anim_cycle=STATE.current.anim_cycle + (player_speed*factor+0.75)
			-- bubble animation would go here but i don't have that (yet)
		end

	else
		STATE.old.anim_cycle=STATE.current.anim_cycle
		STATE.current.anim_cycle=STATE.current.anim_cycle+1
	end
end



-- }}}

-- Wipers {{{
do
	local wiper_state="STOPPED" -- valid states are "STOPPED", "STARTING", "STARTED", "STOPPING"
	local wiper_wipe=animations[MODEL_NAME].wiper_wipe
	local wiper_target_state=false
	local wiper_deploy=animations[MODEL_NAME].wiper_deploy
	local wiper=model.Head_Accessory.Wiper
	assert(type(wiper_wipe)=="Animation")
	assert(type(wiper_deploy)=="Animation")
	assert(type(wiper)=="ModelPart")

	wiper_wipe:setPriority(1)
	wiper_deploy:setPriority(0)
	wiper:setVisible(false)

	---Check if a HOLD type animation is currently held
	---@param animation Animation Check if an animation is waiting at the end of a hold
	local function hold_ended(animation)
		local anim_time=animation:getTime()
		local anim_length=animation:getLength()
		
		return animation:getLoop()=="HOLD" and
			(anim_time==anim_length or anim_time==0)
	end

	---Finish and hold an animation on the last frame
	---@return boolean Whether or not the animation has reached the end of the hold
	function stop_and_wait(animation)
		if hold_ended(animation) then
			return true
		end
		animation:setLoop("HOLD")
		return false
	end

	-- steps to stop:
	-- stop wiping animation
	-- (set to hold, wait for time)
	-- start retract animation (wiper_deploy, speed: -1)
	-- wait for end
	-- finalize state, hide part
	--
	-- steps to start:
	-- show part
	-- start animation (wiper_deploy, speed: 1)
	-- wait for end
	-- start wipe animation, loop
	local wiper_run_deploy_switch={
		STOPPED=function(state)
			if state then
				wiper_state="STARTING"
				wiper_deploy:setSpeed(1):stop():play()
				wiper:setVisible(true)
			end
		end,
		STARTING=function(state)
			if wiper_deploy:getTime()==wiper_deploy:getLength() then
				wiper_deploy:stop()
				wiper_wipe:play():setLoop("LOOP")
				wiper_state="STARTED"
			end
		end,
		STARTED=function(state)
			if not state then
				wiper_state="STOPPING"
				stop_and_wait(wiper_wipe)
			end
		end,
		STOPPING=function(state)
			-- stop process has concluded
			if wiper_deploy:getPlayState() == "PLAYING" and wiper_deploy:getTime()==0 then
				wiper_state="STOPPED"
				wiper:setVisible(false)
				wiper_deploy:stop()
			elseif stop_and_wait(wiper_wipe) then
				wiper_wipe:stop()
				wiper_deploy:setSpeed(-1):play()
			end
		end,
	}

	--- Run deploy action
	---@param state boolean Should the wiper be deployed?
	local function wiper_run_deploy(state)
		wiper_run_deploy_switch[wiper_state](state)
	end

	local function wiper_sync_state()
		wiper_run_deploy(wiper_target_state)
	end
	--- Set wiper state. Ignores subsequent calls
	---@param state boolean true to start wiper, false to stop wiper
	function set_wiper(state)
		if state ~= wiper_target_state then
			wiper_target_state=state
		end
	end

	function wiper_tick()
		set_wiper(player:isInRain())
		wiper_sync_state()
	end

	events.TICK:register(wiper_tick, "wiper")
end
-- }}}

-- initialize values -- {{{
function player_init()
	local function health_callback(new, old)
		if old > new then
			hurt()
		end
		PartsManager.refreshPart(SHATTER)
	end

	sharedstate.init("health", player:getHealth(), health_callback)
	-- TODO set part visibility in avatar.json
	-- actually it's probably fine, it's jsut here because i forget visibility settings
	-- local all_parts=util.recurseModelGroup(model)

	-- for k, v in pairs(all_parts) do
	-- 	v:setVisible(nil)
	-- end
	pmRefresh()
	events.ENTITY_INIT:remove("player_init")
end

events.ENTITY_INIT:register(function() return player_init() end, "player_init")

-- Initial configuration --
-- TODO x2 fix below, this entire block may not be needed with PartsManager
if avatar:canEditVanillaModel() then
	vanilla_model.PLAYER:setVisible(false)
else
	model:setVisible(false)
end
STATE.current.anim_tick=0
-- }}}

-- Tick function -- {{{
function hostTick()
	sharedstate.set("health", player:getHealth())
end

local gay_idiot_check
do
	local nearby=false
	local nearby_ticks=0
	function pings.set_gay_idiot_nearby(state)
		if state then
			pings.expr("owo")
			purr(true)
		else
			pings.expr("neutral")
			purr(false)
		end
	end

	local function set_gay_idiot_nearby(state)
		if state ~= nearby then
			nearby=state
			pings.set_gay_idiot_nearby(state)
		end
	end

	---@param frequency? integer time since last check, default 1
	function gay_idiot_check(frequency)
		frequency=frequency or 1
		nearby_ticks=nearby_ticks or 0
		local gay_idiot=world.getEntity(gay_idiot_uuid)

		-- if exists
		if gay_idiot ~= nil then
			-- if nearby then add to timer
			local distance = (gay_idiot:getPos() - player:getPos()):length()
			if distance <= 1 then
				nearby_ticks=nearby_ticks+frequency
			else
				nearby_ticks=0
			end

			set_gay_idiot_nearby(nearby_ticks>=5*20)
		else
			set_gay_idiot_nearby(false)
		end
	end
end

function tick()
	STATE.current.color_check=player:isInLava() ~=
		(player:getDimensionName()=="minecraft:the_nether")
	if STATE.old.color_check~=STATE.current.color_check then
		setColor()
	end
	-- optimization, only execute these with certain frequency --
	if world.getTime() % 5 == 0 then -- 1/4 second
		if player:getPose() == "SLEEPING" then
			snore()
		end

		if host:isHost() then gay_idiot_check(5) end

		-- unneeded for now but can uncomment if needed
		--if world.getTime() % 20 == 0 then -- 1 second
			-- Sync state every 10 seconds
			if world.getTime() % (20*10) == 0 then
				sharedstate.sync()
			end
		--end
	end

	hostTick()

	-- TODO
	-- if animation.tpose.isPlaying() and local_state.emote_vector.distanceTo(player.getPos()) >= 0.5 then
	-- 	animation.tpose.stop()
	-- end


	-- Refresh tail armor state
	-- TODO re add armor stuff
	--armor()
	-- Implements tail cooldown conditions
	updateTailVisibility()

	-- Animation code resides in this function
	animateTick()

	-- Check for queued PartsManager refresh
	doPmRefresh()
	-- End of tick --
	STATE.old.color_check=STATE.current.color_check
end
events.TICK:register(function() if player then tick() end end, "main_tick")
-- }}}

-- Render function {{{
local function render(delta)
	if aquaticTailVisible() then
		animateMTail((lerp(STATE.old.anim_cycle, STATE.current.anim_cycle, delta) * 0.2), delta)
	else
		resetAngles(model.Body)
		-- resetAngles(vanilla_model.BODY)
		-- resetAngles(vanilla_model.JACKET)
		-- resetAngles(armor_model.CHESTPLATE)
		animateTail((lerp(STATE.old.anim_cycle, STATE.current.anim_cycle, delta)))
	end
end
-- TODO this may break animation during death
events.RENDER:register(function(delta) if player then render(delta) end end, "main_render")
-- }}}