-- 2450^2 voxels @ 4 studs/voxel is the default size
local _mapSize = Conf.map_size or (2450 * 4)
local _minimapWidth = Conf.minimap_width or 0.3
local _minimapHeight = Conf.minimap_height or 0.3
local _worldmapWidth = Conf.worldmap_width or 0.9
local _worldmapHeight = Conf.worldmap_height or 0.9
local _isCurrentlyMinimap = true

function MinimapGui.updateStormIndicator(pos, radius, targetPos, targetRadius)
	_stormData = {
		pos = pos,
		radius = radius,
		targetPos = targetPos,
		targetRadius = targetRadius,
	}
end

local function _UpdateStormIndicators(pos, radius, targetPos, targetRadius)
	if not _stormIndicator then
		return
	end

	-- Current storm
	local mapSize = _map.Size
	local mapPos = _map.Position
	_stormIndicator.Position = UDim2.new(0, pos.X / _mapSize * mapSize.X.Offset + mapPos.X.Offset,
										 0, pos.Z / _mapSize * mapSize.Y.Offset + mapPos.Y.Offset)

	local uiScale = radius * 2 / _mapSize
	_stormIndicator.Size = UDim2.new(0, uiScale * mapSize.X.Offset,
									 0, uiScale * mapSize.Y.Offset)

	-- Storm fillers (outside current storm)
	if _stormIndicator.Size.X.Offset <= 0 then
		_stormFiller.Size = UDim2.new(0, 1, 0, 1)
	end
	-- Add new outerStormFillers if current largest one doesn't cover the map
	local largestOuterStormFiller = _outerStormFillers[#_outerStormFillers]
	while largestOuterStormFiller.AbsoluteSize.X < _mapFrame.AbsoluteSize.X * 3 or largestOuterStormFiller.AbsoluteSize.Y < _mapFrame.AbsoluteSize.Y * 3 do
		local newOuterStormFiller = largestOuterStormFiller:Clone()
		newOuterStormFiller.Parent = largestOuterStormFiller
		table.insert(_outerStormFillers, newOuterStormFiller)
		largestOuterStormFiller = newOuterStormFiller
	end

	-- Storm destination
	_stormTargetIndicator.Position = UDim2.new(0, targetPos.X / _mapSize * mapSize.X.Offset + mapPos.X.Offset,
											   0, targetPos.Z / _mapSize * mapSize.Y.Offset + mapPos.Y.Offset)
	uiScale = targetRadius * 2 / _mapSize
	_stormTargetIndicator.Size = UDim2.new(0, -uiScale * mapSize.X.Offset,
										   0, -uiScale * mapSize.Y.Offset)

	_stormIndicator.Visible = true
	_stormTargetIndicator.Visible = true
	_stormFiller.Visible = true
	for _, outerStormFiller in ipairs(_outerStormFillers) do
		outerStormFiller.Visible = true
	end
end

local function _updateMapFocus()
	if _isCurrentlyMinimap then
		local character = Util.getClientFocus()

		if not character or not character.PrimaryPart then
			return
		end

		local root = character.PrimaryPart
		local pos = root.Position

		-- Update focus point
		_mapView.focus_x = pos.X
		_mapView.focus_y = pos.Z
	else
		-- Set focus point to middle of map for worldmap view
		_mapView.focus_x = _mapSize / 2
		_mapView.focus_y = _mapSize / 2
	end

	-- Panning/scaling of viewport/map
	-- Note:  We use Offsets here to maintain the correct aspect ratio for the map inside the map frame (which may be arbitrarily sized)
	-- TODO:  Currently this assumes a square map
	_map.Size = UDim2.new(0, _mapFrame.AbsoluteSize.X * _mapView.scale,
						  0, _mapFrame.AbsoluteSize.X * _mapView.scale)

	-- Get the viewport position of the map focus point and adjust the map position accordingly.
	local relativeFocus = Vector2.new(_mapView.focus_x / _mapSize,
									  _mapView.focus_y / _mapSize)

	_map.Position = UDim2.new(0, _mapFrame.AbsoluteSize.X * 0.5 - _map.Size.X.Offset * relativeFocus.X,
							  0, _mapFrame.AbsoluteSize.Y * 0.5 - _map.Size.Y.Offset * relativeFocus.Y)
end

local function _Update(dt)
	_updateMapFocus()

	for tag, icon in pairs(_mapTags) do
		local count = 0

		for i, model in ipairs(CollectionService:GetTagged(tag)) do
			if model.PrimaryPart then
				-- TODO:  May need an actual unique identifier
				local indicator = _mapIndicators[tag][i]

				if not indicator then
					indicator = icon:Clone()

					-- Set color of deliveryVehicle indicator to team color if there are teams
					if string.sub(model.Name, 1, 15) == "DeliveryVehicle" then
						if tonumber(string.sub(model.Name, 16)) == nil then
							indicator.ImageTransparency = 1
						elseif Conf.num_teams > 0 then
							indicator.ImageColor3 = Conf.team_colors[tonumber(string.sub(model.Name, 16))]
						end
					end

					-- Set color of all players on team to team color if there are teams
					if Conf.num_teams > 0 and model:FindFirstChildOfClass("Humanoid") ~= nil then
						indicator.ImageColor3 = Conf.team_colors[PlayerMatchInfo.GetField(Players:GetPlayerFromCharacter(model), "teamNumber")]
					end

					indicator.Parent = _mapFrame
					_mapIndicators[tag][i] = indicator
				end

				indicator.Visible = true

				local root = model.PrimaryPart
				local pos = root.Position
				local partCFrame = root.CFrame

				local mapSize = _map.Size
				local mapPos = _map.Position

				-- NOTE: We determine whether the forward vector for this part should be its look or up vectors by checking LookVector against up
				-- .That vector is then flattened along the x/y plane and normalized
				-- .The angle is calculated, converted into degrees, and used to rotate the indicator

				local forward = Vector3.new(partCFrame.LookVector.X, 0, partCFrame.LookVector.Z).Unit
				local lookDot = partCFrame.LookVector:Dot(_unitUp)

				if math.abs(lookDot) > 0.5 then
					forward = Vector3.new(partCFrame.UpVector.X, 0, partCFrame.UpVector.Z).Unit
				end

				local rotation = math.acos(forward:Dot(_unitForward)) * 57.2958

				if forward:Dot(_unitRight) < 0 then
					rotation = 360 - rotation
				end

				indicator.Position = UDim2.new(0, pos.X / _mapSize * mapSize.X.Offset + mapPos.X.Offset,
											   0, pos.Z / _mapSize * mapSize.Y.Offset + mapPos.Y.Offset)

				-- HACK:  This extra inversion and 180 degree rotation could probably be factored out, but it was taking more time than appropriate to fix.
				indicator.Rotation = 360 - (rotation + 180) % 360

				--
				count = count + 1
			end
		end

		-- Destroy any unused indicators based on count
		local existingIndicatorCount = #_mapIndicators[tag]

		if count < existingIndicatorCount then
			for i = count, existingIndicatorCount do
				local indicator = _mapIndicators[tag][i]

				if indicator then
					indicator:Destroy()
				end

				_mapIndicators[tag][i] = nil
			end
		end
	end

	--
	if _stormData then
		_UpdateStormIndicators(_stormData.pos, _stormData.radius, _stormData.targetPos, _stormData.targetRadius)
	end
end

local function toggleMap()
	if _isCurrentlyMinimap then -- change to worldmap
		_mapFrame.Size = UDim2.new(_worldmapWidth, 0, _worldmapHeight, 0)
		_mapFrame.AnchorPoint = Vector2.new(0.5, 0.5)
		_mapFrame.Position = UDim2.new(0.5, 0, 0.5, 0)
		_mapView.scale = _worldmapZoom
		_isCurrentlyMinimap = false
	else -- change to minimap
		_mapFrame.Size = UDim2.new(_minimapWidth, 0, _minimapHeight, 0)
		_mapFrame.AnchorPoint = Vector2.new(1, 0)
		_mapFrame.Position = UDim2.new(1, 0, 0, 0)
		_mapView.scale = _minimapZoom
		_isCurrentlyMinimap = true
	end

	return _isCurrentlyMinimap
end

local function onInputBegan(input, gameProcessed)
	if not gameProcessed and input.KeyCode == Enum.KeyCode.M or input.KeyCode == Enum.KeyCode.DPadUp then
		toggleMap()
	end
end

function MinimapGui.start(teamDividingAngle)
	while not _setupFinished do
		wait()
	end

	MinimapGui.addMapTag("DeliveryVehicle", "Bus")
	MinimapGui.addMapTag(Util._clientFocusTag, "LocalPlayerLocation")
	MinimapGui.addMapTag("Player")
	MinimapGui.addMapTag("Vehicle", "VehicleLocation")

	ToggleMapEvent.OnInvoke = toggleMap

	UserInputService.InputBegan:Connect(onInputBegan)

	RunService:BindToRenderStep("Minimap", Enum.RenderPriority.Last.Value, _Update)

	_isCurrentlyMinimap = false
	toggleMap() -- ensures minimap size + position + anchor will be the same regardless of values set in studio

	_mapFrame.Visible = true
	if Conf.num_teams == 2 and teamDividingAngle ~= nil then
		local xCoord = math.cos(math.rad(teamDividingAngle)) * dividingLineLength / 2
		local yCoord = math.sin(math.rad(teamDividingAngle)) * dividingLineLength / 2
		for i, dot in ipairs(_teamDividingDots) do
			dot.Position = UDim2.new(0.5 + xCoord - 2 * xCoord * ((i - 1) / (numTeamDividingDots - 1)), 0, 0.5 + yCoord - 2 * yCoord * ((i - 1) / (numTeamDividingDots - 1)), 0)
			dot.Visible = true
		end
	end

	EnableToggleMapButtonEvent:Fire(true, _isCurrentlyMinimap)
end

function MinimapGui.addMapTag(tag, icon)
	local tagIndicatorTemplate

	if not icon then
		tagIndicatorTemplate = _playerIndicator

	elseif type(icon) == "string" then
		tagIndicatorTemplate = _mapFrame:FindFirstChild(icon) or _playerIndicator

	else
		tagIndicatorTemplate = icon
	end

	_mapTags[tag] = tagIndicatorTemplate

	if not _mapIndicators[tag] then
		_mapIndicators[tag] = {}
	else
		-- TODO:  Handle possibly switching to a different icon
	end
end

function MinimapGui.removeMapTag(tag)
	local indicators = _mapIndicators[tag]

	if indicators then
		for _, icon in ipairs(indicators) do
		end
	end
end

local function onCharacterAdded(character)
	if not PlayerMatchInfo.GetField(Players.LocalPlayer, "didJump") then
		local PlayerGui = Players.LocalPlayer:WaitForChild("PlayerGui")

		local container = ReplicatedStorage.Assets.GuiObjects.minimap:Clone()
		container.ResetOnSpawn = false

		_mapFrame = container:WaitForChild("mapframe")
		_mapFrame.Size = UDim2.new(_minimapWidth, 0, _minimapHeight, 0)

		_map = _mapFrame:WaitForChild("map")
		_playerIndicator = _mapFrame:WaitForChild("PlayerLocation")
		_stormIndicator = _mapFrame:WaitForChild("StormIndicator")
		_stormTargetIndicator = _mapFrame:WaitForChild("StormTargetIndicator")
		_stormFiller = _stormIndicator:WaitForChild("StormFiller")
		table.insert(_outerStormFillers, _stormFiller:WaitForChild("OuterStormFiller"))

		table.insert(_teamDividingDots, _map:WaitForChild("TeamDividingDot"))
		for i = 1, (numTeamDividingDots - 1) do
			local newDot = _teamDividingDots[1]:Clone()
			newDot.Parent = _map
			table.insert(_teamDividingDots, newDot)
		end

		-- Hide minimap since match hasn't started yet
		_playerIndicator.Visible = false
		_stormIndicator.Visible = false
		_stormTargetIndicator.Visible = false
		_stormFiller.Visible = false
		for _, outerStormFiller in ipairs(_outerStormFillers) do
			outerStormFiller.Visible = false
		end
		for _, dot in ipairs(_teamDividingDots) do
			dot.Visible = false
		end
		_mapFrame.Visible = false

		container.Enabled = true
		container.Parent = PlayerGui
		_setupFinished = true
	elseif not _isCurrentlyMinimap then
		-- Make sure map is mini when player respawns
		toggleMap()
	end
end

function MinimapGui.isCurrentlyMinimap()
	return _isCurrentlyMinimap
end

function MinimapGui.isEnabled()
	return _mapFrame and _mapFrame.Visible
end

if Players.LocalPlayer.Character then
	onCharacterAdded(Players.LocalPlayer.Character)
end
Players.LocalPlayer.CharacterAdded:Connect(onCharacterAdded)