/*
	server/ai/pathfind_core.qc

	generic waypoint pathfinding

	Copyright (C) 2021-2024 NZ:P Team

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	as published by the Free Software Foundation; either version 2
	of the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

	See the GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to:

		Free Software Foundation, Inc.
		59 Temple Place - Suite 330
		Boston, MA  02111-1307, USA

*/

void() Load_Waypoints_Legacy;

float Distance (vector from, vector to) {
	return vlen(from - to);
}

float HeuristicCostEstimate (float start_way, float end_way)
{
	//for now we will just look the distance between.
	return Distance(waypoints[start_way].org, waypoints[end_way].org);
}

void ReconstructPath(entity z, float start_way, float end_way) {

}

float open_first;
float open;
float closed_init;
float closed;
float closed_first;
void SV_WP_ClearSet()
{
	float i;

	for (i = 0; i < MAX_WAYPOINTS; i = i + 1)
	{
		waypoints[i].set = 0;
	}
}

float IsActive(float way) {
	if (waypoints[way].targetdoor != "") {
		entity door;

		door = find(world, wayTarget, waypoints[way].targetdoor);

		if (door != world) {
			if (door.state == STATE_BOTTOM) {
				return 0;
			}
		}
	}

	return 1;
}

void SV_WP_SetAdd(float wp, float isopen)
{
	if (isopen)
	{
		// we only get added here from none, so no cleanup required
		// gotta check if last entry was removed before this call
		if (waypoints[open_first].set == SET_CLOSED)
		{
			//Con_Printf("Empty set: %i is new first\n", wp);
			open_first = wp;
			waypoints[wp].prev = wp;
		}
		else
		{
			waypoints[wp].prev = open;
			waypoints[open].next = wp;
		}
		waypoints[wp].next = wp;
		waypoints[wp].set = SET_OPEN;
		open = wp;
	}
	else
	{
		// even if wp is the only one in the set, it doesn't really matter
		if (wp == open_first)
			open_first = waypoints[wp].next;
		if (wp == open)
			open = waypoints[wp].prev;

		// update links so open set doesn't get fucked
		waypoints[waypoints[wp].prev].next = waypoints[wp].next;
		waypoints[waypoints[wp].next].prev = waypoints[wp].prev;

		if (!closed_init)
		{
			closed_first = wp;
			waypoints[wp].prev = wp;
			closed_init = true;
		}
		else
		{
			waypoints[closed].next = wp;
			waypoints[wp].prev = closed;
		}
		waypoints[wp].next = wp;
		waypoints[wp].set = SET_CLOSED;
		closed = wp;
	}
}

float Pathfind (entity z, float s_wp, float e_wp) {
	float current;
	float i, j;
	float bestf;


	if (s_wp == e_wp) {
		return s_wp;
	}

	SV_WP_ClearSet();

	open_first = s_wp;
	open = s_wp;
	waypoints[s_wp].next = s_wp;
	waypoints[s_wp].prev = s_wp;
	waypoints[s_wp].set = SET_OPEN;
	waypoints[s_wp].g = 0;
	waypoints[s_wp].f = HeuristicCostEstimate(s_wp, e_wp);
	waypoints[s_wp].step = s_wp;
	current = s_wp;

	closed_init = false;

	// while openset is not empty
	while (waypoints[open_first].set == SET_OPEN) {
		i = open_first;

		bestf = 320000;
		//print("Current ", ftos(current), "\n");

		// go from first open to last, pick the one with lowest f
		do {
			if (waypoints[i].f < bestf) {
				current = i;
				bestf = waypoints[i].f;
			}
			i = waypoints[i].next;
		} while (i != open);

		// mark node as visited
		//print("Removing ", ftos(current), " from open\n");
		SV_WP_SetAdd(current, false);

		// we found a result, loop back with pathlength
		if (current == e_wp) {
			j = i = current;
			// walk constructed path backwards
			// keep 2 next steps with us
			//print("Result\n");
			//print(ftos(current), "\n");
			while (waypoints[current].step != current) {
				j = i; // 3rd waypoint along path
				i = current; // 2nd waypoint along path
				current = waypoints[current].step; // Start waypoint
			}

			// go to the furthest one on the path that we can actually see
			// If within a radius of 6 qu of the first waypoint along the path,
			// Always return the 2nd waypoint along the path
			float is_at_first_waypoint = vlen(z.origin - waypoints[s_wp].org) <= 6;



			// Check if we can tracemove to the 3rd waypoint along the path
			if (tracemove(z.origin,VEC_HULL_MIN,VEC_HULL_MAX,waypoints[i].org,TRUE,z))
			{
				//VectorCopy(waypoints[i].origin, res);
				//print("Giving third wp ", ftos(i), "\n");
				return i;
			}
			// Check if we can tracemove to the 2nd waypoint along the path
			else if (is_at_first_waypoint || tracemove(z.origin,VEC_HULL_MIN,VEC_HULL_MAX,waypoints[j].org,TRUE,z))
			{
				//VectorCopy(waypoints[j].origin, res);
				//print("Giving second wp ", ftos(j), "\n");
				return j;
			}
			else
			{
				//VectorCopy(waypoints[current].origin, res);
				//print("Giving first wp ", ftos(current), "\n");
				return current;
			}
		}


		// check neighbours to add to openset
		for (j = 0; j < MAX_WAY_TARGETS; j++)
		{
			i = waypoints[current].target_id[j];
			// Skip empty neighbor slots
			if(i == -1) {
				continue;
			}

			if (waypoints[i].set != SET_CLOSED && i != current && IsActive(i))
			{
				bestf = waypoints[current].g + HeuristicCostEstimate(i, current);
				if (waypoints[i].set == SET_NONE || bestf < waypoints[i].g)
				{
					waypoints[i].step = current;
					waypoints[i].g = bestf;
					waypoints[i].f = bestf + HeuristicCostEstimate(i, e_wp);

					if (waypoints[i].set == SET_NONE)
					{
						//print("Adding ", ftos(i), " into open\n");
						SV_WP_SetAdd(i, true);
					}
				}
			}
		}
	}
	print("Waypointing failed\n");
	return -1;
}


float Do_Pathfind(entity from, entity to) {

	float i;
	float TraceResult;

	float dist, best_dist, best, best_target;
	
	best = 0;
	best_target = 0;
	dist = 0;
	best_dist = 100000000;

	#ifdef FTE
	for (i = 0; i < MAX_WAYPOINTS; i = i + 1) {
		// Skip unused waypoint slots
		if(waypoints[i].id < 0) {
			continue;
		}
		
		if (IsActive(i)) {
			TraceResult = tracemove (from.origin, VEC_HULL_MIN, VEC_HULL_MAX, waypoints[i].org, TRUE ,from);
			if (TraceResult) {
				dist = Distance(waypoints[i].org, from.origin);

				if(dist < best_dist) {
					best_dist = dist;
					best = i;
				}
			}
		}
	}
	dist = 0;
	best_dist = 100000000;

	for (i = 0; i < MAX_WAYPOINTS; i = i + 1) {
		// Skip unused waypoint slots
		if(waypoints[i].id < 0) {
			continue;
		}

		if (IsActive(i)) {
			TraceResult = tracemove (to.origin, VEC_HULL_MIN, VEC_HULL_MAX, waypoints[i].org, TRUE ,to);
			if (TraceResult) {
				dist = Distance(waypoints[i].org, to.origin);

				if(dist < best_dist) {
					best_dist = dist;
					best_target = i;
				}
			}
		}
	}

	//print("Starting waypoint: ", ftos(best)," Ending waypoint: ", ftos(best_target), "\n");




	return Pathfind(from, best, best_target);
	#else 
	return 0;
	#endif
}

void CalcDistances() {
	float i, s;

	for (i = 1; i < MAX_WAYPOINTS; i++) {
		waypoint_ai way;
		way = waypoints[i];
		if (way.id > 0) {
			for (s = 0; s < MAX_WAY_TARGETS; s++) {
				float targ;
				targ = way.target_id[s];
				if (targ > 0) {
					way.dist[s] = Distance(way.org, waypoints[targ].org);
				}
			}
		}
	}
}
			

void creator_way_touch();
void() Compat_TryLoadBetaWaypoints;
void LoadWaypointData() { 
    float file, point;
	string h;
	float targetcount, loop, waycount;
	entity new_way;

	h = strcat(mappath, ".way");
	file = fopen (h, FILE_READ);

	if (file == -1)
	{

#ifdef FTE

		dprint("LoadWaypointData: No .way file found, trying Beta format..\n");
		Compat_TryLoadBetaWaypoints();

#endif // FTE

		return;
	}

    float i, s;
	

	#ifdef FTE
    for (i = 0; i < MAX_WAYPOINTS; i = i + 1) {
		waypoint_ai way; 
		
		way = waypoints[i];
        way.org = '0 0 0';
        way.id = 0;
        way.g = 0;
        way.f = 0;
		way.set = 0;
        way.targetdoor = "";

        for (s = 0; s < MAX_WAY_TARGETS; s = s + 1) {
            way.target_id[s] = 0;
            way.dist[s] = 0;
        }
		
    }
	#endif

	new_way = spawn();
	point = 0;
	waycount = 0;
	targetcount = 0;
	loop = 1;
	while (loop)
	{
		string line;
		line = fgets(file);
		if not (line) {
			//bprint(PRINT_HIGH, "End of file\n");
			loop = 0;
			break;
		}
		h = strtrim(line);

		//print(h, "\n");
		if (h == "") {
			continue;
		}

		switch (point) {
			case 0:
				if (h == "waypoint") {
					new_way = spawn();
					new_way.solid = SOLID_TRIGGER;

					setmodel(new_way, "models/way/normal_way.spr");
					new_way.effects = EF_FULLBRIGHT;
					new_way.classname = "waypoint";

					new_way.touch = creator_way_touch;
					point = 1;
					targetcount = 0;
				} else if (h == "Waypoint") {
					//bprint(PRINT_HIGH, "Identified .way as legacy..\n");
					point = 99;
					Load_Waypoints_Legacy();
				} else {
					bprint(PRINT_HIGH, strcat("Error: unknown point ", strcat(h, "\n")));
				}
				break;
			case 1:
				if (h == "{") {
					point = 2;
				} else {
					bprint(PRINT_HIGH, strcat("Error: unknown variable ", strcat(h, " expected {\n")));
				}
				break;
			case 2:
				tokenize(h);

				string value, variable;
				variable = strtrim(argv(0));
				value = strtrim(argv(2));

				switch (variable) {
					case "origin":
						setorigin(new_way, stov(value));
						break;
					case "id":
						new_way.waynum = value;
						break;
					case "door":
						new_way.targetname = value;
						break;
					case "targets":
						point = 3;
						break;
					case "}":
						float id = stof(new_way.waynum);
						waypoint_ai waypoint;
						waypoint = waypoints[id];

						waypoints[id].id = id;
						waypoints[id].org = new_way.origin;
						waypoints[id].targetdoor = new_way.targetname;

						for (i = 0; i < MAX_WAY_TARGETS; i++) {
							waypoints[id].target_id[i] = stof(new_way.targets[i]);
						}

						if (!cvar("waypoint_mode")) {
							remove(new_way);
						}
						

						point = 0;
						break;
					default:
						bprint(PRINT_HIGH, strcat("Error: unknown variable ", strcat(variable, "\n")));
						break;
				}
				break;
			case 3:
				if (h == "[") {
					point = 4;
				} else {
					bprint(PRINT_HIGH, strcat("Error: unknown variable ", strcat(h, " expected [\n")));
				}
				break;
			case 4:
				if (targetcount >= MAX_WAY_TARGETS) {
					bprint(PRINT_HIGH, "Error: Target count too high for waypoint\n");
				} else if (h == "]") {
					point = 2;
				} else  {
					new_way.targets[targetcount] = h;
					targetcount++;
				}
				break;
		}
	}

	if (new_way) {
		if (!cvar("waypoint_mode")) {
			remove(new_way);
		}
	}

	fclose(file);

	#ifdef FTE
	CalcDistances();
	#endif
}