--[[

	Tower Crane Mod
	===============

	v0.05 by JoSt

	Copyright (C) 2017 Joachim Stolberg
	LGPLv2.1+
	See LICENSE.txt for more information

	History:
	2017-06-04  v0.01  first version
	2017-06-06  v0.02  Hook bugfix
	2017-06-07  v0.03  fixed 2 bugs, added config.lua and sound
	2017-06-08  v0.04  recipe and rope length now configurable
	2017-06-10  v0.05  resizing bugfix, area protection added

]]--


towercrane = {}

dofile(minetest.get_modpath("towercrane") .. "/config.lua")

--##################################################################################################
--##  Tower Crane Hook
--##################################################################################################
local hook = {
	physical = true,
	collisionbox = {-0.2, -0.2, -0.2, 0.2, 0.2, 0.2},
	collide_with_objects = false,
	visual = "cube",
	visual_size = {x=0.6, y=0.6},
	textures = {
		"towercrane_hook.png",
		"towercrane_hook.png",
		"towercrane_hook.png",
		"towercrane_hook.png",
		"towercrane_hook.png",
		"towercrane_hook.png",
	},
	groups = {cracky=1},
	-- local variabels
	driver = nil,
	speed_forward=0,
	speed_right=0,
	speed_up=0,
	sound=nil,
}

----------------------------------------------------------------------------------------------------
-- Enter/leave the Hook
----------------------------------------------------------------------------------------------------
function hook:on_rightclick(clicker)
	local name = clicker:get_player_name()
	if self.driver and clicker == self.driver then  -- leave?
		clicker:set_detach()
		default.player_attached[name] = false
		default.player_set_animation(clicker, "stand" , 10)
		self.driver = nil
		if self.sound ~= nil then
			minetest.sound_stop(self.sound)
			self.sound = nil
		end
	elseif not self.driver then                     -- enter?
		self.driver = clicker
		clicker:set_attach(self.object, "", {x=0,y=15,z=-3}, {x=0,y=0,z=0})
		default.player_attached[name] = true
		default.player_set_animation(clicker, "sit" , 10)
	end
end

----------------------------------------------------------------------------------------------------
-- Hook control
----------------------------------------------------------------------------------------------------
function hook:on_step(dtime)
	-- remove hook from  last visit
	if self.pos1 == nil or self.pos2 == nil then
		self.object:remove()
		return
	end
	if self.driver then
		local ctrl = self.driver:get_player_control()
		local yaw = self.driver:get_look_horizontal()
		local pos = self.driver:getpos()
		local max_speed = 5
		local velocity = 0.5

		if ctrl.up then             -- forward
			self.speed_forward = math.min(self.speed_forward + velocity, max_speed)
		elseif ctrl.down then       -- backward
			self.speed_forward = math.max(self.speed_forward - velocity, -max_speed)
		elseif self.speed_forward > 0 then
			self.speed_forward = self.speed_forward - velocity
		elseif self.speed_forward < 0 then
			self.speed_forward = self.speed_forward + velocity
		end

		if ctrl.right then          -- right
			self.speed_right = math.min(self.speed_right + velocity, max_speed)
		elseif ctrl.left then       -- left
			self.speed_right = math.max(self.speed_right - velocity, -max_speed)
		elseif self.speed_right > 0 then
			self.speed_right = self.speed_right - velocity
		elseif self.speed_right < 0 then
			self.speed_right = self.speed_right + velocity
		end

		if ctrl.jump then           -- up
			self.speed_up = math.min(self.speed_up + velocity, 5)
		elseif ctrl.sneak then      -- down
			self.speed_up = math.max(self.speed_up - velocity, -5)
		elseif self.speed_up > 0 then
			self.speed_up = self.speed_up - velocity
		elseif self.speed_up < 0 then
			self.speed_up = self.speed_up + velocity
		end

		-- calculate the direction vector
		local vx = math.cos(yaw+math.pi/2) * self.speed_forward + math.cos(yaw) * self.speed_right
		local vz = math.sin(yaw+math.pi/2) * self.speed_forward + math.sin(yaw) * self.speed_right

		-- check if outside of the construction area
		if pos.x < self.pos1.x then vx= velocity end
		if pos.x > self.pos2.x then vx= -velocity end
		if pos.y < self.pos1.y then self.speed_up=  velocity end
		if pos.y > self.pos2.y then self.speed_up= -velocity end
		if pos.z < self.pos1.z then vz=  velocity end
		if pos.z > self.pos2.z then vz= -velocity end

		-- sound control
		if vx ~= 0 or vz ~= 0 or self.speed_up ~= 0 then
			if self.sound == nil then
				self.sound = minetest.sound_play({name="crane"},{object=self.object,
												 gain=towercrane.gain, max_hear_distance=20,
												 loop=true})
			end
		elseif self.sound ~= nil then
			minetest.sound_stop(self.sound)
			self.sound = nil
		end

		self.object:setvelocity({x=vx, y=self.speed_up,z=vz})
	else
		self.object:setvelocity({x=0, y=0,z=0})
	end
end

----------------------------------------------------------------------------------------------------
-- LuaEntitySAO (non-player moving things): see http://dev.minetest.net/LuaEntitySAO
----------------------------------------------------------------------------------------------------
minetest.register_entity("towercrane:hook", hook)



--##################################################################################################
--##  Tower Crane
--##################################################################################################

local function turnright(dir)
	local facedir = minetest.dir_to_facedir(dir)
	return minetest.facedir_to_dir((facedir + 1) % 4)
end

local function turnleft(dir)
	local facedir = minetest.dir_to_facedir(dir)
	return minetest.facedir_to_dir((facedir + 3) % 4)
end

----------------------------------------------------------------------------------------------------
-- Check space for mast and arm
----------------------------------------------------------------------------------------------------
local function check_space(pos, dir, height, width)
	for i = 1,height+2 do
		pos.y = pos.y + 1
		if minetest.get_node_or_nil(pos).name ~= "air" then
			return false
		end
	end

	pos.x = pos.x + dir.x*2
	pos.z = pos.z + dir.z*2
	for i = 1,width+3 do
		pos.x = pos.x + dir.x
		pos.z = pos.z + dir.z
		if minetest.get_node_or_nil(pos).name ~= "air" then
			return false
		end
	end
	return true
end

----------------------------------------------------------------------------------------------------
-- Constuct mast and arm
----------------------------------------------------------------------------------------------------
local function construct_crane(pos, dir, height, width, owner)
	pos.y = pos.y + 1
	minetest.env:add_node(pos, {name="towercrane:mast_ctrl_off", param2=minetest.dir_to_facedir(dir)})
	local meta = minetest.get_meta(pos)
	meta:set_string("dir", minetest.pos_to_string(dir))
	meta:set_string("owner", owner)
	meta:set_int("height", height)
	meta:set_int("width", width)

	for i = 1,height+1 do
		pos.y = pos.y + 1
		minetest.env:add_node(pos, {name="towercrane:mast"})
	end

	pos.y = pos.y - 2
	pos.x = pos.x - dir.x
	pos.z = pos.z - dir.z
	minetest.env:add_node(pos, {name="towercrane:arm2"})
	pos.x = pos.x - dir.x
	pos.z = pos.z - dir.z
	minetest.env:add_node(pos, {name="towercrane:arm"})
	pos.x = pos.x - dir.x
	pos.z = pos.z - dir.z
	minetest.env:add_node(pos, {name="towercrane:balance"})
	pos.x = pos.x + 3 * dir.x
	pos.z = pos.z + 3 * dir.z

	for i = 1,width do
		pos.x = pos.x + dir.x
		pos.z = pos.z + dir.z
		if i % 2 == 0 then
			minetest.env:add_node(pos, {name="towercrane:arm2"})
		else
			minetest.env:add_node(pos, {name="towercrane:arm"})
		end
	end
end

----------------------------------------------------------------------------------------------------
-- Remove the crane
----------------------------------------------------------------------------------------------------
local function remove_crane(pos, dir, height, width)
	pos.y = pos.y + 1
	minetest.env:remove_node(pos, {name="towercrane:mast_ctrl_off"})

	for i = 1,height+1 do
		pos.y = pos.y + 1
		minetest.env:remove_node(pos, {name="towercrane:mast"})
	end

	pos.y = pos.y - 2
	pos.x = pos.x - dir.x
	pos.z = pos.z - dir.z
	minetest.env:remove_node(pos, {name="towercrane:arm2"})
	pos.x = pos.x - dir.x
	pos.z = pos.z - dir.z
	minetest.env:remove_node(pos, {name="towercrane:arm"})
	pos.x = pos.x - dir.x
	pos.z = pos.z - dir.z
	minetest.env:remove_node(pos, {name="towercrane:balance"})
	pos.x = pos.x + 3 * dir.x
	pos.z = pos.z + 3 * dir.z

	for i = 1,width do
		pos.x = pos.x + dir.x
		pos.z = pos.z + dir.z
		if i % 2 == 0 then
			minetest.env:remove_node(pos, {name="towercrane:arm2"})
		else
			minetest.env:remove_node(pos, {name="towercrane:arm"})
		end
	end
end

----------------------------------------------------------------------------------------------------
-- Place the hook in front of the base
----------------------------------------------------------------------------------------------------
local function place_hook(pos, dir)
	pos.y = pos.y - 1
	pos.x = pos.x + dir.x
	pos.z = pos.z + dir.z
	return minetest.add_entity(pos, "towercrane:hook")
end


----------------------------------------------------------------------------------------------------
-- Check if the given construction area is not already protected
----------------------------------------------------------------------------------------------------
local function check_area(pos1, pos2, owner)
	if not areas then return true end
	for id, a in ipairs(areas:getAreasIntersectingArea(pos1, pos2)) do
		print(dump(a.owner))
		if a.owner ~= owner then
			return false
		end
	end
	return true
end

----------------------------------------------------------------------------------------------------
-- Calculate and set the protection area (pos1, pos2)
----------------------------------------------------------------------------------------------------
local function protect_area(pos, dir, height, width, owner)
	if not areas then return 0 end
	-- pos1 = close/right/below
	dir = turnright(dir)
	dir = turnright(dir)
	local pos1 = vector.add(pos, vector.multiply(dir, 2))
	dir = turnleft(dir)
	pos1 = vector.add(pos1, vector.multiply(dir, width/2))
	dir = turnleft(dir)
	pos1.y = pos.y - 2

	-- pos2 = far/left/above
	local pos2 = vector.add(pos1, vector.multiply(dir, width+2))
	dir = turnleft(dir)
	pos2 = vector.add(pos2, vector.multiply(dir, width))
	pos2.y = pos.y + 2 + height

	-- add area
	local canAdd, errMsg = areas:canPlayerAddArea(pos1, pos2, owner)
	if canAdd then
		local id = areas:add(owner, owner .. "'s construction site", pos1, pos2, nil)
		areas:save()
		return id
	end
	return nil
end

----------------------------------------------------------------------------------------------------
-- Remove the protection area
----------------------------------------------------------------------------------------------------
local function remove_area(id, owner)
	if not areas then return end
	if areas:isAreaOwner(id, owner) then
		areas:remove(id)
		areas:save()
	end
end

----------------------------------------------------------------------------------------------------
-- Check user input (height, width)
----------------------------------------------------------------------------------------------------
local function check_input(fields)
	local size = string.split(fields.size, ",")
	if #size == 2  then
		local height = tonumber(size[1])
		local width = tonumber(size[2])
		if height ~= nil and width ~= nil then
			height = math.max(height, 8)
			height = math.min(height, towercrane.max_height)
			width = math.max(width, 8)
			width = math.min(width, towercrane.max_width)
			return height, width
		end
	end
	return 0, 0
end

----------------------------------------------------------------------------------------------------
-- Register Crane base
----------------------------------------------------------------------------------------------------
minetest.register_node("towercrane:base", {
	description = "Tower Crane Base",
	inventory_image = "towercrane_invent.png",
	tiles = {
		"towercrane_base_top.png",
		"towercrane_base.png",
		"towercrane_base.png",
		"towercrane_base.png",
		"towercrane_base.png",
		"towercrane_base.png",
	},
	paramtype2 = "facedir",
	is_ground_content = false,
	groups = {cracky=2},
	formspec = set_formspec,

	-- set meta data (form for crane height and width, dir of the arm)
	after_place_node = function(pos, placer)
		local meta = minetest.get_meta(pos)
		local owner = placer:get_player_name()
		meta:set_string("owner", owner)
		local formspec = "size[5,4]"..
			"label[0,0;Construction area size]" ..
			"field[1,1.5;3,1;size;height,width;]" ..
			"button_exit[1,2;2,1;exit;Save]"
		meta:set_string("formspec", formspec)

		local fdir = minetest.dir_to_facedir(placer:get_look_dir(), false)
		local dir = minetest.facedir_to_dir(fdir)
		meta:set_string("dir", minetest.pos_to_string(dir))
	end,

	-- evaluate user input (height, width), destroyed old crane and build a new one with
	-- the given size
	on_receive_fields = function(pos, formname, fields, player)
		if fields.size == nil then
			return
		end
		local meta = minetest.get_meta(pos)
		local owner = meta:get_string("owner")
		local dir = minetest.string_to_pos(meta:get_string("dir"))
		local height = meta:get_int("height")
		local width = meta:get_int("width")
		local id = meta:get_int("id")

		if not player or not player:is_player() then
			return
		end
		if player:get_player_name() ~= owner then
			return
		end
		-- destroy area and crane
		if dir ~= nil and height ~= nil and width ~= nil then
			remove_area(id, owner)
			remove_crane(table.copy(pos), dir, height, width)
		end

		-- evaluate user input
		height, width = check_input(fields)
		if height ~= 0 then
			meta:set_int("height", height)
			meta:set_int("width", width)
			meta:set_string("infotext", "Crane size: " .. height .. "," .. width)
			if dir ~= nil then
				if check_space(table.copy(pos), dir, height, width) then
					-- add protection area
					local id = protect_area(table.copy(pos), table.copy(dir), height, width, owner)
					if id ~= nil then
						meta:set_int("id", id)
						construct_crane(table.copy(pos), table.copy(dir), height, width, owner)
					else
						minetest.chat_send_player(owner, "Construction area is already protected!")
					end
				else
					minetest.chat_send_player(owner, "Too less space to raise up the tower crane!")
				end
			end
		end
	end,

	-- remove mast and arm if base gets destroyed
	on_destruct = function(pos)
		local meta = minetest.get_meta(pos)
		local dir = minetest.string_to_pos(meta:get_string("dir"))
		local height = meta:get_int("height")
		local width = meta:get_int("width")
		local id = meta:get_int("id")
		local owner = meta:get_string("owner")

		-- remove protection area
		if id ~= nil then
			remove_area(id, owner)
		end
		-- remove crane
		if dir ~= nil and height ~= nil and width ~= nil then
		   remove_crane(pos, dir, height, width)
		end
		-- remove hook
		id = minetest.hash_node_position(pos)
		if towercrane.id then
			towercrane.id:remove()
			towercrane.id = nil
		end
	end,
})

----------------------------------------------------------------------------------------------------
-- Register Crane balance
----------------------------------------------------------------------------------------------------
minetest.register_node("towercrane:balance", {
	description = "Tower Crane Balance",
	tiles = {
		"towercrane_base.png",
		"towercrane_base.png",
		"towercrane_base.png",
		"towercrane_base.png",
		"towercrane_base.png",
		"towercrane_base.png",
	},
	paramtype2 = "facedir",
	is_ground_content = false,
	groups = {crumbly=0, not_in_creative_inventory=1},
})

----------------------------------------------------------------------------------------------------
-- Register Crane mast
----------------------------------------------------------------------------------------------------
minetest.register_node("towercrane:mast", {
	description = "Tower Crane Mast",
	drawtype = "glasslike_framed",
	tiles = {
		"towercrane_mast.png",
		"towercrane_mast.png",
		"towercrane_mast.png",
		"towercrane_mast.png",
		"towercrane_mast.png",
		"towercrane_mast.png",
	},
	paramtype2 = "facedir",
	is_ground_content = false,
	groups = {crumbly=0, not_in_creative_inventory=1},
})

----------------------------------------------------------------------------------------------------
-- Register Crane Switch (on)
----------------------------------------------------------------------------------------------------
minetest.register_node("towercrane:mast_ctrl_on", {
	description = "Tower Crane Mast Ctrl On",
	drawtype = "node",
	tiles = {
		"towercrane_mast_ctrl.png",
		"towercrane_mast_ctrl.png",
		"towercrane_mast_ctrl_on.png",
		"towercrane_mast_ctrl_on.png",
		"towercrane_mast_ctrl.png",
		"towercrane_mast_ctrl.png",
	},
	-- switch the crane OFF
	on_rightclick = function (pos, node, clicker)
		local meta = minetest.get_meta(pos)
		if not clicker or not clicker:is_player() then
			return
		end
		if clicker:get_player_name() ~= meta:get_string("owner") then
			return
		end
		node.name = "towercrane:mast_ctrl_off"
		minetest.swap_node(pos, node)

		local id = minetest.hash_node_position(pos)
		if towercrane.id then
			towercrane.id:remove()
			towercrane.id = nil
		end
	end,

	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		meta:set_string("infotext", "Switch crane on/off")
	end,

	after_place_node = function(pos, placer, itemstack, pointed_thing)
		local meta = minetest.get_meta(pos)
		local owner = placer:get_player_name()
		meta:set_string("owner", owner)
	end,

	paramtype2 = "facedir",
	is_ground_content = false,
	groups = {crumbly=0, not_in_creative_inventory=1},
})

----------------------------------------------------------------------------------------------------
-- Register Crane Switch (off)
----------------------------------------------------------------------------------------------------
minetest.register_node("towercrane:mast_ctrl_off", {
	description = "Tower Crane Mast Ctrl Off",
	drawtype = "node",
	tiles = {
		"towercrane_mast_ctrl.png",
		"towercrane_mast_ctrl.png",
		"towercrane_mast_ctrl_off.png",
		"towercrane_mast_ctrl_off.png",
		"towercrane_mast_ctrl.png",
	  "towercrane_mast_ctrl.png",
	},
	-- switch the crane ON
	on_rightclick = function (pos, node, clicker)
		-- calculate the construction area, and place the hook
		local meta = minetest.get_meta(pos)
		-- only the owner is allowed to switch
		if not clicker or not clicker:is_player() then
			return
		end
		if clicker:get_player_name() ~= meta:get_string("owner") then
			return
		end
		-- swap to the other node
		node.name = "towercrane:mast_ctrl_on"
		minetest.swap_node(pos, node)
		local dir = minetest.string_to_pos(meta:get_string("dir"))
		if pos ~= nil and dir ~= nil then
			-- store hook instance in 'towercrane'
			local id = minetest.hash_node_position(pos)
			towercrane.id = place_hook(table.copy(pos), dir)

			--
			-- calculate the construction area dimension (pos1, pos2)
			--
			local height = meta:get_int("height")
			local width = meta:get_int("width")

			-- pos1 = close/right/below
			dir = turnright(dir)
			local pos1 = vector.add(pos, vector.multiply(dir, width/2))
			dir = turnleft(dir)
			local pos1 = vector.add(pos1, vector.multiply(dir, 1))
			pos1.y = pos.y - 2 + height - towercrane.rope_length

			-- pos2 = far/left/above
			local pos2 = vector.add(pos1, vector.multiply(dir, width-1))
			dir = turnleft(dir)
			pos2 = vector.add(pos2, vector.multiply(dir, width))
			pos2.y = pos.y - 3 + height

			-- normalize x/z so that pos2 > pos1
			if pos2.x < pos1.x then
				pos2.x, pos1.x = pos1.x, pos2.x
			end
			if pos2.z < pos1.z then
				pos2.z, pos1.z = pos1.z, pos2.z
			end

			-- store pos1/pos2 in the hook (LuaEntitySAO)
			towercrane.id:get_luaentity().pos1 = pos1
			towercrane.id:get_luaentity().pos2 = pos2
		end
	end,

	on_construct = function(pos)
		-- add infotext
		local meta = minetest.get_meta(pos)
		meta:set_string("infotext", "Switch crane on/off")
	end,

	after_place_node = function(pos, placer, itemstack, pointed_thing)
		-- store owner for dig protection
		local meta = minetest.get_meta(pos)
		local owner = placer:get_player_name()
		meta:set_string("owner", owner)
	end,

	paramtype2 = "facedir",
	is_ground_content = false,
	groups = {crumbly=0, not_in_creative_inventory=1},
})

----------------------------------------------------------------------------------------------------
-- Register Crane arm 1
----------------------------------------------------------------------------------------------------
minetest.register_node("towercrane:arm", {
	description = "Tower Crane Arm",
	drawtype = "glasslike_framed",
	tiles = {
		"towercrane_arm.png",
		"towercrane_arm.png",
		"towercrane_arm.png",
		"towercrane_arm.png",
		"towercrane_arm.png",
		"towercrane_arm.png",
	},
	paramtype2 = "facedir",
	is_ground_content = false,
	groups = {crumbly=0, not_in_creative_inventory=1},
})

----------------------------------------------------------------------------------------------------
-- Register Crane arm 2
----------------------------------------------------------------------------------------------------
minetest.register_node("towercrane:arm2", {
	description = "Tower Crane Arm2",
	drawtype = "glasslike_framed",
	tiles = {
		"towercrane_arm2.png",
		"towercrane_arm2.png",
		"towercrane_arm2.png",
		"towercrane_arm2.png",
		"towercrane_arm2.png",
		"towercrane_arm2.png",
	},
	paramtype2 = "facedir",
	is_ground_content = false,
	groups = {crumbly=0, not_in_creative_inventory=1},
})


----------------------------------------------------------------------------------------------------
-- Register Recipe
----------------------------------------------------------------------------------------------------
if towercrane.recipe then
	minetest.register_craft({
		output = "towercrane:base",
		recipe = {
			{"default:steel_ingot", "default:steel_ingot", "default:steel_ingot"},
			{"default:steel_ingot", "", ""},
			{"default:steel_ingot", "dye:yellow", ""}
		}
	})
end