ZipLine = ZipLine or class()
ZipLine.DEBUG = false
ZipLine.TYPES = {"person", "bag"}
ZipLine.NET_EVENTS = {}
ZipLine.NET_EVENTS.request_access = 1
ZipLine.NET_EVENTS.access_denied = 2
ZipLine.NET_EVENTS.access_granted = 3
ZipLine.NET_EVENTS.set_user = 4
ZipLine.NET_EVENTS.remove_user = 5
ZipLine.NET_EVENTS.request_attach_bag = 6
ZipLine.NET_EVENTS.attach_bag_denied = 7
ZipLine.NET_EVENTS.attach_bag_granted = 8
local ids_rope_obj = Idstring("rope")

function ZipLine:init(unit)
	self._unit = unit
	self._rope_obj = unit:get_object(ids_rope_obj)
	self:set_usage_type(self._usage_type or "person")
	self._wire_brush = Draw:brush(Color.black:with_alpha(1))
	self._current_time = 0
	self._dirty = true
	self._start_pos = self._unit:position()
	self._end_pos = self._start_pos + self._unit:rotation():y() * 1000
	self._speed = 1000
	self._slack = 0
	self:_update_total_time()
	self._line_data = {}
	self._line_data.offset = Vector3(0, 0, 200)
	self._line_data.pos = mvector3.copy(self._start_pos)
	self._line_data.current_dir = Vector3()
	self._sled_data = {}
	self._sled_data.len = 200
	self._sled_data.object = self._unit:get_object(Idstring("move"))
	self._sled_data.pos = Vector3()
	self._sled_data.tip1 = Vector3()
	self._sled_data.tip2 = Vector3()
	self._sound_source = SoundDevice:create_source("zipline")
	self._sound_source:link(self._sled_data.object)
	self:_update_pos_data()
	self:set_enabled(true)
end

function ZipLine:update(unit, t, dt)
	if not self._enabled then
		return
	end
	self:_update_sled(t, dt)
	self:_update_sounds(t, dt)
	if ZipLine.DEBUG then
		self:debug_draw(t, dt)
	end
end

local mvec1 = Vector3()
local mvec2 = Vector3()

function ZipLine:_update_sled(t, dt)
	local current_time = self._current_time
	if self._attached_bag then
		if alive(self._attached_bag) then
			self._current_time = math.min(1, self._current_time + dt / self._total_time)
			local dir = math.lerp(self._line_data.dir_s, self._line_data.dir_e, self._current_time)
			local rot = Rotation(dir, math.UP)
			self._attached_bag:set_rotation(rot)
			local pos = self:update_and_get_pos_at_time(self._current_time)
			mvector3.set(mvec1, pos)
			mvector3.set(mvec2, math.UP)
			mvector3.multiply(mvec2, 180)
			mvector3.add(mvec1, mvec2)
			local dot = mvector3.dot(rot:y(), math.UP)
			mvector3.set(mvec2, dir)
			mvector3.multiply(mvec2, 20 * dot)
			mvector3.add(mvec1, mvec2)
			mvector3.set(mvec2, rot:z())
			mvector3.multiply(mvec2, math.abs(dot) * -5 - 3)
			mvector3.add(mvec1, mvec2)
			mvector3.add(mvec1, rot:x() * self._attached_bag_offset.x)
			mvector3.add(mvec1, rot:y() * self._attached_bag_offset.y)
			mvector3.add(mvec1, rot:z() * self._attached_bag_offset.z)
			self._attached_bag:set_position(mvec1)
			if self._current_time == 1 then
				self:release_bag(self._attached_bag)
			end
		else
			self._attached_bag = nil
		end
	elseif not alive(self._user_unit) and self._current_time ~= 0 then
		self._current_time = math.max(0, self._current_time - dt / self._total_time)
		self:update_and_get_pos_at_time(self._current_time)
		if self._current_time == 0 then
			self:_check_interaction_active_state()
		end
	elseif self._synced_user then
	end
	self._dirty = self._current_time ~= current_time or self._dirty
	self:_check_dirty()
	if self._synced_user and alive(self._user_unit) and self._sled_data.object then
		local pos = self._sled_data.object:position() + self._sled_data.object:rotation():z() * -22
		self._wire_brush:cylinder(pos, pos + math.UP * -100, 1)
		self._wire_brush:sphere(pos, 2)
	end
end

function ZipLine:_update_sounds(t, dt)
	if self._current_time ~= 0 and not self._running then
		self._sound_data = {}
		self._sound_data.last_pos = mvector3.copy(self._sled_data.pos)
		self._sound_source:post_event("zipline_hook")
		self._sound_source:post_event("zipline_start")
		self._running = true
	elseif self._current_time == 0 and self._running then
		self._sound_data = nil
		self._sound_source:post_event("zipline_stop")
		self._running = false
	elseif self._running then
		local speed = mvector3.length(self._sled_data.pos - self._sound_data.last_pos) / dt
		mvector3.set(self._sound_data.last_pos, self._sled_data.pos)
		local rtpc = math.clamp(speed, 0, 1500) / 1500
		self._sound_source:set_rtpc("zipline_speed", rtpc)
		if not self._sound_data.has_hooked_off and not alive(self._attached_bag) and not alive(self._user_unit) then
			self._sound_source:post_event("zipline_unhook")
			self._sound_data.has_hooked_off = true
		end
	end
end

function ZipLine:_check_dirty()
	if not self._dirty then
		return
	end
	self._dirty = nil
	mvector3.lerp(self._line_data.current_dir, self._line_data.dir_s, self._line_data.dir_e, self._current_time)
	local len = 16
	mvector3.set(self._sled_data.tip1, self._line_data.current_dir)
	mvector3.multiply(self._sled_data.tip1, -len)
	mvector3.add(self._sled_data.tip1, self._sled_data.pos)
	mvector3.set(self._sled_data.tip2, self._line_data.current_dir)
	mvector3.multiply(self._sled_data.tip2, len)
	mvector3.add(self._sled_data.tip2, self._sled_data.pos)
	self:_update_sled_object()
	self._rope_obj:set_control_points({
		self._line_data.start_pos,
		self._sled_data.tip1,
		self._sled_data.tip2,
		self._line_data.end_pos
	})
end

function ZipLine:_update_sled_object()
	if self._sled_data.object then
		self._sled_data.object:set_position(self._sled_data.pos)
		self._sled_data.object:set_rotation(Rotation(self._line_data.current_dir, math.UP))
		self._unit:set_moving()
		if self._unit:interaction():active() then
			self._unit:interaction():external_upd_interaction_topology()
		end
	end
end

function ZipLine:_check_interaction_active_state()
	if not self._enabled then
		self._unit:interaction():set_active(false)
		return
	end
	self._unit:interaction():set_active(not self:is_interact_blocked())
end

function ZipLine:is_interact_blocked()
	if self._booked_by_peer_id then
		return true
	end
	if self._booked_bag_peer_id then
		return true
	end
	if alive(self._attached_bag) then
		return true
	end
	return self._current_time ~= 0 or alive(self:user_unit())
end

function ZipLine:on_interacted(unit)
	if self:is_interact_blocked() then
		return
	end
	if self:is_usage_type_bag() then
		if managers.player:is_carrying() then
			if Network:is_server() then
				managers.player:drop_carry(self._unit)
			else
				self:_client_request_attach_bag(unit)
			end
			return
		end
		return
	end
	if self:is_usage_type_person() then
		if Network:is_server() then
			if not alive(self._user_unit) then
				self:set_user(unit)
			end
		else
			self:_client_request_access(unit)
		end
	end
end

function ZipLine:_client_request_attach_bag(player)
	self._request_unit = player
	player:movement():set_carry_restriction(true)
	managers.network:session():send_to_host("sync_unit_event_id_16", self._unit, "zipline", self.NET_EVENTS.request_attach_bag)
end

function ZipLine:_attach_bag_response(granted)
	if alive(self._request_unit) then
		self._request_unit:movement():set_carry_restriction(false)
		if granted then
			managers.player:drop_carry(self._unit)
		end
	end
	self._request_unit = nil
end

function ZipLine:_client_request_access(unit)
	self._request_unit = unit
	managers.network:session():send_to_host("sync_unit_event_id_16", self._unit, "zipline", self.NET_EVENTS.request_access)
end

function ZipLine:set_user(unit)
	local old_unit = self._user_unit
	self._user_unit = unit
	if self._user_unit then
		self:run_sequence("on_person_enter_zipline", self._user_unit)
		self._user_unit:movement():on_enter_zipline(self._unit)
		self:_send_net_event(self.NET_EVENTS.set_user)
	else
		self:run_sequence("on_person_exit_zipline", old_unit)
		self:_send_net_event(self.NET_EVENTS.remove_user)
	end
	self:_check_interaction_active_state()
end

function ZipLine:sync_set_user(unit)
	self._booked_by_peer_id = nil
	local old_unit = self._user_unit
	self._user_unit = unit
	self._synced_user = alive(self._user_unit) and true or nil
	if self._user_unit then
		self:run_sequence("on_person_enter_zipline", self._user_unit)
		self._user_unit:movement():on_enter_zipline(self._unit)
	elseif old_unit then
		old_unit:movement():on_exit_zipline()
		self:run_sequence("on_person_exit_zipline", old_unit)
		self:_send_net_event(self.NET_EVENTS.remove_user)
	end
	self:_check_interaction_active_state()
end

function ZipLine:sync_remove_user()
	if alive(self._user_unit) then
		self:run_sequence("on_person_exit_zipline", self._user_unit)
		self._user_unit:movement():on_exit_zipline()
	end
	self._user_unit = nil
	self._synced_user = nil
	self:_check_interaction_active_state()
end

function ZipLine:user_unit()
	return self._user_unit
end

function ZipLine:is_valid()
	return self._start_pos and self._end_pos and true
end

function ZipLine:set_speed(speed)
	if not speed then
		return
	end
	self._speed = speed
	self:_update_total_time()
end

function ZipLine:speed()
	return self._speed
end

function ZipLine:set_ai_ignores_bag(ai_ignores_bag)
	self._ai_ignores_bag = ai_ignores_bag
end

function ZipLine:ai_ignores_bag()
	return self._ai_ignores_bag
end

function ZipLine:set_slack(slack)
	if not slack then
		return
	end
	self._slack = slack
	self:_update_pos_data()
end

function ZipLine:slack()
	return self._slack
end

function ZipLine:set_total_time(total_time)
	self._total_time = total_time
end

function ZipLine:total_time()
	return self._total_time
end

function ZipLine:_update_total_time()
	self:set_total_time((self:start_pos() - self:end_pos()):length() / self._speed)
end

function ZipLine:start_pos()
	return self._start_pos
end

function ZipLine:end_pos()
	return self._end_pos
end

function ZipLine:set_start_pos(pos)
	self._start_pos = pos
	self:_update_pos_data()
	self:_update_total_time()
end

function ZipLine:set_end_pos(pos)
	self._end_pos = pos
	self:_update_pos_data()
	self:_update_total_time()
end

function ZipLine:set_end_pos_by_line(pos)
	self:set_end_pos(pos - self._line_data.offset)
end

function ZipLine:_update_pos_data()
	if not self:is_valid() then
		return
	end
	mvector3.set(self._sled_data.pos, self._start_pos)
	mvector3.add(self._sled_data.pos, self._line_data.offset)
	self._line_data.start_pos = self._start_pos + self._line_data.offset
	self._line_data.end_pos = self._end_pos + self._line_data.offset
	self._line_data.dir = self._line_data.end_pos - self._start_pos
	self._line_data.dir_s = (self:pos_at_time(0.5) + self._line_data.offset - self._line_data.start_pos):normalized()
	self._line_data.dir_e = (self._line_data.end_pos - (self:pos_at_time(0.5) + self._line_data.offset)):normalized()
	mvector3.set_z(self._line_data.dir, 0)
	mvector3.normalize(self._line_data.dir)
	mvector3.set(self._line_data.pos, self._sled_data.pos)
	self._dirty = true
end

function ZipLine:set_enabled(enabled)
	self._enabled = enabled
	if self._enabled then
	else
	end
	self:_check_interaction_active_state()
end

function ZipLine:set_usage_type(usage_type)
	if not usage_type then
		return
	end
	self._usage_type = usage_type
	self._unit:interaction():set_tweak_data(self:is_usage_type_bag() and "bag_zipline" or "player_zipline")
end

function ZipLine:usage_type()
	return self._usage_type
end

function ZipLine:is_usage_type_person()
	return self._usage_type == "person"
end

function ZipLine:is_usage_type_bag()
	return self._usage_type == "bag"
end

function ZipLine:current_time()
	return self._current_time
end

function ZipLine:pos_at_current_time()
	return self:pos_at_time(self._current_time)
end

function ZipLine:_update_and_get_pos_at_time(time, func)
	self._current_time = time
	self._dirty = true
	local pos = func(self, time)
	mvector3.set(self._sled_data.pos, pos)
	mvector3.add(self._sled_data.pos, self._line_data.offset)
	mvector3.set(self._line_data.pos, self._sled_data.pos)
	self:_check_dirty()
	return pos
end

function ZipLine:update_and_get_pos_at_time(time)
	return self:_update_and_get_pos_at_time(time, self.pos_at_time)
end

function ZipLine:update_and_get_pos_at_time_linear(time)
	return self:_update_and_get_pos_at_time(time, self.pos_at_time_linear)
end

local ease_bezier_points = {
	0,
	0,
	1,
	1
}
local slack_bezier_points = {
	0,
	1,
	0.5,
	0
}

function ZipLine:pos_at_time(time)
	local bezier_value = math.bezier(ease_bezier_points, time)
	local pos = math.lerp(self._start_pos, self._end_pos, bezier_value)
	local slack_bezier = math.bezier(slack_bezier_points, bezier_value)
	local slack = math.lerp(0, self._slack, slack_bezier)
	mvector3.set_z(pos, mvector3.z(pos) - slack)
	return pos
end

function ZipLine:pos_at_time_linear(time)
	local pos = math.lerp(self._start_pos, self._end_pos, time)
	local slack_bezier = math.bezier(slack_bezier_points, time)
	local slack = math.lerp(0, self._slack, slack_bezier)
	mvector3.set_z(pos, mvector3.z(pos) - slack)
	return pos
end

function ZipLine:speed_at_time(time, step)
	step = step or 0.01
	local pos1 = self:pos_at_time(time)
	local pos2 = self:pos_at_time(math.clamp(time + step, 0, 1))
	local dist = mvector3.distance(pos1, pos2)
	return dist
end

function ZipLine:current_direction()
	return self._line_data.current_dir
end

function ZipLine:sync_net_event(event_id, peer)
	local net_events = self.NET_EVENTS
	if event_id == net_events.request_access then
		print("! request_access")
		if self:is_interact_blocked() then
			print(" blocked")
			peer:send_queued_sync("sync_unit_event_id_16", self._unit, "zipline", self.NET_EVENTS.access_denied)
		else
			print(" allowed")
			self._booked_by_peer_id = peer:id()
			peer:send_queued_sync("sync_unit_event_id_16", self._unit, "zipline", self.NET_EVENTS.access_granted)
		end
	elseif event_id == net_events.access_denied then
		print("! access_denied")
		self._request_unit = nil
	elseif event_id == net_events.access_granted then
		print("! access_granted")
		if alive(self._request_unit) then
			self:set_user(self._request_unit)
		end
		self._request_unit = nil
	elseif event_id == net_events.set_user then
		print("! set user")
		local unit = peer:unit()
		if alive(unit) then
			self:sync_set_user(unit)
		end
	elseif event_id == net_events.remove_user then
		print("! remove user")
		self:sync_remove_user()
	elseif event_id == net_events.request_attach_bag then
		print("! net_events.request_attach_bag")
		if self:is_interact_blocked() then
			print(" respons attach_bag_denied")
			peer:send_queued_sync("sync_unit_event_id_16", self._unit, "zipline", self.NET_EVENTS.attach_bag_denied)
		else
			self._booked_bag_peer_id = peer:id()
			print(" respons attach_bag_granted")
			peer:send_queued_sync("sync_unit_event_id_16", self._unit, "zipline", self.NET_EVENTS.attach_bag_granted)
		end
	elseif event_id == net_events.attach_bag_denied then
		print("! net_events.attach_bag_denied")
		self:_attach_bag_response(false)
	elseif event_id == net_events.attach_bag_granted then
		print("! net_events.attach_bag_granted")
		self:_attach_bag_response(true)
	end
end

function ZipLine:_send_net_event(event_id)
	managers.network:session():send_to_peers_synched("sync_unit_event_id_16", self._unit, "zipline", event_id)
end

function ZipLine:attach_bag(bag)
	self._booked_bag_peer_id = nil
	local body = bag:body("hinge_body_1") or bag:body(0)
	body:set_keyframed()
	self._attached_bag = bag
	local carry_id = self._attached_bag:carry_data():carry_id()
	self._attached_bag_offset = tweak_data.carry:get_zipline_offset(carry_id)
	self._bag_disabled_collisions = {}
	local nr_bodies = bag:num_bodies()
	for i_body = 0, nr_bodies - 1 do
		local body = bag:body(i_body)
		if body:collisions_enabled() then
			body:set_keyframed()
			table.insert(self._bag_disabled_collisions, body)
			body:set_collisions_enabled(false)
		end
	end
	self._attached_bag:set_rotation(Rotation(self._line_data.dir_s, math.UP))
	self._attached_bag:carry_data():set_zipline_unit(self._unit)
	self:_check_interaction_active_state()
	self:run_sequence("on_attached_bag", self._attached_bag)
end

function ZipLine:release_bag()
	local body = self._attached_bag:body("hinge_body_1") or self._attached_bag:body(0)
	body:set_dynamic()
	if self._bag_disabled_collisions then
		for _, body in ipairs(self._bag_disabled_collisions) do
			body:set_dynamic()
			body:set_collisions_enabled(true)
		end
		self._bag_disabled_collisions = nil
	end
	self._attached_bag:carry_data():set_zipline_unit(nil)
	self:run_sequence("on_detached_bag", self._attached_bag)
	self._attached_bag = nil
end

function ZipLine:run_sequence(sequence_name, user_unit)
	if self._unit:damage():has_sequence(sequence_name) then
		self._unit:damage():run_sequence_simple(sequence_name, {unit = user_unit})
	end
end

function ZipLine:destroy(unit)
end

function ZipLine:debug_draw(t, dt)
	if not self:is_valid() then
		return
	end
	local brush = Draw:brush(Color.white:with_alpha(0.5))
	for i = 0, 1, 0.005 do
		brush:sphere(self:pos_at_time(i), 2)
	end
	local offset = Vector3(0, 0, 200)
	local brush = Draw:brush(Color.green:with_alpha(0.5))
	local pos = self:pos_at_time((1 + math.sin(t * 50)) / 2)
	brush:cylinder(self._start_pos + offset, pos + offset, 1)
	brush:cylinder(pos + offset, self._end_pos + offset, 1)
	brush:sphere(pos + offset + Vector3(0, 0, 10), 10)
	brush:sphere(pos, 10)
end

function ZipLine:save(data)
	local state = {}
	state.enabled = self._enabled
	state.current_time = self._current_time
	state.end_pos = self._end_pos
	state.speed = self._speed
	state.slack = self._slack
	state.usage_type = self._usage_type
	data.ZipLine = state
end

function ZipLine:load(data)
	local state = data.ZipLine
	if state.enabled ~= self._enabled then
		self:set_enabled(state.enabled)
	end
	self:set_end_pos(state.end_pos)
	self:set_speed(state.speed)
	self:set_slack(state.slack)
	self:set_usage_type(state.usage_type)
	self._current_time = state.current_time
	managers.worlddefinition:use_me(self._unit)
end
