/*
 * Portions of this file are copyright Rebirth contributors and licensed as
 * described in COPYING.txt.
 * Portions of this file are copyright Parallax Software and licensed
 * according to the Parallax license below.
 * See COPYING.txt for license details.

THE COMPUTER CODE CONTAINED HEREIN IS THE SOLE PROPERTY OF PARALLAX
SOFTWARE CORPORATION ("PARALLAX").  PARALLAX, IN DISTRIBUTING THE CODE TO
END-USERS, AND SUBJECT TO ALL OF THE TERMS AND CONDITIONS HEREIN, GRANTS A
ROYALTY-FREE, PERPETUAL LICENSE TO SUCH END-USERS FOR USE BY SUCH END-USERS
IN USING, DISPLAYING,  AND CREATING DERIVATIVE WORKS THEREOF, SO LONG AS
SUCH USE, DISPLAY OR CREATION IS FOR NON-COMMERCIAL, ROYALTY OR REVENUE
FREE PURPOSES.  IN NO EVENT SHALL THE END-USER USE THE COMPUTER CODE
CONTAINED HEREIN FOR REVENUE-BEARING PURPOSES.  THE END-USER UNDERSTANDS
AND AGREES TO THE TERMS HEREIN AND ACCEPTS THE SAME BY USE OF THIS FILE.
COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION.  ALL RIGHTS RESERVED.
*/

/*
 *
 * Routines for displaying the auto-map.
 *
 */

#include "dxxsconf.h"
#include <algorithm>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#if DXX_USE_OGL
#include "ogl_init.h"
#endif

#include "dxxerror.h"
#include "3d.h"
#include "inferno.h"
#include "u_mem.h"
#include "render.h"
#include "object.h"
#include "game.h"
#include "polyobj.h"
#include "sounds.h"
#include "player.h"
#include "bm.h"
#include "key.h"
#include "newmenu.h"
#include "textures.h"
#include "hudmsg.h"
#include "timer.h"
#include "segpoint.h"
#include "joy.h"
#include "pcx.h"
#include "palette.h"
#include "wall.h"
#include "physfsx.h"
#include "gameseq.h"
#include "gamefont.h"
#include "gameseg.h"
#include "common/3d/globvars.h"
#include "multi.h"
#include "kconfig.h"
#include "endlevel.h"
#include "text.h"
#include "gauges.h"
#include "powerup.h"
#include "switch.h"
#include "automap.h"
#include "timer.h"
#include "config.h"
#include "playsave.h"
#include "window.h"
#include "playsave.h"
#include "args.h"
#include "physics.h"

#include "compiler-range_for.h"
#include "d_enumerate.h"
#include "d_levelstate.h"
#include "d_range.h"
#include "d_zip.h"
#include <memory>

#define EF_USED     1   // This edge is used
#define EF_DEFINING 2   // A structure defining edge that should always draw.
#define EF_FRONTIER 4   // An edge between the known and the unknown.
#define EF_SECRET   8   // An edge that is part of a secret wall.
#define EF_GRATE    16  // A grate... draw it all the time.
#define EF_NO_FADE  32  // An edge that doesn't fade with distance
#define EF_TOO_FAR  64  // An edge that is too far away

namespace dcx {

namespace {

struct segment_depth_array_t : public per_segment_array<uint8_t>
{
};

struct Edge_info
{
	std::array<vertnum_t, 2> verts;     // 8  bytes
	std::array<sidenum_t, 4> sides;     // 4  bytes
	std::array<segnum_t, 4> segnum;    // 16 bytes  // This might not need to be stored... If you can access the normals of a side.
	ubyte flags;        // 1  bytes  // See the EF_??? defines above.
	color_t color;        // 1  bytes
	ubyte num_faces;    // 1  bytes  // 31 bytes...
};

struct automap : ::dcx::window
{
	static constexpr fix ZOOM_DEFAULT{i2f(20 * 10)};
	using ::dcx::window::window;
	fix64			entry_time;
	fix64			t1, t2;
	static_assert(PF_WIGGLE < UINT8_MAX, "storing PF_WIGGLE into old_wiggle would truncate the value");
	uint8_t			old_wiggle;
	uint8_t			leave_mode{};
	uint8_t			pause_game;
	vms_angvec		tangles;
	int max_segments_away{};
	int segment_limit{1};

	// Edge list variables
	int num_edges{};
	unsigned max_edges; //set each frame
	unsigned end_valid_edges{};
	std::unique_ptr<Edge_info[]>		edges;
	std::unique_ptr<Edge_info *[]>			drawingListBright;

	// Screen canvas variables
	grs_subcanvas		automap_view;

	grs_main_bitmap		automap_background;

	// Rendering variables
	fix			zoom{0x9000};
	vms_vector		view_target;
	vms_vector		view_position;
	fix			farthest_dist{(F1_0 * 20 * 50)}; // 50 segments away
	vms_matrix		viewMatrix;
	fix viewDist{ZOOM_DEFAULT};

	segment_depth_array_t depth_array;
	color_t			wall_normal_color;
	color_t			wall_door_color;
	color_t			wall_door_blue;
	color_t			wall_door_gold;
	color_t			wall_door_red;
	color_t			hostage_color;
	color_t			green_31;
};

//	----------------------------------------------------------------------------------------------------------
//	Set the segment depth of all segments from start_seg in `depth`.
//	Returns maximum depth value.
[[nodiscard]]
unsigned set_segment_depths(vcsegidx_t start_seg, const std::array<uint8_t, MAX_SEGMENTS> *const limit, segment_depth_array_t &depth)
{
	std::array<segnum_t, MAX_SEGMENTS> queue;
	int	head, tail;

	head = 0;
	tail = 0;

	visited_segment_bitarray_t visited;

	queue[tail++] = start_seg;
	visited[start_seg] = true;
	depth[start_seg] = 1;

	unsigned parent_depth;
	do {
		const auto curseg = queue[head++];
		auto &children = vcsegptr(curseg)->shared_segment::children;
		parent_depth = depth[curseg];

		for (const auto childnum : children)
		{
			if (childnum != segment_none && childnum != segment_exit)
				if (!limit || (*limit)[childnum])
				{
					auto &&v = visited[childnum];
					if (!v)
					{
						v = true;
						depth[childnum] = std::min(static_cast<unsigned>(std::numeric_limits<segment_depth_array_t::value_type>::max()), parent_depth + 1);
						queue[tail++] = childnum;
					}
				}
		}
	} while (head < tail);

	return parent_depth+1;
}

}

}

namespace dsx {

namespace {

struct automap : ::dcx::automap
{
	using ::dcx::automap::automap;
#if DXX_BUILD_DESCENT == 2
	color_t white_63;
	color_t blue_48;
	color_t wall_revealed_color;
#endif
	control_info controls;
	virtual window_event_result event_handler(const d_event &) override;
};

static void init_automap_subcanvas(grs_subcanvas &view, grs_canvas &container)
{
#if DXX_USE_STEREOSCOPIC_RENDER
	if (VR_stereo != StereoFormat::None) {
		const auto &&[x, y, w, h]{gr_build_stereo_viewport_window(VR_stereo, (SWIDTH / 23), (SHEIGHT / 6), (SWIDTH / 1.1), (SHEIGHT / 1.45))};
		gr_init_sub_canvas(view, container, x, y, w, h);
		return;
	}
#endif
#if DXX_BUILD_DESCENT == 1
	if (MacHog)
		gr_init_sub_canvas(view, container, 38*(SWIDTH/640.0), 77*(SHEIGHT/480.0), 564*(SWIDTH/640.0), 381*(SHEIGHT/480.0));
	else
#endif
		gr_init_sub_canvas(view, container, (SWIDTH/23), (SHEIGHT/6), (SWIDTH/1.1), (SHEIGHT/1.45));
}

static void automap_build_edge_list(automap &am, int add_all_edges);
}

}

namespace dcx {

#define MAX_EDGES_FROM_VERTS(v)     ((v)*4)

#define K_WALL_NORMAL_COLOR     BM_XRGB(29, 29, 29 )
#define K_WALL_DOOR_COLOR       BM_XRGB(5, 27, 5 )
#define K_WALL_DOOR_BLUE        BM_XRGB(0, 0, 31)
#define K_WALL_DOOR_GOLD        BM_XRGB(31, 31, 0)
#define K_WALL_DOOR_RED         BM_XRGB(31, 0, 0)
#define K_WALL_REVEALED_COLOR   BM_XRGB(0, 0, 25 ) //what you see when you have the full map powerup
#define K_HOSTAGE_COLOR         BM_XRGB(0, 31, 0 )
#define K_FONT_COLOR_20         BM_XRGB(20, 20, 20 )
#define K_GREEN_31              BM_XRGB(0, 31, 0)

uint8_t Automap_active;

namespace {

#ifndef NDEBUG
static uint8_t Automap_debug_show_all_segments;
#endif

static void automap_clear_visited(d_level_unique_automap_state &LevelUniqueAutomapState)
{
#ifndef NDEBUG
	Automap_debug_show_all_segments = 0;
#endif
	LevelUniqueAutomapState.Automap_visited = {};
}

static void init_automap_colors(automap &am)
{
	am.wall_normal_color = K_WALL_NORMAL_COLOR;
	am.wall_door_color = K_WALL_DOOR_COLOR;
	am.wall_door_blue = K_WALL_DOOR_BLUE;
	am.wall_door_gold = K_WALL_DOOR_GOLD;
	am.wall_door_red = K_WALL_DOOR_RED;
	am.hostage_color = K_HOSTAGE_COLOR;
	am.green_31 = K_GREEN_31;
}

void adjust_segment_limit(automap &am, const unsigned SegmentLimit)
{
	const auto &depth_array{am.depth_array};
	const auto predicate = [&depth_array, SegmentLimit](const segnum_t &e1) {
		return depth_array[e1] <= SegmentLimit;
	};
	for (auto &i : unchecked_partial_range(am.edges.get(), am.end_valid_edges))
	{
		// Unchecked for speed
		const auto &&range{unchecked_partial_range(i.segnum, i.num_faces)};
		if (std::any_of(range.begin(), range.end(), predicate))
			i.flags &= ~EF_TOO_FAR;
		else
			i.flags |= EF_TOO_FAR;
	}
}

}

}

namespace dsx {

namespace {

static void recompute_automap_segment_visibility(const d_level_unique_automap_state &LevelUniqueAutomapState, unsigned compute_depth_all_segments, const segnum_t initial_segnum, automap &am)
{
#ifndef NDEBUG
	if (Automap_debug_show_all_segments)
		compute_depth_all_segments = 1;
#endif
	if (cheats.fullautomap)
		compute_depth_all_segments = 1;
	automap_build_edge_list(am, compute_depth_all_segments);
	am.max_segments_away = set_segment_depths(initial_segnum, compute_depth_all_segments ? nullptr : &LevelUniqueAutomapState.Automap_visited, am.depth_array);
	am.segment_limit = am.max_segments_away;
	adjust_segment_limit(am, am.segment_limit);
}

#if DXX_BUILD_DESCENT == 2
#ifdef RELEASE
constexpr
#endif
static float MarkerScale{2.0f};
static unsigned Marker_index;

struct marker_delete_are_you_sure_menu : std::array<newmenu_item, 2>, newmenu
{
	using array_type = std::array<newmenu_item, 2>;
	d_level_unique_object_state &LevelUniqueObjectState;
	segment_array &Segments;
	d_marker_state &MarkerState;
	marker_delete_are_you_sure_menu(grs_canvas &canvas, d_level_unique_object_state &LevelUniqueObjectState, segment_array &Segments, d_marker_state &MarkerState) :
		array_type{{
			newmenu_item::nm_item_menu{TXT_YES},
			newmenu_item::nm_item_menu{TXT_NO},
		}},
		newmenu(menu_title{nullptr}, menu_subtitle{"Delete Marker?"}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(*static_cast<array_type *>(this), 0), canvas),
		LevelUniqueObjectState(LevelUniqueObjectState),
		Segments(Segments),
		MarkerState(MarkerState)
	{
	}
	virtual window_event_result event_handler(const d_event &) override;
	static std::pair<imobjidx_t *, game_marker_index> get_marker_object(d_marker_state &MarkerState);
	void handle_selected_yes() const;
};

window_event_result marker_delete_are_you_sure_menu::event_handler(const d_event &event)
{
	switch (event.type)
	{
		case event_type::newmenu_selected:
		{
			const auto citem{static_cast<const d_select_event &>(event).citem};
			if (citem == 0)
				/* User chose Yes */
				handle_selected_yes();
			/* The dialog should close after the user picks Yes or No.
			 */
			return window_event_result::close;
		}
		default:
			return newmenu::event_handler(event);
	}
}

std::pair<imobjidx_t *, game_marker_index> marker_delete_are_you_sure_menu::get_marker_object(d_marker_state &MarkerState)
{
	const auto HighlightMarker{MarkerState.HighlightMarker};
	if (!MarkerState.imobjidx.valid_index(HighlightMarker))
		return {nullptr, HighlightMarker};
	auto &mo{MarkerState.imobjidx[HighlightMarker]};
	return {mo == object_none ? nullptr : &mo, HighlightMarker};
}

void marker_delete_are_you_sure_menu::handle_selected_yes() const
{
	const auto &&[mo, HighlightMarker]{get_marker_object(MarkerState)};
	if (!mo)
		/* Check that the selected marker is still a valid object. */
		return;
	/* FIXME: this event should be sent to other players
	 * so that they remove the marker.
	 */
	obj_delete(LevelUniqueObjectState, Segments, LevelUniqueObjectState.Objects.vmptridx(std::exchange(*mo, object_none)));
	MarkerState.message[HighlightMarker] = {};
	MarkerState.HighlightMarker = game_marker_index::None;
}

void init_automap_colors(automap &am)
{
	::dcx::init_automap_colors(am);
	am.white_63 = gr_find_closest_color_current(63, 63, 63);
	am.blue_48 = gr_find_closest_color_current(0, 0, 48);
	am.wall_revealed_color = K_WALL_REVEALED_COLOR;
}
#endif

// Map movement defines
#define PITCH_DEFAULT 9000

}

/* MAX_DROP_MULTI_* must be a power of 2 for LastMarkerDropped to work
 * properly.
 */
#define	MAX_DROP_MULTI_COOP_0	2
#define	MAX_DROP_MULTI_COOP_1	4
#define MAX_DROP_MULTI_COOP_P	(max_numplayers > 4)
#define	MAX_DROP_MULTI_COOP	(MAX_DROP_MULTI_COOP_P ? MAX_DROP_MULTI_COOP_0 : MAX_DROP_MULTI_COOP_1)
#define	MAX_DROP_MULTI_COMPETITIVE	2
#define	MAX_DROP_SINGLE	9

#if DXX_BUILD_DESCENT == 2
marker_message_text_t Marker_input;

d_marker_state MarkerState;

game_marker_index convert_player_marker_index_to_game_marker_index(const game_mode_flags game_mode, const unsigned max_numplayers, const unsigned player_num, const player_marker_index player_marker_num)
{
	if (+(game_mode & GM_MULTI_COOP))
		return static_cast<game_marker_index>((player_num * MAX_DROP_MULTI_COOP) + static_cast<unsigned>(player_marker_num));
	if (+(game_mode & GM_MULTI))
		return static_cast<game_marker_index>((player_num * MAX_DROP_MULTI_COMPETITIVE) + static_cast<unsigned>(player_marker_num));
	return static_cast<game_marker_index>(player_marker_num);
}

unsigned d_marker_state::get_markers_per_player(const game_mode_flags game_mode, const unsigned max_numplayers)
{
	if (+(game_mode & GM_MULTI_COOP))
		return MAX_DROP_MULTI_COOP;
	if (+(game_mode & GM_MULTI))
		return MAX_DROP_MULTI_COMPETITIVE;
	return MAX_DROP_SINGLE;
}

xrange<player_marker_index> get_player_marker_range(const unsigned maxdrop)
{
	const auto base{player_marker_index::_0};
	return {base, static_cast<player_marker_index>(static_cast<unsigned>(base) + maxdrop)};
}

playernum_t get_marker_owner(const game_mode_flags game_mode, const game_marker_index gmi, const unsigned max_numplayers)
{
	const auto ugmi{static_cast<unsigned>(gmi)};
	if (+(game_mode & GM_MULTI_COOP))
	{
		/* This is split out to encourage the compiler to recognize that
		 * the divisor is a constant in every path, and in every path,
		 * the divisor was chosen to allow use of right shift in place
		 * of division.
		 */
		if (MAX_DROP_MULTI_COOP_P)
			return ugmi / MAX_DROP_MULTI_COOP_0;
		return ugmi / MAX_DROP_MULTI_COOP_1;
	}
	if (+(game_mode & GM_MULTI))
		return ugmi / MAX_DROP_MULTI_COMPETITIVE;
	return 0;
}

namespace {

xrange<game_marker_index> get_game_marker_range(const game_mode_flags game_mode, const unsigned max_numplayers, const unsigned player_num, const unsigned maxdrop)
{
	const auto base{convert_player_marker_index_to_game_marker_index(game_mode, max_numplayers, player_num, player_marker_index::_0)};
	return {base, static_cast<game_marker_index>(static_cast<unsigned>(base) + maxdrop)};
}

}
#endif

// -------------------------------------------------------------

namespace {

static void draw_all_edges(automap &am);
#if DXX_BUILD_DESCENT == 2
static void DrawMarkerNumber(grs_canvas &canvas, const automap &am, const game_marker_index gmi, const player_marker_index pmi, const g3_draw_sphere_point &BasePoint)
{
	struct xy
	{
		float x0, y0, x1, y1;
	};
	static constexpr enumerated_array<std::array<xy, 5>, 9, player_marker_index> sArray{{{
		{{
			{-0.25, 0.75, 0, 1},
			{0, 1, 0, -1},
			{-1, -1, 1, -1},
		}},
		{{
			{-1, 1, 1, 1},
			{1, 1, 1, 0},
			{-1, 0, 1, 0},
			{-1, 0, -1, -1},
			{-1, -1, 1, -1}
		}},
		{{
			{-1, 1, 1, 1},
			{1, 1, 1, -1},
			{-1, -1, 1, -1},
			{0, 0, 1, 0},
		}},
		{{
			{-1, 1, -1, 0},
			{-1, 0, 1, 0},
			{1, 1, 1, -1},
		}},
		{{
			{-1, 1, 1, 1},
			{-1, 1, -1, 0},
			{-1, 0, 1, 0},
			{1, 0, 1, -1},
			{-1, -1, 1, -1}
		}},
		{{
			{-1, 1, 1, 1},
			{-1, 1, -1, -1},
			{-1, -1, 1, -1},
			{1, -1, 1, 0},
			{-1, 0, 1, 0}
		}},
		{{
			{-1, 1, 1, 1},
			{1, 1, 1, -1},
		}},
		{{
			{-1, 1, 1, 1},
			{1, 1, 1, -1},
			{-1, -1, 1, -1},
			{-1, -1, -1, 1},
			{-1, 0, 1, 0}
		}},
		{{
			{-1, 1, 1, 1},
			{1, 1, 1, -1},
			{-1, 0, 1, 0},
			{-1, 0, -1, 1},
		 }}
	}}};
	static constexpr enumerated_array<uint_fast8_t, 9, player_marker_index> NumOfPoints{{{3, 5, 4, 3, 5, 5, 2, 5, 4}}};

	const auto color{gmi == MarkerState.HighlightMarker ? am.white_63 : am.blue_48};
	const g3_draw_line_context text_line_context{canvas, color};
	const auto scale_x{Matrix_scale.x};
	const auto scale_y{Matrix_scale.y};
	range_for (const auto &i, unchecked_partial_range(sArray[pmi], NumOfPoints[pmi]))
	{
		const auto ax0{i.x0 * MarkerScale};
		const auto ay0{i.y0 * MarkerScale};
		const auto ax1{i.x1 * MarkerScale};
		const auto ay1{i.y1 * MarkerScale};
		const auto build_displaced_point{[](const g3_rotated_point &BasePoint, const fix x, const fix y) {
			auto result{BasePoint};
			result.p3_vec.x += x;
			result.p3_vec.y += y;
			return result;
		}};
		g3_draw_line_point FromPoint{build_displaced_point(BasePoint, fixmul(fl2f(ax0), scale_x), fixmul(fl2f(ay0), scale_y))};
		g3_draw_line_point ToPoint{build_displaced_point(BasePoint, fixmul(fl2f(ax1), scale_x), fixmul(fl2f(ay1), scale_y))};
		g3_draw_line(text_line_context, FromPoint, ToPoint);
	}
}

static void DropMarker(fvmobjptridx &vmobjptridx, fvmsegptridx &vmsegptridx, const object &plrobj, const game_marker_index marker_num, const player_marker_index player_marker_num)
{
	auto &marker_objidx{MarkerState.imobjidx[marker_num]};
	if (marker_objidx != object_none)
		obj_delete(LevelUniqueObjectState, Segments, vmobjptridx(marker_objidx));

	marker_objidx = drop_marker_object(plrobj.pos, vmsegptridx(plrobj.segnum), plrobj.orient, marker_num);
#if DXX_USE_MULTIPLAYER
	if (+(Game_mode & GM_MULTI))
		multi_send_drop_marker(Player_num, plrobj.pos, player_marker_num, MarkerState.message[marker_num]);
#else
	(void)player_marker_num;
#endif
}

}

void DropBuddyMarker(object &objp)
{
	auto &Objects{LevelUniqueObjectState.Objects};
	auto &vmobjptridx{Objects.vmptridx};

	constexpr auto marker_num{game_marker_index::GuidebotDeathSite};
	static_assert(MarkerState.message.valid_index(marker_num), "not enough markers");

	auto &MarkerMessage{MarkerState.message[marker_num]};
	snprintf(MarkerMessage.data(), MarkerMessage.size(), "RIP: %s", static_cast<const char *>(PlayerCfg.GuidebotName));

	auto &marker_objidx{MarkerState.imobjidx[marker_num]};
	if (marker_objidx != object_none)
		obj_delete(LevelUniqueObjectState, Segments, vmobjptridx(marker_objidx));

	marker_objidx = drop_marker_object(objp.pos, vmsegptridx(objp.segnum), objp.orient, marker_num);
}

namespace {

static void DrawMarkers(const g3_instance_context &viewer_context, fvcobjptr &vcobjptr, grs_canvas &canvas, automap &am)
{
	static int cyc=10,cycdir=1;

	const auto game_mode{Game_mode};
	const auto max_numplayers{
#if DXX_USE_MULTIPLAYER
		Netgame.max_numplayers
#else
		1u
#endif
	};
	const auto maxdrop{MarkerState.get_markers_per_player(game_mode, max_numplayers)};
	const auto &&game_marker_range{get_game_marker_range(game_mode, max_numplayers, Player_num, maxdrop)};
	const auto &&player_marker_range{get_player_marker_range(maxdrop)};
	const auto &&zipped_marker_range{zip(game_marker_range, player_marker_range, unchecked_partial_range(&MarkerState.imobjidx[*game_marker_range.begin()], maxdrop))};
	const auto &&mb{zipped_marker_range.begin()};
	const auto &&me{zipped_marker_range.end()};
	auto iter{mb};
	/* Find the first marker object in the player's marker range that is
	 * not object_none.  If every marker object in the range is
	 * object_none, then there are no markers to draw, so return.
	 */
	for (;;)
	{
		auto &&[gmi, pmi, objidx]{*iter};
		(void)gmi;
		(void)pmi;
		if (objidx != object_none)
			break;
		if (++ iter == me)
			return;
	}
	/* A marker was found, so at least one marker will be drawn.  Set up
	 * colors for the markers.
	 */
	const auto current_cycle_color{cyc};
	const std::array<color_t, 3> colors{{
		gr_find_closest_color_current(current_cycle_color, 0, 0),
		gr_find_closest_color_current(current_cycle_color + 10, 0, 0),
		gr_find_closest_color_current(current_cycle_color + 20, 0, 0),
	}};
	for (; iter != me; ++iter)
	{
		auto &&[gmi, pmi, objidx]{*iter};
		if (objidx != object_none)
		{
			g3_draw_sphere_point sphere_point{viewer_context, vcobjptr(objidx)->pos};
			static constexpr fix MARKER_SPHERE_SIZE{0x58000};
			g3_draw_sphere(canvas, sphere_point, MARKER_SPHERE_SIZE, colors[0]);
			g3_draw_sphere(canvas, sphere_point, MARKER_SPHERE_SIZE / 2, colors[1]);
			g3_draw_sphere(canvas, sphere_point, MARKER_SPHERE_SIZE / 4, colors[2]);
			DrawMarkerNumber(canvas, am, gmi, pmi, sphere_point);
		}
	}

	if (cycdir)
		cyc+=2;
	else
		cyc-=2;

	if (cyc>43)
	{
		cyc=43;
		cycdir=0;
	}
	else if (cyc<10)
	{
		cyc=10;
		cycdir=1;
	}
}

static void ClearMarkers()
{
	static_cast<d_marker_object_numbers &>(MarkerState) = {};
	MarkerState.message = {};
}
#endif

}

void automap_clear_visited(d_level_unique_automap_state &LevelUniqueAutomapState)
{
#if DXX_BUILD_DESCENT == 2
	ClearMarkers();
#endif
	::dcx::automap_clear_visited(LevelUniqueAutomapState);
}

namespace {

static void draw_player(const g3_instance_context &viewer_context, const g3_draw_line_context &context, const object_base &obj)
{
	// Draw Console player -- shaped like a ellipse with an arrow.
	g3_draw_sphere_point sphere_point{viewer_context, obj.pos};
	const auto obj_size{obj.size};
	g3_draw_sphere(context.canvas, sphere_point, obj_size, context.color);

	// Draw shaft of arrow
	const auto head_pos{vm_vec_scale_add(obj.pos, obj.orient.fvec, obj_size * 2)};
	{
		g3_draw_line_point arrow_point{viewer_context, vm_vec_scale_add(obj.pos, obj.orient.fvec, obj_size * 3)};
	g3_draw_line(context, sphere_point, arrow_point);

	// Draw right head of arrow
		g3_draw_line_point lhead_point{viewer_context, /* rhead_pos = */ vm_vec_scale_add(head_pos, obj.orient.rvec, obj_size)};
		g3_draw_line(context, arrow_point, lhead_point);

	// Draw left head of arrow
		g3_draw_line_point rhead_point{viewer_context, /* lhead_pos = */ vm_vec_scale_add(head_pos, obj.orient.rvec, -obj_size)};
		g3_draw_line(context, arrow_point, rhead_point);
	}

	// Draw player's up vector
	g3_draw_line_point arrow_point{viewer_context, /* arrow_pos = */ vm_vec_scale_add(obj.pos, obj.orient.uvec, obj_size * 2)};
	g3_draw_line(context, sphere_point, arrow_point);
}

#if DXX_BUILD_DESCENT == 2
//name for each group.  maybe move somewhere else
constexpr char system_name[][17] = {
			"Zeta Aquilae",
			"Quartzon System",
			"Brimspark System",
			"Limefrost Spiral",
			"Baloris Prime",
			"Omega System"};
#endif

static void name_frame(grs_canvas &canvas, automap &am, int dx = 0, int dy = 0)
{
	gr_set_fontcolor(canvas, am.green_31, -1);
	char		name_level_left[128];

	auto &game_font{*GAME_FONT};
#if DXX_BUILD_DESCENT == 1
	const char *name_level;
	if (Current_level_num > 0)
	{
		snprintf(name_level_left, sizeof(name_level_left), "%s %i: %s",TXT_LEVEL, Current_level_num, static_cast<const char *>(Current_level_name));
		name_level = name_level_left;
	}
	else
		name_level = Current_level_name;

	gr_string(canvas, game_font, dx + (SWIDTH / 64), dy + (SHEIGHT / 48), name_level);
#elif DXX_BUILD_DESCENT == 2
	char	name_level_right[128];
	if (Current_level_num > 0)
		snprintf(name_level_left, sizeof(name_level_left), "%s %i",TXT_LEVEL, Current_level_num);
	else
		snprintf(name_level_left, sizeof(name_level_left), "Secret Level %i",-Current_level_num);

	const char *const current_level_name{Current_level_name};
	if (PLAYING_BUILTIN_MISSION && Current_level_num > 0)
		snprintf(name_level_right, sizeof(name_level_right), "%s %d: %s", system_name[(Current_level_num-1)/4], ((Current_level_num - 1) % 4) + 1, current_level_name);
	else
		snprintf(name_level_right, sizeof(name_level_right), " %s", current_level_name);

	gr_string(canvas, game_font, dx + (SWIDTH / 64), dy + (SHEIGHT / 48), name_level_left);
	const auto &&[wr, h]{gr_get_string_size(game_font, name_level_right)};
	gr_string(canvas, game_font, dx + canvas.cv_bitmap.bm_w - wr - (SWIDTH / 64), dy + (SHEIGHT / 48), name_level_right, wr, h);
#endif
}

static void automap_apply_input(automap &am, const vms_matrix &plrorient, const vms_vector &plrpos)
{
	constexpr int SLIDE_SPEED{350};
	constexpr int ZOOM_SPEED_FACTOR{500};	//(1500)
	constexpr int ROT_SPEED_DIVISOR{115000};
	if (PlayerCfg.AutomapFreeFlight)
	{
		if (am.controls.state.fire_primary)
		{
			// Reset orientation
			am.controls.state.fire_primary = 0;
			am.viewMatrix = plrorient;
			am.view_position = vm_vec_scale_add(plrpos, am.viewMatrix.fvec, -am.ZOOM_DEFAULT);
		}
		
		if (am.controls.pitch_time || am.controls.heading_time || am.controls.bank_time)
		{
			vms_angvec tangles;

			tangles.p = fixdiv(am.controls.pitch_time, ROT_SPEED_DIVISOR);
			tangles.h = fixdiv(am.controls.heading_time, ROT_SPEED_DIVISOR);
			tangles.b = fixdiv(am.controls.bank_time, ROT_SPEED_DIVISOR * 2);

			const auto &&tempm{vm_angles_2_matrix(tangles)};
			am.viewMatrix = vm_matrix_x_matrix(am.viewMatrix, tempm);
			check_and_fix_matrix(am.viewMatrix);
		}
		
		if (const auto forward_thrust_time{am.controls.forward_thrust_time}, vertical_thrust_time{am.controls.vertical_thrust_time}, sideways_thrust_time{am.controls.sideways_thrust_time}; forward_thrust_time || vertical_thrust_time || sideways_thrust_time)
		{
			auto view_position{am.view_position};
			/* If the thrust time is 0, then `vm_vec_scale_add2` degrades to
			 * multiplying the second term by 0 and adding the 0 result to the
			 * first term.  Avoid the wasted multiply+write by checking for a
			 * zero thrust time.
			 */
			if (forward_thrust_time)
				vm_vec_scale_add2(view_position, am.viewMatrix.fvec, forward_thrust_time * ZOOM_SPEED_FACTOR);
			if (vertical_thrust_time)
				vm_vec_scale_add2(view_position, am.viewMatrix.uvec, vertical_thrust_time * SLIDE_SPEED);
			if (sideways_thrust_time)
				vm_vec_scale_add2(view_position, am.viewMatrix.rvec, sideways_thrust_time * SLIDE_SPEED);
			
			// Crude wrapping check
			constexpr fix upper_bound{F1_0 * 32000}, lower_bound{-upper_bound};
			am.view_position.x = std::clamp(view_position.x, lower_bound, upper_bound);
			am.view_position.y = std::clamp(view_position.y, lower_bound, upper_bound);
			am.view_position.z = std::clamp(view_position.z, lower_bound, upper_bound);
		}
	}
	else
	{
		if (am.controls.state.fire_primary)
		{
			// Reset orientation
			am.viewDist = am.ZOOM_DEFAULT;
			am.tangles.p = PITCH_DEFAULT;
			am.tangles.h  = 0;
			am.tangles.b  = 0;
			am.view_target = plrpos;
			am.controls.state.fire_primary = 0;
		}

		if (am.controls.forward_thrust_time)
		{
			constexpr fix ZOOM_MIN_VALUE{i2f(20*5)};
			constexpr fix ZOOM_MAX_VALUE{i2f(20*100)};
			am.viewDist = std::clamp(am.viewDist - (am.controls.forward_thrust_time * ZOOM_SPEED_FACTOR), ZOOM_MIN_VALUE, ZOOM_MAX_VALUE);
		}
		am.tangles.p += fixdiv(am.controls.pitch_time, ROT_SPEED_DIVISOR);
		am.tangles.h += fixdiv(am.controls.heading_time, ROT_SPEED_DIVISOR);
		am.tangles.b += fixdiv(am.controls.bank_time, ROT_SPEED_DIVISOR * 2);

		if (am.controls.vertical_thrust_time || am.controls.sideways_thrust_time)
		{
			vms_vector old_vt{am.view_target};
			vms_angvec tangles1{am.tangles};
			const auto &&tempm{vm_angles_2_matrix(tangles1)};
			vm_matrix_x_matrix(am.viewMatrix, plrorient, tempm);
			vm_vec_scale_add2(am.view_target, am.viewMatrix.uvec, am.controls.vertical_thrust_time * SLIDE_SPEED);
			vm_vec_scale_add2(am.view_target, am.viewMatrix.rvec, am.controls.sideways_thrust_time * SLIDE_SPEED);
			if (vm_vec_dist_quick(am.view_target, plrpos) > i2f(1000))
				am.view_target = old_vt;
		}

		const auto &&tempm{vm_angles_2_matrix(am.tangles)};
		vm_matrix_x_matrix(am.viewMatrix, plrorient, tempm);
	}
}

static void draw_automap(fvcobjptr &vcobjptr, automap &am, fix eye = 0)
{
#if DXX_USE_STEREOSCOPIC_RENDER
	int sw = SWIDTH;
	int sh = SHEIGHT;
	const auto &&[dx, dy]{[](const StereoFormat stereo, const int eye) -> std::pair<int, int> {
		// left eye viewport origin
		if (eye <= 0) {
			return {0, gr_build_stereo_viewport_offset_left_eye(stereo, 0)};
		}
		return gr_build_stereo_viewport_offset_right_eye(stereo, 0, 0, eye);
	}(VR_stereo, eye)};
	const auto &&[dw, dh]{gr_build_stereo_viewport_size(VR_stereo, sw, sh)};
	#define SCREEN_SIZE_STEREO 	grd_curscreen->set_screen_width_height(dw, dh)
	#define SCREEN_SIZE_NORMAL 	grd_curscreen->set_screen_width_height(sw, sh)
#else
	#define SCREEN_SIZE_STEREO 	// no-op
	#define SCREEN_SIZE_NORMAL 	// no-op
	const int dx = 0, dy = 0;	// no x,y adjust
#endif

	if ( am.leave_mode==0 && am.controls.state.automap && (timer_query()-am.entry_time)> /* LEAVE_TIME = */ 0x4000)
		am.leave_mode = 1;

	{
		auto &canvas{am.w_canv};
		if (eye <= 0)
			show_fullscr(canvas, am.automap_background);
		gr_set_fontcolor(canvas, BM_XRGB(20, 20, 20), -1);
		SCREEN_SIZE_STEREO;
	{
		int x, y;
#if DXX_BUILD_DESCENT == 1
	if (MacHog)
			x = 80 * (SWIDTH / 640.), y = 36 * (SHEIGHT / 480.);
	else
#endif
			x = SWIDTH / 8, y = SHEIGHT / 16;
		x += dx, y += dy;
		gr_string(canvas, *HUGE_FONT, x, y, TXT_AUTOMAP);
	}
	{
		int x;
		int y0, y1, y2;
#if DXX_BUILD_DESCENT == 1
		const auto s1{TXT_SLIDE_UPDOWN};
		const auto &s2{"F9/F10 Changes viewing distance"};
	if (!MacHog)
	{
			x = SWIDTH / 4.923;
			y0 = SHEIGHT / 1.126;
			y1 = SHEIGHT / 1.083;
			y2 = SHEIGHT / 1.043;
	}
	else
	{
		// for the Mac automap they're shown up the top, hence the different layout
			x = 265 * (SWIDTH / 640.);
			y0 = 27 * (SHEIGHT / 480.);
			y1 = 44 * (SHEIGHT / 480.);
			y2 = 61 * (SHEIGHT / 480.);
	}
#elif DXX_BUILD_DESCENT == 2
		const auto &s1{"F9/F10 Changes viewing distance"};
		const auto s2{TXT_AUTOMAP_MARKER};
		x = SWIDTH / 10.666;
		y0 = SHEIGHT / 1.126;
		y1 = SHEIGHT / 1.083;
		y2 = SHEIGHT / 1.043;
#endif
		x += dx, y0 += dy, y1 += dy, y2 += dy;
		auto &game_font{*GAME_FONT};
		gr_string(canvas, game_font, x, y0, TXT_TURN_SHIP);
		gr_string(canvas, game_font, x, y1, s1);
		gr_string(canvas, game_font, x, y2, s2);
	}

	}
	SCREEN_SIZE_NORMAL;
	auto &canvas{am.automap_view};

	if (eye == 0)
		gr_clear_canvas(canvas, BM_XRGB(0,0,0));

	g3_start_frame(canvas);
	render_start_frame();

#if DXX_USE_STEREOSCOPIC_RENDER
	// select stereo viewport/transform/buffer per left/right eye
	if (VR_stereo != StereoFormat::None)
		g3_stereo_frame(eye, VR_eye_offset);
#endif

	if (!PlayerCfg.AutomapFreeFlight)
		am.view_position = vm_vec_scale_add(am.view_target, am.viewMatrix.fvec, -am.viewDist);

	am.view_position.x += eye;	// stereo viewpoint offset
	g3_set_view_matrix(am.view_position,am.viewMatrix,am.zoom);

	draw_all_edges(am);

	// Draw player...
	const auto &self_ship_rgb{player_rgb[get_player_or_team_color(Netgame, Game_mode, Player_num)]};
	const auto closest_color{BM_XRGB(self_ship_rgb.r, self_ship_rgb.g, self_ship_rgb.b)};
	const g3_instance_context viewer_context{View_matrix, View_position};
	draw_player(viewer_context, g3_draw_line_context{canvas, closest_color}, vcobjptr(get_local_player().objnum));

#if DXX_BUILD_DESCENT == 2
	DrawMarkers(viewer_context, vcobjptr, canvas, am);
#endif
	
	// Draw player(s)...
#if DXX_USE_MULTIPLAYER
	const unsigned show_all_players{+(Game_mode & GM_MULTI_COOP) || underlying_value(Netgame.game_flag & netgame_rule_flags::show_all_players_on_automap)};
	if (show_all_players || +(Game_mode & GM_TEAM))
	{
		const auto local_player_team{multi_get_team_from_player(Netgame, Player_num)};
		const auto n_players{N_players};
		for (const auto iu : xrange(n_players))
		{
			const auto i{static_cast<playernum_t>(iu)};
			if (i == Player_num)
				continue;
			if (show_all_players || local_player_team == multi_get_team_from_player(Netgame, i))
			{
				auto &plr{*vcplayerptr(i)};
				auto &objp{*vcobjptr(plr.objnum)};
				if (objp.type == object_type::OBJ_PLAYER)
				{
					const auto &other_ship_rgb{player_rgb[get_player_or_team_color(Netgame, Game_mode, i)]};
					draw_player(viewer_context, g3_draw_line_context{canvas, BM_XRGB(other_ship_rgb.r, other_ship_rgb.g, other_ship_rgb.b)}, objp);
				}
			}
		}
	}
#endif

	for (auto &obj : vcobjptr)
	{
		switch(obj.type)
		{
		case object_type::OBJ_HOSTAGE:
			g3_draw_sphere(canvas, /* sphere_point = */ g3_rotate_point(obj.pos), obj.size, am.hostage_color);
			break;
		case object_type::OBJ_POWERUP:
			if (LevelUniqueAutomapState.Automap_visited[obj.segnum]
#ifndef NDEBUG
				|| Automap_debug_show_all_segments
#endif
				)
			{
				unsigned r, g, b;
				if (const auto id{get_powerup_id(obj)}; id == powerup_type_t::POW_KEY_RED)
					r = 63 * 2, g = 5 * 2, b = 5 * 2;
				else if (id == powerup_type_t::POW_KEY_BLUE)
					r = 5 * 2, g = 5 * 2, b = 63 * 2;
				else if (id == powerup_type_t::POW_KEY_GOLD)
					r = 63 * 2, g = 63 * 2, b = 10 * 2;
				else
					break;
				g3_draw_sphere(canvas, /* sphere_point = */ g3_rotate_point(obj.pos), obj.size * 4, /* color = */ gr_find_closest_color(r, g, b));
			}
			break;
			default:
				break;
		}
	}

	g3_end_frame();

	SCREEN_SIZE_STEREO;
	name_frame(canvas, am, dx, dy);
	SCREEN_SIZE_NORMAL;

#if DXX_BUILD_DESCENT == 2
	{
		const auto HighlightMarker{MarkerState.HighlightMarker};
		if (MarkerState.message.valid_index(HighlightMarker))
		{
			auto &p{MarkerState.message[HighlightMarker][0u]};
			gr_printf(canvas, *canvas.cv_font, (SWIDTH / 64), (SHEIGHT / 18), "Marker %u%c %s", static_cast<unsigned>(HighlightMarker) + 1, p ? ':' : 0, &p);
		}
	}
#endif

	if (PlayerCfg.MouseFlightSim && PlayerCfg.MouseFSIndicator)
	{
		const auto gwidth{canvas.cv_bitmap.bm_w};
		const auto gheight{canvas.cv_bitmap.bm_h};
		auto &raw_mouse_axis{am.controls.raw_mouse_axis};
		show_mousefs_indicator(canvas, raw_mouse_axis[0], raw_mouse_axis[1], raw_mouse_axis[2], gwidth - (gheight / 8), gheight - (gheight / 8), gheight / 5);
	}

	am.t2 = timer_query();
	const auto vsync{CGameCfg.VSync};
	const auto bound{F1_0 / (vsync ? MAXIMUM_FPS : CGameArg.SysMaxFPS)};
	const auto may_sleep{!CGameArg.SysNoNiceFPS && !vsync};
	const auto multiplayer{+(Game_mode & GM_MULTI)};
	while (am.t2 - am.t1 < bound) // ogl is fast enough that the automap can read the input too fast and you start to turn really slow.  So delay a bit (and free up some cpu :)
	{
		if (multiplayer)
			multi_do_frame(); // during long wait, keep packets flowing
		if (may_sleep)
			timer_delay(F1_0>>8);
		am.t2 = timer_update();
	}
	if (am.pause_game)
	{
		FrameTime=am.t2-am.t1;
		calc_d_tick();
	}
	am.t1 = am.t2;
}

#if DXX_BUILD_DESCENT == 1
#define MAP_BACKGROUND_FILENAME (((SWIDTH>=640&&SHEIGHT>=480) && PHYSFS_exists("maph.pcx")) ? "maph.pcx" : "map.pcx")
#elif DXX_BUILD_DESCENT == 2
#define MAP_BACKGROUND_FILENAME ((HIRESMODE && PHYSFS_exists("mapb.pcx")) ? "mapb.pcx" : "map.pcx")
#endif

static void recompute_automap_segment_visibility(const d_level_unique_automap_state &LevelUniqueAutomapState, const object &plrobj, automap &am)
{
	recompute_automap_segment_visibility(LevelUniqueAutomapState, plrobj.ctype.player_info.powerup_flags & PLAYER_FLAGS_MAP_ALL, plrobj.segnum, am);
}

static window_event_result automap_key_command(const d_event &event, automap &am)
{
#if DXX_BUILD_DESCENT == 1 || !defined(NDEBUG)
	auto &Objects{LevelUniqueObjectState.Objects};
	auto &vmobjptr{Objects.vmptr};
#endif
	int c{event_key_get(event)};

	switch (c)
	{
#if DXX_USE_SCREENSHOT
		case KEY_PRINT_SCREEN: {
			gr_set_default_canvas();
			save_screen_shot(1);
			return window_event_result::handled;
		}
#endif
		case KEY_ESC:
			if (am.leave_mode == 0)
			{
				return window_event_result::close;
			}
			return window_event_result::handled;
#if DXX_BUILD_DESCENT == 1
		case KEY_ALTED+KEY_F:           // Alt+F shows full map, if cheats enabled
			if (cheats.enabled) 	 
			{
				cheats.fullautomap = !cheats.fullautomap;
				// if cheat of map powerup, work with full depth
				auto &plrobj{get_local_plrobj()};
				recompute_automap_segment_visibility(LevelUniqueAutomapState, plrobj, am);
			}
			return window_event_result::handled;
#endif
#ifndef NDEBUG
		case KEY_DEBUGGED+KEY_F: 	{
				Automap_debug_show_all_segments = !Automap_debug_show_all_segments;
				auto &plrobj{get_local_plrobj()};
				recompute_automap_segment_visibility(LevelUniqueAutomapState, plrobj, am);
			}
			return window_event_result::handled;
#endif
		case KEY_F9:
			if (am.segment_limit > 1)
			{
				am.segment_limit--;
				adjust_segment_limit(am, am.segment_limit);
			}
			return window_event_result::handled;
		case KEY_F10:
			if (am.segment_limit < am.max_segments_away) 	{
				am.segment_limit++;
				adjust_segment_limit(am, am.segment_limit);
			}
			return window_event_result::handled;
#if DXX_BUILD_DESCENT == 2
		case KEY_1:
		case KEY_2:
		case KEY_3:
		case KEY_4:
		case KEY_5:
		case KEY_6:
		case KEY_7:
		case KEY_8:
		case KEY_9:
		case KEY_0:
			{
				const auto game_mode{Game_mode};
				const auto max_numplayers{Netgame.max_numplayers};
				const auto maxdrop{MarkerState.get_markers_per_player(game_mode, max_numplayers)};
				const uint8_t marker_num = c - KEY_1;
				if (marker_num <= maxdrop)
				{
					const auto gmi{convert_player_marker_index_to_game_marker_index(game_mode, max_numplayers, Player_num, player_marker_index{marker_num})};
					if (MarkerState.imobjidx[gmi] != object_none)
						MarkerState.HighlightMarker = gmi;
				}
				else
					MarkerState.HighlightMarker = game_marker_index::None;
			}
			return window_event_result::handled;
		case KEY_D+KEY_CTRLED:
			{
				if (const auto &&[mo, HighlightMarker]{marker_delete_are_you_sure_menu::get_marker_object(MarkerState)}; mo == nullptr)
				{
					(void)HighlightMarker;
					/* If the selected index is not a valid marker, do
					 * not offer to delete anything.
					 */
					return window_event_result::handled;
				}
				auto menu{window_create<marker_delete_are_you_sure_menu>(grd_curscreen->sc_canvas, LevelUniqueObjectState, Segments, MarkerState)};
				(void)menu;
			}
			return window_event_result::handled;
#ifndef RELEASE
		case KEY_F11:	//KEY_COMMA:
			if (MarkerScale>.5)
				MarkerScale-=.5;
			return window_event_result::handled;
		case KEY_F12:	//KEY_PERIOD:
			if (MarkerScale<30.0)
				MarkerScale+=.5;
			return window_event_result::handled;
#endif
#endif
	}
	return window_event_result::ignored;
}

static window_event_result automap_process_input(const d_event &event, automap &am)
{
	kconfig_read_controls(am.controls, event, 1);

	if (!am.controls.state.automap && am.leave_mode == 1)
	{
		return window_event_result::close;
	}
	if (am.controls.state.automap)
	{
		am.controls.state.automap = 0;
		if (am.leave_mode == 0)
		{
			return window_event_result::close;
		}
	}
	return window_event_result::ignored;
}

}

window_event_result automap::event_handler(const d_event &event)
{
	auto &Objects{LevelUniqueObjectState.Objects};
	auto &vcobjptr{Objects.vcptr};
	auto &vmobjptr{Objects.vmptr};
	switch (event.type)
	{
		case event_type::window_activated:
			game_flush_inputs(controls);
			event_toggle_focus(1);
			key_toggle_repeat(1);
			break;

		case event_type::window_deactivated:
			event_toggle_focus(0);
			key_toggle_repeat(1);
			break;

#if SDL_MAJOR_VERSION == 2
		case event_type::window_resize:
			init_automap_subcanvas(automap_view, grd_curscreen->sc_canvas);
			break;
#endif

		case event_type::idle:
#if DXX_MAX_BUTTONS_PER_JOYSTICK
		case event_type::joystick_button_up:
		case event_type::joystick_button_down:
#endif
#if DXX_MAX_AXES_PER_JOYSTICK
		case event_type::joystick_moved:
#endif
		case event_type::mouse_button_up:
		case event_type::mouse_button_down:
		case event_type::mouse_moved:
		case event_type::key_release:
			return automap_process_input(event, *this);
		case event_type::key_command:
		{
			window_event_result kret{automap_key_command(event, *this)};
			if (kret == window_event_result::ignored)
				kret = automap_process_input(event, *this);
			return kret;
		}
		case event_type::window_draw:
			{
				auto &plrobj{get_local_plrobj()};
				automap_apply_input(*this, plrobj.orient, plrobj.pos);
			}
#if DXX_USE_STEREOSCOPIC_RENDER
			if (VR_stereo != StereoFormat::None) {
				draw_automap(vcobjptr, *this, -VR_eye_width);
				draw_automap(vcobjptr, *this,  VR_eye_width);
			}
			else
#endif
			draw_automap(vcobjptr, *this);
			break;
		case event_type::window_close:
			if (!pause_game)
				ConsoleObject->mtype.phys_info.flags |= old_wiggle;		// Restore wiggle
			event_toggle_focus(0);
			key_toggle_repeat(1);
			/* grd_curcanv points to `automap_view`, so grd_curcanv
			 * would become a dangling pointer after the call to delete.
			 * Redirect it to the default screen to avoid pointing to
			 * freed memory.  Setting grd_curcanv to nullptr would be
			 * better, but some code assumes that grd_curcanv is never
			 * nullptr, so instead set it to the default canvas.
			 * Eventually, grd_curcanv will be removed entirely.
			 */
			gr_set_default_canvas();
			Game_wind->set_visible(1);
			Automap_active = 0;
#if DXX_USE_MULTIPLAYER
			if (+(Game_mode & GM_MULTI))
				multi_send_msgsend_state(msgsend_state::none);
#endif
			return window_event_result::ignored;	// continue closing

		case event_type::loop_begin_loop:
			kconfig_begin_loop(controls);
			break;
		case event_type::loop_end_loop:
			kconfig_end_loop(controls, FrameTime);
			break;

		default:
			return window_event_result::ignored;
	}
	return window_event_result::handled;
}

void do_automap()
{
	auto &Objects{LevelUniqueObjectState.Objects};
	auto &vmobjptr{Objects.vmptr};
	palette_array_t pal;
	auto am{window_create<automap>(grd_curscreen->sc_canvas, 0, 0, SWIDTH, SHEIGHT)};
	const auto max_edges{LevelSharedSegmentState.Num_segments * 12};
	am->max_edges = max_edges;
	am->edges = std::make_unique<Edge_info[]>(max_edges);
	am->drawingListBright = std::make_unique<Edge_info *[]>(max_edges);

	init_automap_colors(*am);
	am->pause_game = !(+(Game_mode & GM_MULTI) && (!Endlevel_sequence)); // Set to 1 if everything is paused during automap...No pause during net.

	if (am->pause_game) {
		Game_wind->set_visible(0);
	}
	else
	{
		am->old_wiggle = ConsoleObject->mtype.phys_info.flags & PF_WIGGLE;	// Save old wiggle
		ConsoleObject->mtype.phys_info.flags &= ~PF_WIGGLE;		// Turn off wiggle
	}

	//Max_edges = min(MAX_EDGES_FROM_VERTS(Num_vertices),MAX_EDGES); //make maybe smaller than max

	gr_set_default_canvas();

	auto &plrobj{get_local_plrobj()};
	am->viewMatrix = plrobj.orient;
	am->tangles.p = PITCH_DEFAULT;
	am->tangles.h  = 0;
	am->tangles.b  = 0;
	am->view_target = plrobj.pos;
	
	if (PlayerCfg.AutomapFreeFlight)
		am->view_position = vm_vec_scale_add(plrobj.pos, am->viewMatrix.fvec, -am->ZOOM_DEFAULT);

	am->t1 = am->entry_time = timer_query();
	am->t2 = am->t1;

	//Fill in Automap_visited from Objects[Players[Player_num].objnum].segnum
	recompute_automap_segment_visibility(LevelUniqueAutomapState, plrobj, *am);

	// ZICO - code from above to show frame in OGL correctly. Redundant, but better readable.
	// KREATOR - Now applies to all platforms so double buffering is supported
	{
		const auto pcx_error{pcx_read_bitmap_or_default(MAP_BACKGROUND_FILENAME, am->automap_background, pal)};
		if (pcx_error != pcx_result::SUCCESS)
			con_printf(CON_URGENT, DXX_STRINGIZE_FL(__FILE__, __LINE__, "automap: File %s - PCX error: %s"), MAP_BACKGROUND_FILENAME, pcx_errormsg(pcx_error));
		gr_remap_bitmap_good(am->automap_background, pal, -1, -1);
	}
	init_automap_subcanvas(am->automap_view, grd_curscreen->sc_canvas);

	gr_palette_load( gr_palette );
	Automap_active = 1;
#if DXX_USE_MULTIPLAYER
	if (+(Game_mode & GM_MULTI))
		multi_send_msgsend_state(msgsend_state::automap);
#endif
}

namespace {

void draw_all_edges(automap &am)
{
	auto &canvas{am.automap_view};
	auto &LevelSharedVertexState{LevelSharedSegmentState.get_vertex_state()};
	auto &Vertices{LevelSharedVertexState.get_vertices()};
	unsigned nbright{0};
	fix min_distance{INT32_MAX};

	auto &vcvertptr{Vertices.vcptr};
	range_for (auto &i, unchecked_partial_range(am.edges.get(), am.end_valid_edges))
	{
		const auto e{&i};
		if (!(e->flags & EF_USED)) continue;

		if ( e->flags & EF_TOO_FAR) continue;

		if (e->flags&EF_FRONTIER) { 	// A line that is between what we have seen and what we haven't
			if ((!(e->flags & EF_SECRET)) && (e->color == am.wall_normal_color))
				continue; 	// If a line isn't secret and is normal color, then don't draw it
		}
		if (const fix distance{Segment_points[e->verts[1]].p3_vec.z}; min_distance > distance)
			min_distance = distance;

		if (rotate_list(vcvertptr, e->verts).uand == clipping_code::None)
		{			//all off screen?
			uint8_t nfacing{};
			uint8_t nnfacing{};
			auto &tv1{*vcvertptr(e->verts[0])};
			int j{0};
			while( j<e->num_faces && (nfacing==0 || nnfacing==0) )	{
				if (!g3_check_normal_facing(tv1, vcsegptr(e->segnum[j])->shared_segment::sides[e->sides[j]].normals[0]))
					nfacing++;
				else
					nnfacing++;
				j++;
			}

			if ( nfacing && nnfacing )	{
				// a contour line
				am.drawingListBright[nbright++] = e;
			} else if ( e->flags&(EF_DEFINING|EF_GRATE) )	{
				if ( nfacing == 0 )	{
					const uint8_t color = (e->flags & EF_NO_FADE)
						? e->color
						: gr_fade_table[(gr_fade_level{8})][e->color];
					g3_draw_line(g3_draw_line_context{canvas, color}, Segment_points[e->verts[0]], Segment_points[e->verts[1]]);
				} 	else {
					am.drawingListBright[nbright++] = e;
				}
			}
		}
	}
		
	if ( min_distance < 0 ) min_distance = 0;

	/* Sort the bright ones using a projection to extract the z coordinate of
	 * the rotated point.
	 */
	const auto &&range{unchecked_partial_range(am.drawingListBright.get(), nbright)};
	std::ranges::sort(range, {}, [](const Edge_info *const a) { return Segment_points[a->verts[0]].p3_vec.z; });
	// Draw the bright ones
	range_for (const auto e, range)
	{
		const auto p1{&Segment_points[e->verts[0]]};
		const auto p2{&Segment_points[e->verts[1]]};
		fix dist{p1->p3_vec.z - min_distance};
		// Make distance be 1.0 to 0.0, where 0.0 is 10 segments away;
		if ( dist < 0 ) dist=0;
		if ( dist >= am.farthest_dist ) continue;

		const auto color{(e->flags & EF_NO_FADE)
			? e->color
			: gr_fade_table[static_cast<gr_fade_level>(f2i((F1_0 - fixdiv(dist, am.farthest_dist)) * 31))][e->color]
		};
		g3_draw_line(g3_draw_line_context{canvas, color}, *p1, *p2);
	}
}

//==================================================================
//
// All routines below here are used to build the Edge list
//
//==================================================================

//finds edge, filling in edge_ptr. if found old edge, returns index, else return -1
static std::pair<Edge_info &, std::size_t> automap_find_edge(automap &am, const vertnum_t v0, const vertnum_t v1)
{
	const auto &&hash_object{std::hash<vertnum_t>{}};
	const auto initial_hash_slot{(hash_object(v0) ^ (hash_object(v1) << 10)) % am.max_edges};

	for (auto current_hash_slot{initial_hash_slot};;)
	{
		auto &e{am.edges[current_hash_slot]};
		const auto ev0{e.verts[0]};
		const auto ev1{e.verts[1]};
		if (e.num_faces == 0)
			return {e, current_hash_slot};
		if (v1 == ev1 && v0 == ev0)
			return {e, UINT32_MAX};
		else {
			if (++ current_hash_slot == am.max_edges)
				current_hash_slot = 0;
			if (current_hash_slot == initial_hash_slot)
				throw std::runtime_error("edge list full: search wrapped without finding a free slot");
		}
	}
}

static void add_one_edge(automap &am, vertnum_t va, vertnum_t vb, const uint8_t color, const sidenum_t side, const segnum_t segnum, const uint8_t flags)
{
	if (am.num_edges >= am.max_edges)
	{
		// GET JOHN! (And tell him that his
		// MAX_EDGES_FROM_VERTS formula is hosed.)
		// If he's not around, save the mine,
		// and send him  mail so he can look
		// at the mine later. Don't modify it.
		// This is important if this happens.
		Int3();		// LOOK ABOVE!!!!!!
		return;
	}

	if ( va > vb )	{
		std::swap(va, vb);
	}
	const auto &&ef{automap_find_edge(am, va, vb)};
	const auto e{&ef.first};
		
	if (ef.second != UINT32_MAX)
	{
		e->verts[0] = va;
		e->verts[1] = vb;
		e->color = color;
		e->num_faces = 1;
		e->flags = EF_USED | EF_DEFINING;			// Assume a normal line
		e->sides[0] = side;
		e->segnum[0] = segnum;
		++ am.num_edges;
		const auto i{ef.second + 1};
		if (am.end_valid_edges < i)
			am.end_valid_edges = i;
	} else {
		if ( color != am.wall_normal_color )
#if DXX_BUILD_DESCENT == 2
			if (color != am.wall_revealed_color)
#endif
				e->color = color;

		if ( e->num_faces < 4 ) {
			e->sides[e->num_faces] = side;
			e->segnum[e->num_faces] = segnum;
			e->num_faces++;
		}
	}
	e->flags |= flags;
}

static void add_one_unknown_edge(automap &am, vertnum_t va, vertnum_t vb)
{
	if ( va > vb )	{
		std::swap(va, vb);
	}

	const auto &&ef{automap_find_edge(am, va, vb)};
	if (ef.second == UINT32_MAX)
		ef.first.flags |= EF_FRONTIER;		// Mark as a border edge
}

static void add_segment_edges(fvcsegptr &vcsegptr, fvcwallptr &vcwallptr, automap &am, const vcsegptridx_t seg)
{
	auto &ControlCenterState{LevelUniqueObjectState.ControlCenterState};
	auto &WallAnims{GameSharedState.WallAnims};
#if DXX_BUILD_DESCENT == 2
	auto &Objects{LevelUniqueObjectState.Objects};
	auto &vmobjptr{Objects.vmptr};
#endif
	for (const auto sn : MAX_SIDES_PER_SEGMENT)
	{
		uint8_t hidden_flag{0};
		uint8_t is_grate{0};
		uint8_t no_fade{0};

		uint8_t color{255};
		if (seg->shared_segment::children[sn] == segment_none) {
			color = am.wall_normal_color;
		}

		switch( seg->special )	{
			case segment_special::nothing:
			case segment_special::goal_blue:
			case segment_special::goal_red:
				break;
			case segment_special::repaircen:
				color = BM_XRGB(0, 0, 26);
				break;
			case segment_special::fuelcen:
			color = BM_XRGB( 29, 27, 13 );
			break;
			case segment_special::controlcen:
			if (ControlCenterState.Control_center_present)
				color = BM_XRGB( 29, 0, 0 );
			break;
			case segment_special::robotmaker:
			color = BM_XRGB( 29, 0, 31 );
			break;
		}

		const auto wall_num{seg->shared_segment::sides[sn].wall_num};
		if (wall_num != wall_none)
		{
			auto &w{*vcwallptr(wall_num)};
#if DXX_BUILD_DESCENT == 2
			auto trigger_num{w.trigger};
			auto &Triggers{LevelUniqueWallSubsystemState.Triggers};
			auto &vmtrgptr{Triggers.vmptr};
			if (trigger_num != trigger_none && vmtrgptr(trigger_num)->type == trigger_action::secret_exit)
				{
			    color = BM_XRGB( 29, 0, 31 );
				no_fade = EF_NO_FADE;
				 goto Here;
				} 	
#endif

			switch(w.type)
			{
			case WALL_DOOR:
				if ((w.keys == wall_key::blue && (color = am.wall_door_blue, true)) ||
					(w.keys == wall_key::gold && (color = am.wall_door_gold, true)) ||
					(w.keys == wall_key::red && (color = am.wall_door_red, true)))
				{
					no_fade = EF_NO_FADE;
				} else if (!(WallAnims[w.clip_num].flags & WCF_HIDDEN)) {
					const auto connected_seg{seg->shared_segment::children[sn]};
					if (connected_seg != segment_none) {
						const shared_segment &vcseg{*vcsegptr(connected_seg)};
						const auto &connected_side{find_connect_side(seg, vcseg)};
						if (connected_side == side_none)
							break;
						auto &wall{*vcwallptr(vcseg.sides[connected_side].wall_num)};
						switch (wall.keys)
						{
							case wall_key::blue:
								color = am.wall_door_blue;
								no_fade = EF_NO_FADE;
								break;
							case wall_key::gold:
								color = am.wall_door_gold;
								no_fade = EF_NO_FADE;
								break;
							case wall_key::red:
								color = am.wall_door_red;
								no_fade = EF_NO_FADE;
								break;
							default:
								color = am.wall_door_color;
								break;
						}
					}
				} else {
					color = am.wall_normal_color;
					hidden_flag = EF_SECRET;
				}
				break;
			case WALL_CLOSED:
				// Make grates draw properly
				// NOTE: In original D1, is_grate is 1, hidden_flag not used so grates never fade. I (zico) like this so I leave this alone for now. 
				if (!(is_grate = WALL_IS_DOORWAY(GameBitmaps, Textures, vcwallptr, seg, sn) & WALL_IS_DOORWAY_FLAG::rendpast))
					hidden_flag = EF_SECRET;
				color = am.wall_normal_color;
				break;
			case WALL_BLASTABLE:
				// Hostage doors
				color = am.wall_door_color;	
				break;
			}
		}
	
		if (seg==Player_init[Player_num].segnum)
			color = BM_XRGB(31,0,31);

		if ( color != 255 )	{
#if DXX_BUILD_DESCENT == 2
			// If they have a map powerup, draw unvisited areas in dark blue.
			// NOTE: D1 originally had this part of code but w/o cheat-check. It's only supposed to draw blue with powerup that does not exist in D1. So make this D2-only
#ifndef NDEBUG
			if (!Automap_debug_show_all_segments)
#endif
			{
			auto &player_info{get_local_plrobj().ctype.player_info};
				if ((cheats.fullautomap || player_info.powerup_flags & PLAYER_FLAGS_MAP_ALL) && !LevelUniqueAutomapState.Automap_visited[seg])
				color = am.wall_revealed_color;
			}
			Here:
#endif
			const auto vertex_list{get_side_verts(seg,sn)};
			const uint8_t flags = hidden_flag | no_fade;
			add_one_edge(am, vertex_list[0], vertex_list[1], color, sn, seg, flags);
			add_one_edge(am, vertex_list[1], vertex_list[2], color, sn, seg, flags);
			add_one_edge(am, vertex_list[2], vertex_list[3], color, sn, seg, flags);
			add_one_edge(am, vertex_list[3], vertex_list[0], color, sn, seg, flags);

			if ( is_grate )	{
				const uint8_t grate_flags = flags | EF_GRATE;
				add_one_edge(am, vertex_list[0], vertex_list[2], color, sn, seg, grate_flags);
				add_one_edge(am, vertex_list[1], vertex_list[3], color, sn, seg, grate_flags);
			}
		}
	}
}


// Adds all the edges from a segment we haven't visited yet.

static void add_unknown_segment_edges(automap &am, const shared_segment &seg)
{
	for (const auto &&[sn, child] : enumerate(seg.children))
	{
		// Only add edges that have no children
		if (child == segment_none)
		{
			const auto vertex_list{get_side_verts(seg, static_cast<sidenum_t>(sn))};
	
			add_one_unknown_edge( am, vertex_list[0], vertex_list[1] );
			add_one_unknown_edge( am, vertex_list[1], vertex_list[2] );
			add_one_unknown_edge( am, vertex_list[2], vertex_list[3] );
			add_one_unknown_edge( am, vertex_list[3], vertex_list[0] );
		}
	}
}

void automap_build_edge_list(automap &am, int add_all_edges)
{
	// clear edge list
	for (auto &i : unchecked_partial_range(am.edges.get(), am.max_edges))
	{
		i.num_faces = 0;
		i.flags = 0;
	}
	am.num_edges = 0;
	am.end_valid_edges = 0;

	auto &Walls{LevelUniqueWallSubsystemState.Walls};
	auto &vcwallptr{Walls.vcptr};
	if (add_all_edges)	{
		// Cheating, add all edges as visited
		range_for (const auto &&segp, vcsegptridx)
		{
#if DXX_USE_EDITOR
			if (segp->shared_segment::segnum != segment_none)
#endif
			{
				add_segment_edges(vcsegptr, vcwallptr, am, segp);
			}
		}
	} else {
		// Not cheating, add visited edges, and then unvisited edges
		range_for (const auto &&segp, vcsegptridx)
		{
#if DXX_USE_EDITOR
			if (segp->shared_segment::segnum != segment_none)
#endif
				if (LevelUniqueAutomapState.Automap_visited[segp])
				{
					add_segment_edges(vcsegptr, vcwallptr, am, segp);
				}
		}
		range_for (const auto &&segp, vcsegptridx)
		{
#if DXX_USE_EDITOR
			if (segp->shared_segment::segnum != segment_none)
#endif
				if (!LevelUniqueAutomapState.Automap_visited[segp])
				{
					add_unknown_segment_edges(am, segp);
				}
		}
	}

	// Find unnecessary lines (These are lines that don't have to be drawn because they have small curvature)
	for (auto &i : unchecked_partial_range(am.edges.get(), am.end_valid_edges))
	{
		const auto e{&i};
		if (!(e->flags&EF_USED)) continue;

		const auto num_faces{e->num_faces};
		if (num_faces < 2)
			continue;
		for (unsigned e1{0}; e1 < num_faces; ++e1)
		{
			const auto e1segnum{e->segnum[e1]};
			const auto &e1siden0{vcsegptr(e1segnum)->shared_segment::sides[e->sides[e1]].normals[0]};
			for (unsigned e2{1}; e2 < num_faces; ++e2)
			{
				if (e1 == e2)
					continue;
				const auto e2segnum{e->segnum[e2]};
				if (e1segnum == e2segnum)
					continue;
				if (vm_vec_build_dot(e1siden0, vcsegptr(e2segnum)->shared_segment::sides[e->sides[e2]].normals[0]) > (F1_0 - (F1_0 / 10)))
				{
					e->flags &= (~EF_DEFINING);
					break;
				}
			}
			if (!(e->flags & EF_DEFINING))
				break;
		}
	}
}

}

#if DXX_BUILD_DESCENT == 2
void InitMarkerInput ()
{
	//find free marker slot
	const auto game_mode{Game_mode};
	const auto max_numplayers{Netgame.max_numplayers};
	const auto maxdrop{MarkerState.get_markers_per_player(game_mode, max_numplayers)};
	const auto &&game_marker_range{get_game_marker_range(game_mode, max_numplayers, Player_num, maxdrop)};
	const auto &&player_marker_range{get_player_marker_range(maxdrop)};
	const auto &&zipped_marker_range{zip(player_marker_range, unchecked_partial_range(&MarkerState.imobjidx[*game_marker_range.begin()], maxdrop))};
	const auto &&mb{zipped_marker_range.begin()};
	const auto &&me{zipped_marker_range.end()};
	auto iter{mb};
	for (;;)
	{
		auto &&[pmi, objidx]{*iter};
		if (objidx == object_none)		//found free slot!
		{
			MarkerState.MarkerBeingDefined = pmi;
			break;
		}
		if (++ iter == me)		//no free slot
		{
			if (+(game_mode & GM_MULTI))
			{
				//in multi, replace oldest
				MarkerState.MarkerBeingDefined = static_cast<player_marker_index>((static_cast<unsigned>(MarkerState.LastMarkerDropped) + 1) & (maxdrop - 1));
				break;
			}
			else
			{
				HUD_init_message_literal(HM_DEFAULT, "No free marker slots");
				return;
			}
		}
	}

	//got a free slot.  start inputting marker message

	Marker_input.front() = 0;
	Marker_index=0;
	key_toggle_repeat(1);
}

window_event_result MarkerInputMessage(int key, control_info &Controls)
{
	auto &Objects{LevelUniqueObjectState.Objects};
	auto &vmobjptr{Objects.vmptr};
	auto &vmobjptridx{Objects.vmptridx};
	switch( key )
	{
		case KEY_LEFT:
		case KEY_BACKSP:
		case KEY_PAD4:
			if (Marker_index > 0)
				Marker_index--;
			Marker_input[Marker_index] = 0;
			break;
		case KEY_ENTER:
			{
				const auto player_marker_num{MarkerState.MarkerBeingDefined};
				MarkerState.LastMarkerDropped = player_marker_num;
				const auto game_marker_num{convert_player_marker_index_to_game_marker_index(Game_mode, Netgame.max_numplayers, Player_num, player_marker_num)};
				MarkerState.message[game_marker_num] = Marker_input;
				DropMarker(vmobjptridx, vmsegptridx, get_local_plrobj(), game_marker_num, player_marker_num);
			}
			[[fallthrough]];
		case KEY_F8:
		case KEY_ESC:
			MarkerState.MarkerBeingDefined = player_marker_index::None;
			key_toggle_repeat(0);
			game_flush_inputs(Controls);
			break;
		default:
		{
			int ascii{key_ascii()};
			if ((ascii < 255 ))
				if (Marker_index < Marker_input.size() - 1)
				{
					Marker_input[Marker_index++] = ascii;
					Marker_input[Marker_index] = 0;
				}
			return window_event_result::ignored;
		}
	}
	return window_event_result::handled;
}
#endif

}
