
-- used by pistons to move whole chunks of nodes
aom_wire.node_movement = {}
-- functions for tools that cause nodes to move together when pushed
aom_wire.welder = {}
aom_wire.welder.pl = {}
local pl = aom_wire.welder.pl

local has_node_updates = core.get_modpath("node_updates") ~= nil

local adjacent_map = {
	[tostring(vector.new(0,0,1))] = 1,
	[tostring(vector.new(0,0,-1))] = 2,
	[tostring(vector.new(1,0,0))] = 3,
	[tostring(vector.new(-1,0,0))] = 4,
	[tostring(vector.new(0,1,0))] = 5,
	[tostring(vector.new(0,-1,0))] = 6,
}
local adjacent = {
	(vector.new(0,0,1)),
	(vector.new(0,0,-1)),
	(vector.new(1,0,0)),
	(vector.new(-1,0,0)),
	(vector.new(0,1,0)),
	(vector.new(0,-1,0)),
}

function aom_wire.welder.check_player(player)
	local pi = pl[player]
    if not pi then
        pi = {}
		pl[player] = pi
    end
    return pi
end

local SM = {
	state_name = "",
}
function SM:on_step(host, dtime)
	if self.state and self.state.on_step then
		self.meta.state_time = self.meta.state_time + dtime
		self:set_state(host, self.state.on_step(host, self.meta, dtime))
	end
end
function SM:set_state(host, name)
	if not name then return end
	if name == self.state_name then return end
	if self.states[name] then
		local old_name = self.state_name
		-- core.log(name)
		if not self.meta then
			self.meta = {state_time = 0}
		end
		if self.state and self.state.on_end then self.state.on_end(host, self.meta, name) end

		self.state = self.states[name]
		self.meta = {state_time = 0}
		self.state_name = name
		if self.state.on_start then self.state.on_start(host, self.meta, old_name) end
	end
end
do
	local __meta = {__index = SM}
	function SM.new(host)
		return setmetatable(host or {}, __meta)
	end
end

local weld_entity = {
    initial_properties = {
        physical = false,
        textures = {"aom_wire_weld_indicator.png"},
        visual = "mesh",
		mesh = "aom_wire_weld_indicator.b3d",
        use_texture_alpha = false,
        pointable = false,
        glow = 14,
        static_save = false,
    },
    _player = nil,
	_size_center = 1,
	_size_cube = 5,
	_SM = SM.new({
		states = {
			none = {
				on_start = function(self, meta, from)
					self.object:set_animation({x=10, y=10}, 10, 0.2, false)
					local s = 0.0000001
					self.object:set_bone_override("root", {
						scale = {
							vec = vector.new(s,s,s),
							interpolation = 0.5,
							absolute = false,
						}
					})
				end,
			},
			show1 = {
				on_start = function(self, meta, from)
					self.object:set_animation({x=10, y=20}, 30, 0.1, false)
					local s = self._size_center
					self.object:set_bone_override("root", {
						scale = {
							vec = vector.new(s,s,s),
							absolute = false,
						}
					})
				end,
				on_step = function(self, meta, dtime)
					if meta.state_time >= (10 / 50) then return "idle1" end
				end,
			},
			idle1 = {
				on_start = function(self, meta, from)
					self.object:set_animation({x=40, y=60}, 25, 0.2, true)
					local s = self._size_cube / self._size_center
					self.object:set_bone_override("cube", {
						scale = {
							vec = vector.new(s,s,s),
							absolute = true,
						}
					})
				end,
			},
			hide1 = {
				on_start = function(self, meta, from)
					self.object:set_animation({x=0, y=10}, 30, 0.1, false)
				end,
				on_step = function(self, meta, dtime)
					if meta.state_time >= (10 / 50) then return "none" end
				end,
			},
			remove = {
				on_start = function(self, meta, from)
					self.object:set_animation({x=1, y=20}, 50, 0.2, false)
				end,
				on_step = function(self, meta, dtime)
					if meta.state_time >= (10 / 50) then
						self.object:remove()
						return "none"
					end
				end,
			},
		},
	}),
    on_step = function(self, dtime, moveresult)
		self._SM:on_step(self, dtime)
        if (not self._player) or (not core.is_player(self._player)) then
            self.object:remove()
            return
        end
    	local pi = aom_wire.welder.check_player(self._player)
		if pi.gui_ent ~= self then
			self._SM:set_state(self, "remove")
		end
    end,
	_update_overrides = function(self, dirs)
		for i = 1, #adjacent do
			local is_active = dirs[i]
			local is_changed = (not self._last_dirs) or ((self._last_dirs[i] ~= nil) ~= (is_active ~= nil))
			-- don't set if already correct
			if is_changed then
				local bone = tostring(i)
				local s = 4
				local l = 1
				self.object:set_bone_override("a" .. bone, {
					scale = {
						vec = is_active and vector.new(s,s,s) or vector.new(l,l/2,l),
						interpolation = 0.5,
						absolute = false,
					},
				})
			end
		end
		self._last_dirs = dirs
	end,
	_move_to = function(self, pos)
		local dirs = aom_wire.node_movement.get_weld_dir_list(pos)
		self:_update_overrides(dirs)
		local player = self._player
		if core.is_player(player) then
			local eyepos = aom_wire.get_eyepos(player)
			local target = pos
			local dist = vector.distance(eyepos, target)
			local dir = vector.direction(eyepos, target)
			local midpoint = eyepos + (dir * math.max(0.1, dist*0.5))
			-- self.object:set_pos(midpoint)
			self.object:move_to(midpoint, false)
		end
		if self._last_pos and (
			self._last_pos:equals(pos)) then
			return
		end
		local sn = self._SM.state_name
		if (sn == "hide1" or sn == "" or sn == "none") then
			self._SM:set_state(self, "show1")
		end
		self._last_pos = pos
	end,
	_hide = function(self, pos)
		self._last_pos = nil
		local sn = self._SM.state_name
		self._SM:set_state(self, "hide1")
		if (sn == "idle1" or sn == "show1" or sn == "" or sn == "none") then
		end
	end,
	_destroy = function(self)
		self._timer_destroy = 1
	end,
}
core.register_entity("aom_wire:weld_ENTITY", weld_entity)

function aom_wire.welder.on_place(itemstack, player, pointed_thing)
	if not core.is_player(player) then return end
    local ret = aom_util.try_rightclick(itemstack, player, pointed_thing, false)
    if ret then return ret end
    if (not pointed_thing) or (not pointed_thing.under) then return itemstack end
    local node = core.get_node_or_nil(pointed_thing.under)
    if node then
        local meta = core.get_meta(pointed_thing.under)
		if meta:get_string("aom_wire_weld") == "" then return end
		meta:set_string("aom_wire_weld", "")
        core.sound_play("aom_fire_extinguish", {
            gain = 0.5,
            to_player = player:get_player_name(),
        })
    end
end

local function debug_particle(pos, color, time, vel, size)
    -- do return end -- for debug purposes
    core.add_particle({
        size = size or 2,
        pos = pos,
        texture = "white.png^[colorize:"..(color or "#fff")..":255",
        velocity = vel or vector.new(0, 0, 0),
        expirationtime = time,
        glow = 14,
    })
end

function aom_wire.welder.get_eyepos(player)
    local eyepos = vector.add(player:get_pos(), vector.multiply(player:get_eye_offset(), 0.1))
    eyepos.y = eyepos.y + player:get_properties().eye_height
    return eyepos
end

function aom_wire.welder.get_tool_range(player)
    local hand = minetest.registered_items[""]
    local wield = player:get_wielded_item():get_definition()
    return math.max(wield.range or 4, hand.range or 4)
end

function aom_wire.welder.get_pointed_thing(player, eyepos, liquids)
    if not eyepos then eyepos = aom_wire.welder.get_eyepos(player) end
    local range = aom_wire.welder.get_tool_range(player)
    local ray = minetest.raycast(eyepos, vector.add(eyepos, vector.multiply(player:get_look_dir(), range)), false, (liquids == true))
    for pointed_thing in ray do
        if pointed_thing.type == "node" then
            return pointed_thing
        end
    end
    return nil
end

function aom_wire.welder.weld_ent_destroy(player)
    local pi = aom_wire.welder.check_player(player)
	if pi.gui_ent then
		pi.gui_ent._SM:set_state(pi.gui_ent, "remove")
		pi.gui_ent = nil
	end
end

function aom_wire.welder.weld_ent_add(player, pos)
	local obj = core.add_entity(pos, "aom_wire:weld_ENTITY")
	local ent = obj and obj:get_luaentity()
	if not ent then return end
	ent._player = player
	if obj.set_observers then
		obj:set_observers({
			[player:get_player_name()] = true,
		})
	end
	ent._SM:set_state(ent, "none")
	return ent
end

function aom_wire.welder.update_gui(player)
    local pi = aom_wire.welder.check_player(player)
	local pointed_thing = aom_wire.welder.get_pointed_thing(player, nil, false)
	if pi.gui_ent then
		if pi.source_pos then
			pi.gui_ent:_move_to(pi.source_pos)
		elseif pointed_thing then
			pi.gui_ent:_move_to(pointed_thing.under)
		else
			pi.gui_ent:_hide(player:get_pos())
		end
	else
		if pointed_thing then
			pi.gui_ent = aom_wire.welder.weld_ent_add(player, pointed_thing.under)
		end
	end
end

function aom_wire.welder.on_use(itemstack, player, pointed_thing)
	if (not pointed_thing) or (pointed_thing.type ~= "node") then return end
	if not core.is_player(player) then return end
    local pi = aom_wire.welder.check_player(player)
    local node = core.get_node_or_nil(pointed_thing.under)
    if not node then return end

    if not pi.source_pos then
        -- by definition, can only link from movable
        if (core.get_item_group(node.name, "immovable") <= 0) then
            pi.source_pos = vector.round(pointed_thing.under)
            core.sound_play("aom_wrench_plip", {
                gain = 0.5,
                to_player = player:get_player_name(),
            })
        else
            core.sound_play("aom_not_allowed", {
                gain = 0.5,
                to_player = player:get_player_name(),
            })
        end
        pi.t = 0
        -- aom_wire.node_movement.show_connections_particles(pointed_thing.under, player)
    else
        local success
		local face_dir
		if pointed_thing.under:equals(pi.source_pos) then
			if player:get_player_control().aux1 then
				face_dir = vector.subtract(pointed_thing.under, pointed_thing.above)
			else
				face_dir = vector.subtract(pointed_thing.above, pointed_thing.under)
			end
		else
			face_dir = vector.subtract(pointed_thing.under, pi.source_pos)
		end
		-- debug_particle(vector.add(pi.source_pos, vector.multiply(face_dir, 0.55)), "#fea", 1, face_dir, 5)
		-- there are only 6 directions you can weld, we don't need to do a distance check
		local i = adjacent_map[tostring(face_dir)]
		local is_welded = i and aom_wire.node_movement.is_weld_dir_set(pi.source_pos, i)
        if i then
            success = aom_wire.node_movement.set_weld_dir(pi.source_pos, i, not is_welded)
        end
        if success then
            -- aom_wire.node_movement.show_connections_particles_for_pos(pi.source_pos, pointed_thing.under, player)
            pi.source_pos = nil
            core.sound_play("aom_wrench_plip", {
                gain = 0.5,
                to_player = player:get_player_name(),
            })
			local creative = core.is_creative_enabled(player:get_player_name())
			if (not is_welded) and not creative then
				local idef = itemstack:get_definition()
				itemstack:add_wear_by_uses(idef._max_wear or 0)
				return itemstack
			end
        else
			pi.source_pos = nil
            core.sound_play("aom_not_allowed", {
                gain = 0.5,
                to_player = player:get_player_name(),
            })
        end
    end
end

function aom_wire.welder.on_secondary_use(itemstack, player)
	if not core.is_player(player) then return end
    local pi = aom_wire.welder.check_player(player)
    if pi.source_pos then pi.source_pos = nil end
    core.sound_play("aom_wrench_plip", {
        gain = 0.5,
        to_player = player:get_player_name(),
    })
end

function aom_wire.welder.on_step(itemstack, player, dtime)
    local pi = aom_wire.welder.check_player(player)
	aom_wire.welder.update_gui(player)
    if not pi.source_pos then return end

    pi.t = (pi.t or 0) - dtime
    if pi.t < 0 then pi.t = pi.t + 1 else return end

    local pos = pi.source_pos
    local eyepos = aom_wire.get_eyepos(player)
    local dir = vector.direction(eyepos, pos)
    core.add_particle({
        -- pos = eyepos + dir,
        pos = pos,
        velocity = dir * -10,
        expirationtime = 0.1,
        size = 2,
        collisiondetection = false,
        vertical = false,
        texture = "aom_wire_particle_highlight.png^[multiply:#fe8",
    })
end

function aom_wire.welder.on_select(itemstack, player)
    local pi = aom_wire.welder.check_player(player)
end

function aom_wire.welder.on_deselect(itemstack, player)
    local pi = aom_wire.welder.check_player(player)
	aom_wire.welder.weld_ent_destroy(player)
    pi.source_pos = nil
end

-- Node movement

-- get list of dirs that stick to this node
function aom_wire.node_movement.get_weld_dir_list(pos)
	local meta = core.get_meta(pos)
	local chars = meta:get_string("aom_wire_weld")
	if (chars == "") or string.len(chars) ~= 6 then return {} end
	local dirs = {}
	for i = 1, #adjacent do
		local c = string.sub(chars, i, i)
		if c == "1" then
			dirs[i] = adjacent[i]
		end
	end
	return dirs
end

-- sets the whole list at once similar to `set_weld_dir`
function aom_wire.node_movement.set_weld_dir_list(pos, dir_list)
	local meta = core.get_meta(pos)
	if dir_list == nil then
		dir_list = {}
		meta:set_string("aom_wire_weld", "")
	end
	local t = {}
	for i = 1, #adjacent do t[i] = "0" end
	for k, dir in pairs(dir_list) do
		local i = adjacent_map[tostring(dir)]
		if i then
			t[i] = "1"
		end
	end
	meta:set_string("aom_wire_weld", table.concat(t))
end

-- makes nodes in this dir stick to this node (and NOT vice versa)
function aom_wire.node_movement.set_weld_dir(pos, i, active)
	local c = #adjacent
	if i < 1 or i > c then return false end
	local char = (active and "1") or "0"
	local meta = core.get_meta(pos)
	local s = string.sub
	local t = meta:get_string("aom_wire_weld")
	if string.len(t) < c then t = "000000" end
	local var = table.concat({
		i > 1 and s(t, 1, i-1) or "",
		char,
		i < c and s(t, i+1, c) or ""
	})
	meta:set_string("aom_wire_weld", var)
	-- core.log(var .. tostring(pos))
	return true
end

-- returns true if this index (see `adjacent`) is welded, else false
function aom_wire.node_movement.is_weld_dir_set(pos, i)
	local meta = core.get_meta(pos)
	local t = meta:get_string("aom_wire_weld")
	if t == "" then return false end
	return (string.sub(t, i, i) == "1")
end

-- get info from node such as timer and meta so it can be perfectly replicated
function aom_wire.node_movement.gather_movable(pos, flags)
	if not flags then flags = {} end
	local node = core.get_node_or_nil(pos)
	if not node then return end
	if node.name == "air" then return end
	if core.get_item_group(node.name, "immovable") > 0 then
		return
	end
	local meta = core.get_meta(pos)
	local nt = core.get_node_timer(pos)
	return {
		pos = pos,
		node = node,
		meta = meta:to_table(),
		timer_timeout = nt:get_timeout(),
		timer_elapsed = nt:get_elapsed(),
	}
end

-- place down a `movable`
function aom_wire.node_movement.place_movable(pos, movable, flags)
	if not flags then flags = {} end
	local node = core.get_node_or_nil(pos)
	if not node then return end
	local ndef = core.registered_nodes[node.name]
	if not ndef then return end
	if not ndef.buildable_to then return end
	local nt = core.get_node_timer(pos)
	nt:stop()
	if (movable.timer_timeout or 0) > 0 then
		nt:start(1)
		nt:set(
			math.ceil(movable.timer_timeout),
			math.min(movable.timer_elapsed or 0, movable.timer_timeout)
		)
		-- core.log(":"..core.get_meta(pos):get_string("uid")..":".."   ".."starting timer")
		-- core.log((movable.timer_timeout or 0) - (movable.timer_elapsed or 0))
	end
	core.swap_node(pos, movable.node)
	local meta = core.get_meta(pos)
	meta:from_table(movable.meta)
	-- core.log(":"..core.get_meta(pos):get_string("uid")..":".."   ".."place")
	return true
end

local function do_particles(p)
    local d = 0.7
    core.add_particlespawner({
        amount = 10,
        time = 0.01,
        collisiondetection = false,
        collision_removal = false,
        object_collision = false,
        vertical = false,
        texpool = {
            {
                name = "gm_windmills_rocket_smoke.png",
                alpha_tween = {
                    0.0, 1.0,
                    style = "rev",
                    reps = 1,
                },
            }
        },
        minpos = vector.new(p.x - d, p.y - d, p.z - d),
        maxpos = vector.new(p.x + d, p.y + d, p.z + d),
        minvel = vector.new(-1, -1, -1),
        maxvel = vector.new( 1,  1,  1),
        minexptime = 0.4,
        maxexptime = 1,
        minsize = 1,
        maxsize = 6,
    })
end

-- test if a set of movables can be moved in `movable_info.dir` without collision
function aom_wire.node_movement.validate_movable_chain(movable_info, flags)
	if not flags then flags = {} end
	local can_move = true
	for i, movable in ipairs(movable_info.list) do
		local pos = movable.pos + movable_info.dir
		local node = core.get_node_or_nil(pos)
		if not node then return false end
		local ndef = core.registered_nodes[node.name]
		if not ndef then return false end
		if (not ndef.buildable_to) and (not movable_info.from_positions[tostring(pos)]) then
			-- core.log("warning", "cannot move to " .. tostring(pos) .. " from " .. tostring(movable.pos))
			can_move = false
			if flags.show_particles then
				do_particles(pos)
			else
				return false
			end
		end
	end
	return can_move
end

-- gets a list of all the movables that are connected, so it can be placed in a new position
function aom_wire.node_movement.gather_movable_chain(start_pos, dir, flags)
	if not flags then flags = {} end
	local to_process = {start_pos}
	local movable_info = {
		from_positions = {},
		list = {},
		dir = dir,
		objects = {},
	}
	local wire_max_movable_nodes = aom_wire.get_setting(nil, "wire_max_movable_nodes", 1000)
	if wire_max_movable_nodes < 1 then return movable_info end
	for c = 1, wire_max_movable_nodes do
		if #to_process == 0 then break end
		for k = #to_process, 1, -1 do
			local pos = to_process[k]
			local movable = aom_wire.node_movement.gather_movable(pos, flags)
			if movable then
				movable_info.from_positions[tostring(pos)] = true
				table.insert(movable_info.list, movable)
				local dirs = aom_wire.node_movement.get_weld_dir_list(pos)
				for i, d in pairs(dirs) do
					local p = vector.add(pos, d)
					if movable_info.from_positions[tostring(p)] == nil then
						table.insert(to_process, p)
					end
				end
			end
			table.remove(to_process, k)
		end
	end
	return movable_info
end

-- validate and then place down entire set of movables and shift objects standing on them
function aom_wire.node_movement.move_movable_chain(movable_info, flags)
	if aom_wire.get_setting(nil, "wire_max_movable_nodes", 1000) < 1 then return false end
	if not flags then flags = {} end
	if not aom_wire.node_movement.validate_movable_chain(movable_info, flags) then
		return false
	end
	-- local cl = os.clock()
	for i, movable in ipairs(movable_info.list) do
		local pos = movable.pos
		core.get_node_timer(pos):stop()
		core.swap_node(pos, {name="air"})
		core.get_meta(pos):from_table()
	end
	-- core.log("      DELETE: " .. tostring((os.clock() - cl) * 1))
	-- cl = os.clock()
	for i, movable in ipairs(movable_info.list) do
		local pos = movable.pos + movable_info.dir
		if aom_wire.node_movement.place_movable(pos, movable, flags) then
			local objects = core.get_objects_in_area(
				vector.offset(movable.pos,-0.5,-0.5,-0.5),
				vector.offset(movable.pos, 0.5, 0.6, 0.5))
			for k, object in ipairs(objects) do if not movable_info.objects[object] then
				if core.is_player(object) then
					object:set_pos(object:get_pos() + movable_info.dir)
				else
					object:set_pos(object:get_pos() + movable_info.dir)
				end
				movable_info.objects[object] = true
			end end
		end
	end
	-- core.log("   PLACEMENT: " .. tostring((os.clock() - cl) * 1))
	-- cl = os.clock()
	local pos
	local p
	local s = ""
	local checked = table.copy(movable_info.from_positions)
	for i, movable in ipairs(movable_info.list) do
		pos = movable.pos + movable_info.dir
		node_updates.cause_single_update(vector.offset(movable.pos, 0, 1, 0), "falling_node_check", nil, {
			_cost = 0.1, _delay = 0,
			_visited_list = checked,
		})
		node_updates.cause_single_update(pos, "falling_node_check", nil, {
			_cost = 0.1, _delay = 0,
			_visited_list = checked,
		})
		if has_node_updates then
			for k, d in ipairs(adjacent) do
				p = movable.pos + d
				s = tostring(p)
				if (not movable_info.from_positions[s]) then
					if not checked[s] then
						node_updates.cause_single_update(p, "move", nil, {_visited_list = checked, _cost = 0.1})
						-- checked[s] = true
					end
					p = pos + d
					s = tostring(p)
					if not checked[s] then
						node_updates.cause_single_update(p, "move", nil, {_visited_list = checked, _cost = 0.1})
						-- checked[s] = true
					end
				end
			end
		end
	end
	-- core.log(core.colorize("#f0f", "      UPDATE: " .. tostring((os.clock() - cl) * 1)))
	-- cl = os.clock()
	return true
end
