-- vim: set foldmethod=marker ts=4 sw=4 :
-- TODO rewrite variables: armor_model, model
--- Initial definitions ---
-- player model backwards compatibility
model=models.player_model
armor_model={
	["BOOTS"]=vanilla_model.BOOTS,
	["LEGGINGS"]=vanilla_model.LEGGINGS,
	["CHESTPLATE"]=vanilla_model.CHESTPLATE,
	["HELMET"]=vanilla_model.HELMET
}
ping=pings
-- Texture dimensions --
TEXTURE_WIDTH = 256
TEXTURE_HEIGHT = 256

LOG_LEVEL="INFO"

-- Basic logging --{{{

do
	logging = {}
	local loglevels={
		["SILENT"]=0,
		["FATAL"]=1,
		["ERROR"]=2,
		["WARN"]=3,
		["INFO"]=4,
		["DEBUG"]=5,
		["TRACE"]=6
	}

	-- default log level
	local loglevel="INFO"

	function setLogLevel(level)
		loglevel=loglevels[level] and level or loglevel
	end

	setLogLevel(LOG_LEVEL)

	local function printLog(severity, message)
		if (loglevels[loglevel]) >= severity then
			log("[" .. loglevel .. "] " .. message)
		end
	end

	function logging.fatal(message) printLog(1, message) end
	function logging.error(message) printLog(2, message) end
	function logging.warn(message) printLog(3, message) end
	function logging.info(message) printLog(4, message) end
	function logging.debug(message) printLog(5, message) end
	function logging.trace(message) printLog(6, message) end
end

-- }}}

-- utility functions -- {{{

--- Create a string representation of a table
--- @param o table
function dumpTable(o)
	if type(o) == 'table' then
		local s = '{ '
		local first_loop=true
		for k,v in pairs(o) do
			if not first_loop then
				s = s .. ', '
			end
			first_loop=false
			if type(k) ~= 'number' then k = '"'..k..'"' end
			s = s .. '['..k..'] = ' .. dumpTable(v)
		end
		return s .. '} '
	else
		return tostring(o)
	end
end

do
	local function format_any_value(obj, buffer)
		local _type = type(obj)
		if _type == "table" then
			buffer[#buffer + 1] = '{"'
			for key, value in next, obj, nil do
				buffer[#buffer + 1] = tostring(key) .. '":'
				format_any_value(value, buffer)
				buffer[#buffer + 1] = ',"'
			end
			buffer[#buffer] = '}' -- note the overwrite
		elseif _type == "string" then
			buffer[#buffer + 1] = '"' .. obj .. '"'
		elseif _type == "boolean" or _type == "number" then
			buffer[#buffer + 1] = tostring(obj)
		else
			buffer[#buffer + 1] = '"???' .. _type .. '???"'
		end
	end
	--- Dumps object as UNSAFE json, i stole this from stackoverflow so i could use json.tool to format it so it's easier to read
	function dumpJSON(obj)
		if obj == nil then return "null" else
			local buffer = {}
			format_any_value(obj, buffer)
			return table.concat(buffer)
		end
	end
end


-- TODO: accept model part to determine texture width and height
---@param uv table
function UV(uv)
	return vec(
	uv[1]/TEXTURE_WIDTH,
	uv[2]/TEXTURE_HEIGHT
	)
end


---@param inputstr string
---@param sep string
function splitstring (inputstr, sep)
        if sep == nil then
                sep = "%s"
        end
        local t={}
        for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
                table.insert(t, str)
        end
        return t
end

---@param input string
function unstring(input)
	if input=="nil" then
		return nil
	elseif input == "true" or input == "false" then
		return input=="true"
	elseif tonumber(input) ~= nil then
		return tonumber(input)
	else
		return input
	end
end

---@param func function
---@param table table
function map(func, table)
	local t={}
	for k, v in pairs(table) do
		t[k]=func(v)
	end
	return t
end


---@param func function
---@param table table
function filter(func, table)
	local t={}
	for k, v in pairs(table) do
		if func(v) then
			t[k]=v
		end
	end
	return t
end

---@param tbl table
---@param val any
function has_value(tbl, val)
	for _, v in pairs(tbl) do
		if v==val then return true end
	end
	return false
end

--- Unordered reduction, only use when working with dictionaries and
--- execution order does not matter
---@param tbl table Table to reduce
---@param func function Function used to reduce table
---@param init any Initial operand for reduce function
function reduce(func, tbl, init)
	local result = init
	local first_loop = true
	for _, v in pairs(tbl) do
		if first_loop and init == nil then
			result=v
		else
			result = func(result, v)
		end
		first_loop=false
	end
	return result
end

--- Ordered reduction, does not work with dictionaries
---@param tbl table Table to reduce
---@param func function Function used to reduce table
---@param init any Initial operand for reduce function
function ireduce(func, tbl, init)
	local result = init
	local first_loop = true
	for _, v in ipairs(tbl) do
		if first_loop and init == nil then
			result=v
		else
			result = func(result, v)
		end
		first_loop=false
	end
	return result
end

--- Merge two tables. First table value takes precedence when conflict occurs.
---@param tb1 table
---@param tb2 table
function mergeTable(tb1, tb2)
	local t={}
	for k, v in pairs(tb1) do
		t[k]=v
	end
	for k, v in pairs(tb2) do
		if type(k)=="number" then
			table.insert(t, v)
		else
			if t[k]==nil then
				t[k]=v
			end
		end
	end
	return t
end

function debugPrint(var)
	print(var)
	return var
end

function debugPrintTable(var)
	printTable(var)
	return var
end

--- Recursively walk a model tree and return a table containing the group and each of its sub-groups
--- @param group table The group to recurse
--- @return table Resulting table
function recurseModelGroup(group)
	local t={}
	table.insert(t, group)
	if group:getType()=="GROUP" then
		for k, v in pairs(group:getChildren()) do
			for _, v2 in pairs(recurseModelGroup(v)) do
				table.insert(t, v2)
			end
		end
	end
	return t
end
-- }}}

-- Timer (not mine lol) -- {{{
-- TODO investigate if events can replace some of this
do
	local timers = {}
	function wait(ticks,next)
		table.insert(timers, {t=world.getTime()+ticks,n=next})
	end
	local function tick()
		for key,timer in pairs(timers) do
			if world.getTime() >= timer.t then
				timer.n()
				table.remove(timers,key)
			end
		end
	end

	events.TICK:register(function() if player then tick() end end, "timer")
end

-- named timers (this one is mine but heavily based on the other) --
-- if timer is armed twice before expiring it will only be called once) --
do
	local timers = {}
	function namedWait(ticks, next, name)
		-- main difference, this will overwrite an existing timer with
		-- the same name
		timers[name]={t=world.getTime()+ticks,n=next}
	end
	local function tick()
		for key, timer in pairs(timers) do
			if world.getTime() >= timer.t then
				timer.n()
				timers[key]=nil
			end
		end
	end
	events.TICK:register(function() if player then tick() end end, "named_timer")
end

-- named cooldowns
do
	local timers={}
	function cooldown(ticks, name)
		if timers[name] == nil then
			timers[name]={t=world.getTime()+ticks}
			return true
		end
		return false
	end
	local function tick()
		for key, timer in pairs(timers) do
			if world.getTime() >= timer.t then
				timers[key]=nil
			end
		end
	end
	events.TICK:register(function() if player then tick() end end, "cooldown")
end

function rateLimit(ticks, next, name)
	if cooldown(ticks+1, name) then
		namedWait(ticks, next, name)
	end
end

-- }}}

-- syncState {{{
do
	local counter=0
	function syncState()
		-- ping.setSnoring(skin_state.snore_enabled)
		if counter < 5 then
			ping.syncState((setLocalState()))
			counter=counter+1
		end
	end

	local function cooldownDecay()
		if counter>0 and world.getTime() % 4 == 0 then
			counter = counter - 1
		end
	end

	events.TICK:register(cooldownDecay,"syncStateCooldown")
end

do
	local pm_refresh=false
	function pmRefresh()
		pm_refresh=true
	end

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

function ping.syncState(tbl)
	logging.debug("ping.syncState")
	for k, v in pairs(tbl) do
		local_state[k]=v
	end
	pmRefresh()
end
-- }}}

-- Math {{{
--- Sine function with period and amplitude
--- @param x number Input value
--- @param period number Period of sine wave
--- @param amp number Peak amplitude of sine wave
function wave(x, period, amp) return math.sin((2/period)*math.pi*(x%period))*amp end
function lerp(a, b, t) return a + ((b - a) * t) end
-- }}}

-- Master and local state variables -- {{{
-- Local State (these are copied by pings at runtime) --
local_state={}
old_state={}
-- 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,
		["print_settings"]=false,
		["vanilla_partial"]=false,
		["tail_enabled"]=true,
		["aquatic_enabled"]=true,
		["aquatic_override"]=false
	}
	function setLocalState()
		if is_host then
			for k, v in pairs(skin_state) do
				local_state[k]=v
			end
		else
			for k, v in pairs(defaults) do
				if local_state[k] == nil then local_state[k]=v end
			end
		end
		return local_state
	end
	-- TODO reimplement with new data API
	-- if is_host then
	if false then
		local savedData=data.loadAll()
		if savedData == nil then
			for k, v in pairs(defaults) do
				data.save(k, v)
			end
			savedData=data.loadAll()
		end
		skin_state=mergeTable(
		map(unstring,data.loadAll()),
		defaults)
	else
		skin_state=defaults
	end
	setLocalState()
end

function printSettings()
	print("Settings:")
	for k, v in pairs(skin_state) do
		print(tostring(k)..": "..tostring(v))
	end
end
if skin_state.print_settings==true then
	printSettings()
end

function setState(name, state)
	if state == nil then
		skin_state[name]=not skin_state[name]
	else
		skin_state[name]=state
	end
	-- TODO
	-- data.save(name, skin_state[name])
end

-- }}}

-- PartsManager -- {{{
do
	PartsManager={}
	local pm={}

	--- ensure part is initialized
	local function initPart(part)
		local part_key=part
		if pm[part_key] == nil then
			pm[part_key]={}
		end
		pm[part_key].part=part
		if pm[part_key].functions == nil then
			pm[part_key].functions = {}
		end
		if pm[part_key].init==nil then
			pm[part_key].init="true"
		end
	end
	--- Add function to part in PartsManager.
	--- @param part table Any object with a setEnabled() method.
	--- @param func function Function to add to model part's function chain.
	--- @param init? boolean Default value for chain. Should only be set once, subsequent uses overwrite the entire chain's initial value.
	function PartsManager.addPartFunction(part, func, init)
		initPart(part)
		local part_key=part
		if init ~= nil then
			pm[part_key].init=init
		end
		table.insert(pm[part_key]["functions"], func)
	end

	--- Set initial value for chain.
	--- @param part table Any object with a setEnabled() method.
	--- @param init? boolean Default value for chain. Should only be set once, subsequent uses overwrite the entire chain's initial value.
	function PartsManager.setInitialValue(part, init)
		assert(init~=nil)
		initPart(part)
		local part_key=part
		pm[part_key].init=init
	end

	--- Set initial value for chain on all objects in table.
	--- @param group table A table containing objects with a setEnabled() method.
	--- @param init? boolean Default value for chain. Should only be set once, subsequent uses overwrite the entire chain's initial value.
	function PartsManager.setGroupInitialValue(group, init)
		assert(init~=nil)
		for _, v in pairs(group) do
			PartsManager.setInitialValue(v, init)
		end
	end

	--- Evaluate a part's chain to determine if it should be visible.
	--- @param part table An object managed by PartsManager.
	function PartsManager.evaluatePart(part)
		local part_key=part
		assert(pm[part_key] ~= nil)

		local evalFunc=function(x, y) return y(x) end
		local init=pm[part_key].init
		return ireduce(evalFunc, pm[part_key].functions, true)
	end
	local evaluatePart=PartsManager.evaluatePart

	--- Refresh (enable or disable) a part based on the result of it's chain.
	--- @param part table An object managed by PartsManager.
	function PartsManager.refreshPart(part)
		local part_enabled=evaluatePart(part)
		part:setVisible(part_enabled)
		return part_enabled
	end

	--- Refresh all parts managed by PartsManager.
	function PartsManager.refreshAll()
		for _, v in pairs(pm) do
			PartsManager.refreshPart(v.part)
		end
	end

	--- Add function to list of parts in PartsManager
	--- @param group table A table containing objects with a setEnabled() method.
	--- @param func function Function to add to each model part's function chain.
	--- @param default? boolean Default value for chain. Should only be set once, subsequent uses overwrite the entire chain's initial value.
	function PartsManager.addPartGroupFunction(group, func, default)
		for _, v in ipairs(group) do
			PartsManager.addPartFunction(v, func, default)
		end
	end
end
-- }}}

-- UVManager {{{
--
-- TODO: accept model part for built-in UV management, automatic texture size
do
	local mt={}
	--- @class UVManager
	UVManager = {
		step=vec(0,0),
		offset=vec(0,0),
		positions={},
		part=nil,
		dimensions=nil
	}
	mt.__index=UVManager
	--- @return UVManager
	--- @param step Vector2 A vector representing the distance between UVs
	--- @param offset Vector2 A vector represnting the starting point for UVs, or nil
	--- @param positions table A dictionary of names and offset vectors
	--- @param part ModelPart Model part to manage
	function UVManager.new(self, step, offset, positions, part)
		local t={}
		if step ~= nil then t.step=step end
		if offset ~= nil then t.offset=offset end
		if positions ~= nil then t.positions=positions end

		if part ~= nil then
			UVManager.setPart(t, part)
		end

		t=setmetatable(t, mt)
		return t
	end

	--- @param part ModelPart Model part to manage
	function UVManager.setPart(self, part)
		self.part=part
		self.dimensions=part:getTextureSize()
	end

	function UVManager.getUV(self, input)
		local vect={}
		local stp=self.step
		local offset=self.offset
		if type(input) == "string" then
			if self.positions[input] == nil then return nil end
			vect=self.positions[input]
		else
			vect=vectors.of(input)
		end
		local u=offset.x+(vect.x*stp.x)
		local v=offset.y+(vect.y*stp.y)
		if self.dimensions ~= nil then
			-- TODO override for my specific texture, replace this with matrix stuff
			-- (get rid of division once you figure out how setUVMatrix works)
			return vec(u/(self.dimensions.x/2), v/(self.dimensions.y/2))
		else
			return UV{u, v}
		end
	end

	function UVManager.setUV(self, input)
		if self.part == nil then return false end
		self.part:setUV(self:getUV(input))
	end
end
-- }}}

-- Parts, groups, other constants -- {{{
HEAD=model.Head.Head
FACE=model.Head.Face
SHATTER=model.Head.Shatter
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_LEG},
	["RIGHT_LEG"]={vanilla_model.RIGHT_LEG, vanilla_model.RIGHT_PANTS_LEG},
	["OUTER"]={ vanilla_model.HAT, vanilla_model.JACKET, vanilla_model.LEFT_SLEEVE, vanilla_model.RIGHT_SLEEVE, vanilla_model.LEFT_PANTS_LEG, vanilla_model.RIGHT_PANTS_LEG },
	["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_LEG, vanilla_model.RIGHT_PANTS_LEG },
	["ARMOR"]=armor_model
}

-- 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={
	model.Body_Tail,
	model.Body_Tail.Tail_L2,
	model.Body_Tail.Tail_L2.Tail_L3,
	model.Body_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,
	model.Body_Tail.TailDots1,
	model.Body_Tail.Tail_L2.TailDots2,
	model.Body_Tail.Tail_L2.Tail_L3.TailDots3,
	model.Body_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=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 k, v in pairs(EMISSIVES) do
	v:setColor(COLORS.neutral)
end

-- }}}

--  PartsManager rules {{{
-- Vanilla rules

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

	local function vanillaPartial()
		if local_state.vanilla_enabled then
			return false
		end
		return local_state.vanilla_partial
	end

	local function forceVanilla()
		print(vanilla_model.PLAYER:getVisible())
		return not avatar:canEditVanillaModel() or local_state.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) return last and not local_state.tail_enabled end)

	--- Custom state
	-- local tail_parts=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=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 local_state.health <= 5 end)

	-- Enable tail setting
	PM.addPartFunction(model.Body_Tail, function(last) return last and local_state.tail_enabled end)
	-- no legs, regular tail in water if tail enabled
	local mtail_mutually_exclusive={model.LeftLeg, model.RightLeg, model.Body_Tail, armor_model.LEGGINGS, armor_model.BOOTS}
	PM.addPartGroupFunction(mtail_mutually_exclusive, function(last) return last and not aquaticTailVisible() end)
	-- aquatic tail in water
	PM.addPartGroupFunction(tail_parts, function(last) return last and aquaticTailVisible() end)

	--- Armor state
	local all_armor=reduce(mergeTable, {VANILLA_GROUPS.ARMOR, TAIL_LEGGINGS, TAIL_BOOTS})
	PM.addPartGroupFunction(all_armor, function(last) return last and local_state.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.addPartGroupFunction(TAIL_LEGGINGS, function(last) return last and armor_state.leggings end)


	-- Disable when vanilla_enabled
	PM.addPartGroupFunction(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])
		namedWait(ticks, resetExpression, "resetExpression")
	end
	function resetExpression()
		lock_color=false
		expruvm:setUV(current_expression)
		setColor()
	end

	function hurt()
		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:createPage()

	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)
	setState("armor_enabled", state)
	syncState()
end

function snore() end
-- TODO re-enable snoring
-- do
-- 	local snore_enabled=false
-- 	local snore_index=1
-- 	function snore()
-- 		if snore_enabled then
-- 			-- TODO
-- 			-- sound.playCustomSound(SNORES[snore_index],
-- 			-- 	player.getPos(), vectors.of{20,1})
-- 			snore_index=snore_index%#SNORES+1
-- 		end
-- 	end
-- 
-- 	function setSnoring(state)
-- 		setState("snore_enabled", state)
-- 		ping.setSnoring(skin_state.snore_enabled)
-- 	end
-- 
-- 	function ping.setSnoring(state)
-- 		snore_enabled=state
-- 	end
-- end

--- Toggle Vanilla ---
function setVanilla(state)
	setState("vanilla_enabled", state)
	syncState()
end


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

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

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 old_state.aquaticTailVisible ~= aquaticTailVisible() then pmRefresh() end
	old_state.aquaticTailVisible=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

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

	model.Body.LeggingsTopTrimF:setRot(vec( wave(val-1, per, 4), 0, 0 ))
	model.Body.LeggingsTopTrimB:setRot(vec( wave(val-1, per, 4), 0, 0 ))
	TAIL_BONES[1]:setRot(vec( wave(val-1, per, 7), 0, 0 ))
	TAIL_BONES[2]:setRot(vec( wave(val-2, per, 8), 0, 0 ))
	TAIL_BONES[3]:setRot(vec( wave(val-3, per, 12), 0, 0 ))
	TAIL_BONES[4]:setRot(vec( wave(val-4, per, 15), 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, v 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

anim_tick=0
anim_cycle=0
old_state.anim_cycle=0

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

		if aquaticTailVisible() then
			old_state.anim_cycle=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
			anim_cycle=anim_cycle + (player_speed*factor+0.75)
			-- bubble animation would go here but i don't have that (yet)
		end

	else
		old_state.anim_cycle=anim_cycle
		anim_cycle=anim_cycle+1
	end
end



-- }}}

-- initialize values -- {{{
function player_init()
	local_state.health=player:getHealth()
	old_state.health=local_state.health
	-- TODO possibly reconsider if this should be redone
	-- actually it's probably fine, it's jsut here because i forget visibility settings
	-- local all_parts=recurseModelGroup(model)

	-- for k, v in pairs(all_parts) do
	-- 	v:setVisible(nil)
	-- end
	setLocalState()
	pmRefresh()
	syncState()
	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
anim_tick=0
-- }}}

-- Tick function -- {{{
function hostTick()
	local_state.health=player:getHealth()
	if local_state.health ~= old_state.health then
		if local_state.health < old_state.health then
			ping.oof(local_state.health)
		end
		syncState()
	end
end

function tick()
	color_check=player:isInLava() ~= (player:getDimensionName()=="minecraft:the_nether")
	if old_state.color_check~=color_check then
		setColor()
	end
	-- optimization, only execute these once a second --
	if world.getTimeOfDay() % 20 == 0 then

		if player:getPose() == "SLEEPING" then
			if cooldown(20*4, "snore") then
				snore()
			end
		end

		-- Sync state every 10 seconds
		if world.getTimeOfDay() % (20*10) == 0 then
			syncState()
		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
	armor()
	-- Implements tail cooldown conditions
	updateTailVisibility()

	-- Animation code resides in this function
	animateTick()

	-- Check for queued PartsManager refresh
	doPmRefresh()
	-- End of tick --
	old_state.health=player:getHealth()
	old_state.color_check=color_check
	local_state.anim=player:getPose()
end
events.TICK:register(function() if player then tick() end end, "main_tick")
-- }}}

-- Render function {{{
function render(delta)
	if aquaticTailVisible() then
		animateMTail((lerp(old_state.anim_cycle, anim_cycle, delta) * 0.2))
	else
		resetAngles(model.Body)
		-- resetAngles(vanilla_model.BODY)
		-- resetAngles(vanilla_model.JACKET)
		-- resetAngles(armor_model.CHESTPLATE)
		animateTail((lerp(old_state.anim_cycle, 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")
-- }}}