#pragma once

#include "camera/camera.h"

class LabCamera {
public:
	LabCamera(camid cam) {
		FS_camera = cam;
	}

	virtual ~LabCamera() = 0;

	camid FS_camera;

	/// <summary>
	/// Returns a string explaining the camera controls
	/// </summary>
	/// <returns></returns>
	virtual SCP_string getUsageInfo() = 0;

	/// <summary>
	/// Returns a string containing miscellaneous info
	/// </summary>
	/// <returns></returns>
	virtual SCP_string getOnFrameInfo() = 0;

	/// <summary>
	/// Called by the Lab Manager each frame to handle input events.
	/// </summary>
	/// <param name="dx">Mouse delta on the x axis</param>
	/// <param name="dy">Mouse delta on the y axis</param>
	/// <param name="dz">Mouse wheel delta</param>
	/// <param name="lmbDown">Whether the left mouse button is currently held</param>
	/// <param name="lmbPressed">Whether the left mouse button was pressed this frame (rising edge only)</param>
	/// <param name="rmbDown">State of the right mouse button</param>
	/// <param name="modifierKeys">State of the various modifier keys. See keys.h</param>
	virtual void handleInput(
		int dx, int dy, int dz, bool lmbDown, bool lmbPressed, bool rmbDown, int modifierKeys, int mouseX, int mouseY) = 0;

	/// <summary>
	/// Called by the lab manager when the displayed object changes
	/// </summary>
	virtual void displayedObjectChanged() = 0;

	/// <summary>
	/// Returns true if this camera also handles object placement
	/// </summary>
	/// <returns></returns>
	virtual bool handlesObjectPlacement() = 0;

	/// <summary>
	/// Forces the camera to update its position based on input or object position
	/// </summary>
	/// <returns></returns>
	virtual void updateCamera() = 0;

	/// Resets the camera orientation, pan, and zoom to default values
	virtual void resetView() {}

	/// Returns the distance from the camera to the point of interest, used to size the orthographic frustum.
	virtual float getCameraDistance() const { return 0.0f; }

	/// Render any 2D overlays associated with this camera (e.g. orientation widgets).
	virtual void renderOverlay() const {}

	/// Returns true if the given screen-space point is over an interactive camera overlay control.
	virtual bool isOverlayHit(int /*mouseX*/, int /*mouseY*/) const { return false; }
};

class OrbitCamera : public LabCamera {
public:
	enum class SnapDirection { Front, Back, Top, Bottom, Left, Right };

	OrbitCamera() : LabCamera(cam_create("Lab orbit camera")) {}

	SCP_string getUsageInfo() override {
		return "Hold LMB to rotate the model. Hold RMB to rotate the camera. Hold Shift + RMB to pan on X/Y. Use mouse wheel to zoom in/out.";
	}

	SCP_string getOnFrameInfo() override {
		SCP_stringstream ss;
		ss.setf(std::ios::fixed);

		ss << "Phi: " << phi << " Theta: " << theta << " Distance: " << distance << " Pan: (" << pan_offset.xyz.x << ", "
		   << pan_offset.xyz.y << ", " << pan_offset.xyz.z << ")";

		return ss.str();
	}

	void handleInput(
		int dx, int dy, int dz, bool /*lmbDown*/, bool lmbPressed, bool rmbDown, int modifierKeys, int mouseX, int mouseY) override;

	void resetView() override;

	void displayedObjectChanged() override;

	bool handlesObjectPlacement() override { return false; }

	void updateCamera() override;

	float getCameraDistance() const override { return distance; }
	void renderOverlay() const override;
	bool isOverlayHit(int mouseX, int mouseY) const override;

  private:
	static constexpr int WIDGET_CUBE_HALF_SIZE = 30;
	static constexpr int WIDGET_MARGIN = 14;

	static constexpr float DEFAULT_DISTANCE = 100.0f;
	static constexpr float DEFAULT_PHI = 1.24f;
	static constexpr float DEFAULT_THETA = 2.25f;

	struct WidgetLayout {
		int size, left, top, center_x, center_y, cube_half;
	};
	static WidgetLayout getWidgetLayout();

	bool handleOrientationWidgetClick(int mouseX, int mouseY);
	void snapToDirection(SnapDirection direction);
	static float getObjectFitDistance();

	float distance = DEFAULT_DISTANCE;
	float phi = DEFAULT_PHI;
	float theta = DEFAULT_THETA;
	vec3d pan_offset = vmd_zero_vector;
};
