--Translator
local S = core.get_translator("shapetester")

--Sparks texture setting
local default_sparks_texture = core.settings:get("shapetester_default_sparks_texture") or "shapetester_particle.png"

--Shapeshift sound setting
local default_shapeshift_sound = core.settings:get("shapetester_default_sound") or "shapetester_shift"

--Tool wear settings
local tool_wear = core.settings:get("shapetester_tool_wear") or "none"
local wear_amount = core.settings:get("shapetester_tool_max_uses") or 256

--Menu style settings
local shapemenu_style = core.settings:get("shapetester_menu_style") or "shapetester"
local custom_menu_style = core.settings:get("shapetester_custom_menu_style") or "shapetester_menu.png"

--Reset button setting
local reset_button = core.settings:get("shapetester_menu_reset") or "reset"

--Debug settings
local log_aux1 = core.settings:get_bool("shapetester_aux1_log") or false
local log_transform = core.settings:get_bool("shapetester_transform_log") or true

--Global variables
shapetester.registered_shapeshifts = {}

--Other variables
local indexed_shapeshifts = {}
shapetester.previewing_shapeshift = {}
shapetester.menu_pages = {}
local columns = 5
local rows = 3
local shapeshifts_per_page = columns * rows

--Wieldhand
local function register_wieldhand(name,def)
    core.register_item(name, {
        type = "none",
        range = def.range or 3.5,
        wield_image = def.wield_image or "wieldhand.png",
        wield_scale = def.wield_scale or {x=1,y=1,z=2.5},
        tool_capabilities = def.tool_capabilities or {
      		full_punch_interval = 0.9,
      		max_drop_level = 0,
      		groupcaps = {
      			crumbly = {times={[2]=3.00, [3]=0.70}, uses=0, maxlevel=1},
      			snappy = {times={[3]=0.40}, uses=0, maxlevel=1},
      			oddly_breakable_by_hand = {times={[1]=3.50,[2]=2.00,[3]=0.70}, uses=0}
      		},
      		damage_groups = {fleshy=1},
      	}
    })
end

--Index shapeshifts
core.register_on_mods_loaded(function()
    for name, def in pairs(shapetester.registered_shapeshifts) do
        if def.shown_in_shapeshift_tool == true then
            table.insert(indexed_shapeshifts, name)
        end
    end
    table.sort(indexed_shapeshifts)
end)

--Local sethand
local function sethand(player,handname)
    player:get_inventory():set_size("hand", 1)
    player:get_inventory():set_stack("hand", 1, handname)
    return true
end

--Local reset_hand
local function reset_hand(player)
    player:get_inventory():set_size("hand", 0)
    return true
end

--Register shapeshift
shapetester.register_shapeshift = function(name,def)
    if not shapetester.registered_shapeshifts[name] then
        shapetester.registered_shapeshifts[name] = def --Add shapeshift to global shapetester.registered_shapeshifts variable
        def.title = def.title or def.description
        def.description = def.description or ""
        if def.animation == nil then
            def.animation = {}
        end
        if def.aux1_cooldown == nil and def.aux1_special ~= nil then
            def.aux1_cooldown = 1
        else
            def.aux1_cooldown = def.aux1_cooldown
        end
        def.aux1_special = def.aux1_special or nil
        def.fly = def.fly or false
        def.fly_trail = def.fly_trail or false
        def.fly_trail_texture = def.fly_trail_texture or ""
        def.max_hp = def.max_hp or 20
        def.animation.idle = def.animation.idle or {x=0,y=0}
        def.animation.walk = def.animation.walk or {x=0,y=0}
        def.animation.dig = def.animation.dig or {x=0,y=0}
        def.animation.walk_while_dig = def.animation.walk_while_dig or {x=0,y=0}
        def.animation.frame_speed = def.animation.frame_speed or 30
        def.textures = def.textures or {}
        def.visual = def.visual or {x=1,y=1,z=1}
        if def.visual == "mesh" then
            def.mesh = def.mesh
        end
        def.stepheight = def.stepheight
        def.collisionbox = def.collisionbox
        def.eye_height = def.eye_height
        def.visual_size = def.visual_size or {x=1,y=1,z=1}
        def.shown_in_shapeshift_tool = def.shown_in_shapeshift_tool or false
        def.gravity = def.gravity or 1
        def.speed = def.speed or 1
        def.jump = def.jump or 1
        --Hand
        if core.is_creative_enabled("") then
            local digtime = 42
	        local caps = {times = {digtime, digtime, digtime}, uses = 0, maxlevel = 256}

            local creative_mining = {
	        	full_punch_interval = 0.5,
	        	max_drop_level = 3,
	        	groupcaps = {
	        		crumbly = caps,
	        		cracky  = caps,
	        		snappy  = caps,
	        		choppy  = caps,
	        		oddly_breakable_by_hand = caps,
	        		dig_immediate =
	        			{times = {[2] = digtime, [3] = 0}, uses = 0, maxlevel = 256},
	        	},
	        	damage_groups = {fleshy = 10},
	        }
            register_wieldhand(name.."_hand", {
                wield_image = def.hand.texture,
                wield_scale = def.hand.scale,
                range = 10,
                tool_capabilities = creative_mining
            })
        else
            register_wieldhand(name.."_hand", {
                wield_image = def.hand.texture,
                wield_scale = def.hand.scale,
                tool_capabilities = def.hand.tool_capabilities
            })
        end
        def.custom_transform_noise = def.custom_transform_noise or false
        if def.custom_transform_noise == true then
            def.transform_noise = def.transform_noise
        end
    else
        core.log("error","[shapetester] Failed to register shapeshift! Shapeshift already exists!")
    end
end

shapetester.unregister_shapeshift = function(shapeshift)
    local shape = shapetester.registered_shapeshifts[shapeshift]
    if shape then
        shapetester.registered_shapeshifts[shapeshift] = nil
    else
        core.log("error","[shapetester] Unregister failed! Shapeshift doesn't exist!")
    end
end

shapetester.override_shapeshift = function(shapeshift,new_def)
    if shapetester.registered_shapeshifts[shapeshift] then
        shapetester.registered_shapeshifts[shapeshift] = new_def
    else
        core.log("error","[shapetester] Override failed! Shapeshift doesn't exist!")
    end
end

--Manually transforming function
shapetester.transform = function(player_name,shapeshift)
    local player = core.get_player_by_name(player_name)
    local hp_fraction = player:get_hp()/player:get_properties().hp_max
    for name,def in pairs(shapetester.registered_shapeshifts) do
        if name == shapeshift then
            if shapetester.registered_shapeshifts[shapeshift] then
                player:set_properties({
		            textures = def.textures,
		            visual = def.visual,
                    mesh = def.mesh,
		            stepheight = def.stepheight,
	                collisionbox = def.collisionbox,
	                eye_height = def.eye_height,
                    visual_size = def.visual_size,
                    hp_max = def.max_hp,
                })
                player:set_local_animation(
                    def.animation.idle,
                    def.animation.walk,
                    def.animation.dig,
                    def.animation.walk_while_dig,
                    def.animation.frame_speed
                )
                player:set_physics_override({gravity = def.gravity,speed = def.speed,jump = def.jump})
                sethand(player,name.."_hand")
                player:set_pos({x=player:get_pos().x,y=player:get_pos().y+1,z=player:get_pos().z})
                player:get_meta():set_string("shapetester_last_shapeshift",shapeshift)
                new_current_hp = hp_fraction * player:get_properties().hp_max
                if new_current_hp < 1 then
                    new_current_hp = math.ceil(new_current_hp)
                end
                player:set_hp(new_current_hp)
                if log_transform == true then
                    core.log("action","[shapetester] " ..player_name.." shapeshifts into "..shapeshift)
                end
            else
                core.log("error","[shapetester] Transformation failed! Shapeshift doesn't exist!")
            end
        end
    end
end

--Reseting model
shapetester.reset = function(player)
    if core.get_modpath("player_api") then
        player:set_properties({
	    	textures = {"character.png"},
	    	visual = "mesh",
            mesh = "character.b3d",
	    	collisionbox = {-0.3, 0.0, -0.3, 0.3, 1.7, 0.3},
	        stepheight = 0.6,
	        eye_height = 1.47,
            visual_size = {x = 1, y = 1, z = 1},
            hp_max = 20,
	    })
        player:set_physics_override({gravity = 1,speed = 1,jump = 1})
        player:set_local_animation(
            {x = 0,   y = 79},
            {x = 168, y = 187},
            {x = 189, y = 198},
            {x = 200, y = 219},
            30
        )
        reset_hand(player)
        player:get_meta():set_string("shapetester_last_shapeshift","player")
        local player_name = player:get_player_name()
    else
        shapetester.complete_reset(player)
    end
    local player_name = player:get_player_name()
    core.log("action","[shapetester] " ..player_name.." model reset")
end

--Completely reset model
shapetester.complete_reset = function(player)
    player:set_properties({
		textures = {"player.png","player_back.png"},
		visual = "upright_sprite",
        collisionbox = {-0.3, 0.0, -0.3, 0.3, 1.75, 0.3},
	    stepheight = 0.6,
	    eye_height = 1.625,
        visual_size = {x = 1, y = 2},
        hp_max = 20,
	})
    player:set_physics_override({gravity = 1,speed = 1,jump = 1})
    player:set_local_animation(
        {x = 0,   y = 0},
        {x = 0, y = 0},
        {x = 0, y = 0},
        {x = 0, y = 0},
        0
    )
    reset_hand(player)
    player:get_meta():set_string("shapetester_last_shapeshift","base_player")
    local player_name = player:get_player_name()
    core.log("action","[shapetester] " ..player_name.." model completely reset")
end

--Sparks
shapetester.sparks = function(player,texture)
    local pos = player:get_pos()
	core.add_particlespawner({
		amount = 100,
		time = 0.5,
		minpos = pos,
		maxpos = {x=pos.x,y=pos.y+1,z=pos.z},
		minvel = {x=2, y=0.2, z=2},
		maxvel = {x=-2, y=2, z=-2},
		minacc = {x=0, y=-6, z=0},
		maxacc = {x=0, y=-10, z=0},
		minexptime = 0.2,
		maxexptime = 1,
		minsize = 0.3,
		maxsize = 2.5,
		collisiondetection = true,
		texture = texture or "shapetester_particle.png",
        glow = 14,
	})
end

--The cube (for experimenting) only shown in shapeshift tool if true in settings
if core.settings:get_bool("shapetester_cube") == true then
    shapetester.register_shapeshift("shapetester:cube",{
        title = S("Cube"),
        textures = {"shapetester_cube.png"},
        visual = "mesh",
        mesh = "shapetester_cube.obj",
        stepheight = 0.6,
        collisionbox = {-0.5, -0.5, -0.5, 0.5, 0.475, 0.5},
        eye_height = 0.25,
        visual_size = {x = 1, y = 1, z = 1},
        gravity = 1,
        max_hp = 10,
        speed = 1,
        jump = 1,
        shown_in_shapeshift_tool = true,
        aux1_cooldown = 4,
        sparks_textures = "shapetester_hand_cube.png",
        aux1_special = function(player,name,def)
            player:set_physics_override({speed = def.speed+2})
            core.after(0.5, function(player)player:set_physics_override({speed = def.speed})end, player)
        end,
        special_desc = S("Dash"),
        animation = {
            idle = {x = 0,   y = 0},
            walk = {x = 0, y = 0},
            dig = {x = 0, y = 0},
            walk_while_dig = {x = 0, y = 0},
            frame_speed = 30
        },
        hand = {
            texture = "shapetester_hand_cube.png",
            scale = {x=1,y=1,z=1},
            tool_capabilities = {
    		    full_punch_interval = 0.9,
    		    max_drop_level = 0,
    		    groupcaps = {
    		    	crumbly = {times={[2]=3.00, [3]=0.70}, uses=0, maxlevel=1},
    		    	snappy = {times={[3]=0.40}, uses=0, maxlevel=1},
    		    	oddly_breakable_by_hand = {times={[1]=3.50,[2]=2.00,[3]=0.70}, uses=0}
    		    },
    		    damage_groups = {fleshy=1},
    	    }
        }
    })
end

--Setting player to last shapeshift when joined (if player is the value then shapetester.reset)
core.register_on_joinplayer(function(player)
    if player:get_meta():get("shapetester_last_shapeshift") == nil then
        player:get_meta():set_string("shapetester_last_shapeshift","player")
    end
    if player:get_meta():get_string("shapetester_last_shapeshift") == "player" or player:get_meta():get_string("shapetester_last_shapeshift") == "custom" then
        shapetester.reset(player)
    elseif player:get_meta():get_string("shapetester_last_shapeshift") == "base_player" then
        shapetester.complete_reset(player)
    else
        shapetester.transform(player:get_player_name(),player:get_meta():get_string("shapetester_last_shapeshift"))
    end
end)

core.register_on_dieplayer(function(player)
    local pos = player:get_pos()
    player:set_pos({x = pos.x,y = pos.y+1,z = pos.z})
    shapetester.reset(player)
end)

--Heavily modified cinventory code -- thanks to the author of cinventory
--########################################

function shapetester.get_total_pages()
    return math.ceil(#indexed_shapeshifts / shapeshifts_per_page)
end

function shapetester.get_formspec(player)
    local player_name = player:get_player_name()
    local current_page = shapetester.menu_pages[player_name] or 1
    local total_pages = shapetester.get_total_pages()
    if not shapetester.previewing_shapeshift[player_name] then
        shapetester.previewing_shapeshift[player_name] = "none"
    end
    local model = ""
    local texture = ""
    local title = ""
    local jump = ""
    local speed = ""
    local gravity = ""
    local max_hp = ""
    local damage = ""
    local special_desc = ""
    if shapetester.registered_shapeshifts[shapetester.previewing_shapeshift[player_name]] then
        texture = shapetester.registered_shapeshifts[shapetester.previewing_shapeshift[player_name]].textures[1]
        title = shapetester.registered_shapeshifts[shapetester.previewing_shapeshift[player_name]].title
        jump = tostring(shapetester.registered_shapeshifts[shapetester.previewing_shapeshift[player_name]].jump)
        speed = tostring(shapetester.registered_shapeshifts[shapetester.previewing_shapeshift[player_name]].speed)
        gravity = tostring(shapetester.registered_shapeshifts[shapetester.previewing_shapeshift[player_name]].gravity)
        max_hp = tostring(shapetester.registered_shapeshifts[shapetester.previewing_shapeshift[player_name]].max_hp)
        damage = tostring(shapetester.registered_shapeshifts[shapetester.previewing_shapeshift[player_name]].hand.tool_capabilities.damage_groups.fleshy)
        if shapetester.registered_shapeshifts[shapetester.previewing_shapeshift[player_name]].special_desc then
            special_desc = shapetester.registered_shapeshifts[shapetester.previewing_shapeshift[player_name]].special_desc
        end
        if shapetester.registered_shapeshifts[shapetester.previewing_shapeshift[player_name]].visual == "mesh" then
            model = shapetester.registered_shapeshifts[shapetester.previewing_shapeshift[player_name]].mesh
        end
    end

    local formspec = "size[15.5,4.5]" ..
        "label[0.5,0.2;"..S("Click on a button to transform")..":]" ..
        "label[4,4.25;"..S("Page").." "..current_page.." "..S("of").." "..total_pages.."]"..
        "label[10,2.4;"..S("Name")..":"..title.."]"..
        "label[10,2.7;"..S("Jump")..":"..jump.."]"..
        "label[10,3;"..S("Speed")..":"..speed.."]"..
        "label[10,3.3;"..S("Gravity")..":"..gravity.."]"..
        "label[10,3.6;"..S("Health")..":"..max_hp.."]"..
        "label[13,2.4;"..S("Damage")..":"..damage.."]"

    if special_desc ~= "" then
        formspec = formspec.."label[13,2.7;".."Aux1"..":"..special_desc.."]"
    end
    if shapetester.registered_shapeshifts[shapetester.previewing_shapeshift[player_name]] then
        if shapetester.registered_shapeshifts[shapetester.previewing_shapeshift[player_name]].visual == "mesh" then
            formspec = formspec.."model[10.75,-1;4,4;preview;"..model..";"..texture..";0,-180;true;false;]"
        else
            formspec = formspec.."image[11.5,0.5;2,2;"..texture.."]"
        end
    else
        formspec = formspec.."label[10.75,1;"..S("Select a shapeshift to preview").."]"
    end
    local start_index = (current_page - 1) * shapeshifts_per_page + 1
    local end_index = math.min(current_page * shapeshifts_per_page, #indexed_shapeshifts)

    for i = start_index, end_index do
        local shapeshift = indexed_shapeshifts[i]
        local rel = i - start_index
        local col = rel % columns
        local row = math.floor(rel / columns)
        local x = 0.5 + col * 1.7
        local y = 0.7 + row * 1.1
        local name = shapetester.registered_shapeshifts[shapeshift].title
        formspec = formspec ..
            string.format("button[%f,%f;1.6,0.8;transform_%s;%s]",
                x, y,
                core.formspec_escape(shapeshift),
                core.formspec_escape(name))
    end
    
    if #indexed_shapeshifts == 0 then
        formspec = formspec .. "label[3.5,2;"..S("No shapeshifts Found!").."]"
    end

    formspec = formspec .. "image_button[0.675,4.125;1,0.75;shapetester_previous.png;prev;]"
    formspec = formspec .. "image_button[1.5,4.125;1,0.75;shapeteseter_next.png;next;]"
    formspec = formspec .. "image_button_exit[9,-0.15;0.8,0.8;shapetester_close.png;exit;]"
    formspec = formspec .. "image_button[9,0.75;0.8,0.8;shapetester_shape_computer.png;tech_menu;]"
    formspec = formspec .. "image_button[9,1.65;0.8,0.8;shapetester_favourites.png;favourites;]"
    formspec = formspec .. "button[7,4.25;2,0.5;reset;"..S("Reset").."]"
    formspec = formspec .. "button[11.5,4.25;2,0.5;transform;"..S("Transform!").."]"
    if shapemenu_style == "shapetester" then
        formspec = formspec .. "background[0,0;0,0;shapetester_menu.png;true]"
    elseif shapemenu_style == "white" then
        formspec = formspec .. "background[0,0;0,0;shapetester_plain_menu.png;true]"
    elseif shapemenu_style == "grey" then
        formspec = formspec .. "background[0,0;0,0;shapetester_grey_menu.png;true]"
    elseif shapemenu_style == "mese" then
        formspec = formspec .. "background[0,0;0,0;shapetester_mese_menu.png;true]"
    elseif shapemenu_style == "custom" then
        formspec = formspec .. "background[0,0;0,0;"..custom_menu_style..";true]"
    end
    return formspec
end

core.register_tool("shapetester:shape_stone", {
    description = S("Shape-shift Stone"),
    inventory_image = "shapetester_shape_stone.png",
    on_use = function(itemstack,player)
        local name = player:get_player_name()
        shapetester.menu_pages[name] = 1
        local formspec = shapetester.get_formspec(player)
        core.show_formspec(name, "shapetester:form", formspec)
        if not core.is_creative_enabled("") then
            if tool_wear == "both" or tool_wear == "shape_stone" then 
                itemstack:add_wear_by_uses(wear_amount)
                return itemstack
            end
        end
    end
})

core.register_on_player_receive_fields(function(player, formname, fields)
    if formname ~= "shapetester:form" then
        return
    end

    local total_pages = shapetester.get_total_pages()
    if total_pages ~= 0 then
        if fields.next then
            local player_name = player:get_player_name()
            local current_page = shapetester.menu_pages[player_name] or 1
            if current_page < total_pages then
                if shapetester.menu_pages[player_name] then
                    shapetester.menu_pages[player_name] = current_page + 1
                end
            else
                if shapetester.menu_pages[player_name] then
                    shapetester.menu_pages[player_name] = 1
                end
            end
            core.show_formspec(player_name, "shapetester:form", shapetester.get_formspec(player))
            return true
        elseif fields.prev then
            local player_name = player:get_player_name()
            local current_page = shapetester.menu_pages[player_name] or 1
            if current_page > 1 then
                if shapetester.menu_pages[player_name] then
                    shapetester.menu_pages[player_name] = current_page - 1
                end
            else
                if shapetester.menu_pages[player_name] then
                    shapetester.menu_pages[player_name] = total_pages
                end
            end
            core.show_formspec(player_name, "shapetester:form", shapetester.get_formspec(player))
            return true
        end
    end

    for field, _ in pairs(fields) do
        if field:sub(1, 10) == "transform_" then
            local shiftname = field:sub(11)
            local player_name = player:get_player_name()
            shapetester.previewing_shapeshift[player_name] = shiftname
            core.show_formspec(player_name, "shapetester:form", shapetester.get_formspec(player))
            return true
        end
    end
    if fields.transform then
        local player_name = player:get_player_name()
        shapetester.transform(player_name,shapetester.previewing_shapeshift[player_name])
        for name,def in pairs(shapetester.registered_shapeshifts) do
            if name == shapetester.previewing_shapeshift[player_name] then
                core.sound_play(def.transform_noise or default_shapeshift_sound, {pos=player:get_pos(),gain=0.4,max_hear_distance = 16})
                if core.settings:get_bool("shapeshift_sparks") ~= false then
                    shapetester.sparks(player, def.sparks_textures or default_sparks_texture)
                end        
            end
        end
    end
    if fields.reset then
        if reset_button == "reset" then
            shapetester.reset(player)
        else
            shapetester.complete_reset(player)
        end
        if core.settings:get_bool("shapeshift_sparks") ~= false then
            shapetester.sparks(player)
        end
        core.sound_play(default_shapeshift_sound, {pos=player:get_pos(),gain=0.4,max_hear_distance = 16})
        player:set_pos({x=player:get_pos().x,y=player:get_pos().y+1,z=player:get_pos().z})
    end
    if fields.tech_menu then
        if core.check_player_privs(player, {shapeshift=true}) then
            local name = player:get_player_name()
            shapetester.show_shapeshift_tech_menu(name)
        else
            core.chat_send_player(player:get_player_name(), S("Shapetester -!- Insufficient privileges! (shapeshift)"))
        end
    end
    if fields.favourites then
        shapetester.show_favourites_form(player)
    end
end)
--End of Heavily modified cinventory code
--########################################

--Aux1 special (WIP)
local t = 0

local function aux1_func(...)
    t=t+...
    for _,player in pairs(core.get_connected_players()) do
        for name,def in pairs(shapetester.registered_shapeshifts) do
            local last_shift = player:get_meta():get("shapetester_last_shapeshift")
            if name == last_shift and def.aux1_cooldown and t > def.aux1_cooldown and player:get_player_control().aux1 and def.aux1_special then
                if type(def.aux1_special) == "function" then
                    def.aux1_special(player,name,def)
                    t=0
                    if log_aux1 == true then
                        core.log("action",player:get_player_name().." used aux1 special with shapeshift "..name)
                    end
                elseif log_aux1 == true then
                    core.log("error","Aux1 special failed! aux1_special must be a function value!")
                end
            end
        end
    end
end

core.register_globalstep(function(...)
    aux1_func(...)
end)

--Fly (WIP)
local function fly_trail(player,texture,glow)
    local pos = player:get_pos()
	core.add_particlespawner({
		amount = 1,
		time = 0.25,
		minpos = pos,
		maxpos = {x=pos.x,y=pos.y+0.1,z=pos.z},
		minvel = {x=2, y=-0.1, z=2},
		maxvel = {x=-2, y=0.1, z=-2},
		minexptime = 0.2,
		maxexptime = 1,
		minsize = 0.3,
		maxsize = 2.5,
		collisiondetection = true,
		texture = texture,
        glow = glow or 14,
	})
end

local function fly_func(...)
    for _,player in pairs(core.get_connected_players()) do
        local pos = player:get_pos()
        for name,def in pairs(shapetester.registered_shapeshifts) do
            local last_shift = player:get_meta():get("shapetester_last_shapeshift")
            if name == last_shift and def.fly and def.fly == true then
                if core.get_node({x=pos.x,y=pos.y-0.6,z=pos.z}).name == "air" then
                    if def.fly_trail == true then
                        fly_trail(player,def.fly_trail_texture,14)
                    end
                end
                if player:get_player_control().sneak then
                    if core.get_node({x=pos.x,y=pos.y-0.6,z=pos.z}).name == "air" then
                        player:add_pos({x=0,y=-0.5,z=0})
                    end
                elseif player:get_player_control().jump then
                    if core.get_node({x=pos.x,y=pos.y+0.6,z=pos.z}).name == "air" then
                        player:add_pos({x=0,y=0.5,z=0})
                    end
                end
            end
        end
    end
end

core.register_globalstep(function(...)
    fly_func(...)
end)