/*
**	Command & Conquer Renegade(tm)
**	Copyright 2025 Electronic Arts Inc.
**
**	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 3 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, see <http://www.gnu.org/licenses/>.
*/

// GraphicView.cpp : implementation of the CGraphicView class
//

#include "stdafx.h"
#include "PhysTest.h"
#include "PhysTestDoc.h"
#include "GraphicView.h"
#include "MainFrm.h"
#include "mmsystem.h"
#include "pscene.h"
#include "ww3d.h"
#include "camera.h"
#include "quat.h"
#include "rcfile.h"
#include "assetmgr.h"
#include "rbody.h"
#include "chunkio.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif


#define ORTHO_CAMERA	0
#define ORTHO_WIDTH	20


////////////////////////////////////////////////////////////////////////////
//
//  CHUNK ID's used by GraphicView
//
enum 
{
	GRAPHICVIEW_CHUNK_VARIABLES			= 0x00789931,

	GRAPHICVIEW_VARIABLE_CAMERAMODE		= 0x00,
	GRAPHICVIEW_VARIABLE_DISPLAYBOXES,
	GRAPHICVIEW_VARIABLE_RUNSIMULATION,
	GRAPHICVIEW_VARIABLE_CAMERATM,
};


const float PIP_VIEW = 0.2f;		// size of the pip viewport

////////////////////////////////////////////////////////////////////////////
//
//  TimerCallback
//
void CALLBACK TimerCallback
(
	UINT uID,
	UINT uMsg,
	DWORD dwUser,
	DWORD dw1,
	DWORD dw2
)
{
	HWND hwnd = (HWND)dwUser;
	if (IsWindow(hwnd)) {

		// Send this event off to the view to process (hackish, but fine for now)
		if ((GetProp (hwnd, "WaitingToProcess") == NULL) &&
			 (GetProp (hwnd, "Inactive") == NULL)) {

			SetProp (hwnd, "WaitingToProcess", (HANDLE)1);

			// Send the message to the view
			::PostMessage (hwnd, WM_USER + 101, 0, 0L);
		}
	}
}


////////////////////////////////////////////////////////////////////////////
//
//  WindowProc
//
LRESULT CGraphicView::WindowProc
(
    UINT message,
    WPARAM wParam,
    LPARAM lParam
)
{
	// Is this the repaint message we are expecting?
	if (message == WM_USER+101) {
		Timestep();
		Repaint_View();
		RemoveProp(m_hWnd,"WaitingToProcess");
	} 

	// Allow the base class to process this message
	return CView::WindowProc(message, wParam, lParam);
}


/////////////////////////////////////////////////////////////////////////////
// CGraphicView

IMPLEMENT_DYNCREATE(CGraphicView, CView)

BEGIN_MESSAGE_MAP(CGraphicView, CView)
	//{{AFX_MSG_MAP(CGraphicView)
	ON_WM_SIZE()
	ON_WM_DESTROY()
	ON_WM_LBUTTONDOWN()
	ON_WM_LBUTTONUP()
	ON_WM_RBUTTONDOWN()
	ON_WM_RBUTTONUP()
	ON_WM_MOUSEMOVE()
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CGraphicView construction/destruction

CGraphicView::CGraphicView() :
	Initialized(false),
	Active(true),
	RunSimulation(false),
	DisplayBoxes(false),
	CameraMode(CAMERA_FLY),
	Camera(NULL),
	TimerID(0),
	LMouseDown(false),
	RMouseDown(false),
	LastPoint(0,0),
	PipCamera(NULL),
	PipScene(NULL),
	Axes(NULL)
{
}

CGraphicView::~CGraphicView()
{
	REF_PTR_RELEASE(Camera);
	REF_PTR_RELEASE(PipCamera);
	REF_PTR_RELEASE(PipScene);
	REF_PTR_RELEASE(Axes);
}

BOOL CGraphicView::PreCreateWindow(CREATESTRUCT& cs)
{
	// TODO: Modify the Window class or styles here by modifying
	//  the CREATESTRUCT cs

	return CView::PreCreateWindow(cs);
}

BOOL CGraphicView::Initialize_WW3D(int device,int bits)
{
	// Assume failure
	BOOL ok = FALSE;
	if (device < 0) {
		return FALSE;
	}
	
	// Initialize the rendering engine with the information from
	// this window.
	RECT rect;
	GetClientRect (&rect);

	int cx = rect.right-rect.left;
	int cy = rect.bottom-rect.top;

	WW3DErrorType err = WW3D_ERROR_GENERIC;
	while (err != WW3D_ERROR_OK) {
		err = WW3D::Set_Render_Device(device,cx,cy,bits,true);
		if (err != WW3D_ERROR_OK) {
			device = (device + 1) % WW3D::Get_Render_Device_Count();
		}
	}

	// Load some models that we're going to need into the asset manager
	ResourceFileClass point_file(NULL, "Point.w3d");
	WW3DAssetManager::Get_Instance()->Load_3D_Assets(point_file);
	ResourceFileClass axes_file(NULL, "Axes.w3d");
	WW3DAssetManager::Get_Instance()->Load_3D_Assets(axes_file);

	if (Camera == NULL) {
		Camera = NEW_REF(CameraClass,());
		ASSERT(Camera);

		// Init the camera
		Matrix3D transform;
		transform.Look_At(Vector3(0.0f,-15.0f,5.0f),Vector3(0,0,0),0);
		Camera->Set_Transform(transform);

		// update the camera FOV settings
		// take the larger of the two dimensions, give it the
		// full desired FOV, then give the other dimension an
		// FOV proportional to its relative size
		double hfov,vfov;
		if (cy > cx) {						
			vfov = (float)DEG_TO_RAD(45.0f);
			hfov = (double)cx / (double)cy * vfov;
		} else {		
			hfov = (float)DEG_TO_RAD(45.0f);
			vfov = (double)cy / (double)cx * hfov;
		}

		// Reset the field of view
		Camera->Set_View_Plane(hfov,vfov);

#if ORTHO_CAMERA
		Camera->Set_Projection_Type(CameraClass::ORTHO);
		Camera->Set_View_Plane(Vector2(-ORTHO_WIDTH,-ORTHO_WIDTH),Vector2(ORTHO_WIDTH,ORTHO_WIDTH));
#endif
	}

	if (PipCamera == NULL) {
		PipCamera = NEW_REF(CameraClass,());
		ASSERT(PipCamera);
		PipCamera->Set_Viewport(Vector2(0.0f,1.0f - PIP_VIEW),Vector2(PIP_VIEW,1.0f));
		PipCamera->Set_View_Plane(Camera->Get_Horizontal_FOV(),Camera->Get_Vertical_FOV());
		Update_Pip_Camera();
	}

	if (PipScene == NULL) {
		PipScene = NEW_REF(SimpleSceneClass,());
		PipScene->Set_Ambient_Light(Vector3(1,1,1));
		ASSERT(PipScene);
	}

	if (Axes == NULL) {
		Axes = WW3DAssetManager::Get_Instance()->Create_Render_Obj("Axes");
		ASSERT(Axes);
		Axes->Set_Transform(Matrix3D(1));
		PipScene->Add_Render_Object(Axes);
	}

	// Kick off a timer that we can use to update
	// the display (kinda like a game loop iterator)
	if (TimerID == 0) {
#if 0
		TimerID = (UINT)::timeSetEvent(	50,
													50,
													TimerCallback,
													(DWORD)m_hWnd,
													TIME_PERIODIC);
#else
		TimerID = (UINT)::timeSetEvent(	66,						// only update 15 times a second
													50,
													TimerCallback,
													(DWORD)m_hWnd,
													TIME_PERIODIC);
#endif
	}

	Initialized = true;

	// Return the TRUE/FALSE result code
	return TRUE;
}

void CGraphicView::Save(ChunkSaveClass & csave)
{
	Matrix3D cameratm = Camera->Get_Transform();
	csave.Begin_Chunk(GRAPHICVIEW_CHUNK_VARIABLES);
	WRITE_MICRO_CHUNK(csave,GRAPHICVIEW_VARIABLE_CAMERAMODE,CameraMode);
	WRITE_MICRO_CHUNK(csave,GRAPHICVIEW_VARIABLE_DISPLAYBOXES,DisplayBoxes);
	WRITE_MICRO_CHUNK(csave,GRAPHICVIEW_VARIABLE_RUNSIMULATION,RunSimulation);
	WRITE_MICRO_CHUNK(csave,GRAPHICVIEW_VARIABLE_CAMERATM,cameratm);
	csave.End_Chunk();
}

void CGraphicView::Load(ChunkLoadClass & cload)
{
	Matrix3D cameratm(1);

	while (cload.Open_Chunk()) {
		switch(cload.Cur_Chunk_ID()) {
		case GRAPHICVIEW_CHUNK_VARIABLES:
			while(cload.Open_Micro_Chunk()) {
				switch(cload.Cur_Micro_Chunk_ID()) {
					READ_MICRO_CHUNK(cload,GRAPHICVIEW_VARIABLE_CAMERAMODE,CameraMode);
					READ_MICRO_CHUNK(cload,GRAPHICVIEW_VARIABLE_DISPLAYBOXES,DisplayBoxes);
					READ_MICRO_CHUNK(cload,GRAPHICVIEW_VARIABLE_RUNSIMULATION,RunSimulation);
					READ_MICRO_CHUNK(cload,GRAPHICVIEW_VARIABLE_CAMERATM,cameratm);
				}
				cload.Close_Micro_Chunk();
			}
			break;
		default:
			WWDEBUG_SAY(("Unhandled chunk: %d File: %s Line: %d\n",cload.Cur_Chunk_ID(),__FILE__,__LINE__));
		}
		cload.Close_Chunk();
	}

	Camera->Set_Transform(cameratm);
}


/////////////////////////////////////////////////////////////////////////////
// CGraphicView drawing

void CGraphicView::OnDraw(CDC* pDC)
{
	Repaint_View();
}

/////////////////////////////////////////////////////////////////////////////
// CGraphicView diagnostics

#ifdef _DEBUG
void CGraphicView::AssertValid() const
{
	CView::AssertValid();
}

void CGraphicView::Dump(CDumpContext& dc) const
{
	CView::Dump(dc);
}

CPhysTestDoc* CGraphicView::GetDocument() // non-debug version is inline
{
	ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CPhysTestDoc)));
	return (CPhysTestDoc*)m_pDocument;
}
#endif //_DEBUG

/////////////////////////////////////////////////////////////////////////////
// CGraphicView message handlers

void CGraphicView::OnSize(UINT nType, int cx, int cy) 
{
	CView::OnSize(nType, cx, cy);
	
	if (Initialized)
	{
		// Change the resolution of the rendering device to 
		// match that of the view's current dimensions
		WW3D::Set_Resolution (cx, cy, -1, true);
 
		// update the camera FOV settings
		// take the larger of the two dimensions, give it the
		// full desired FOV, then give the other dimension an
		// FOV proportional to its relative size
		double hfov,vfov;
		if (cy > cx) {						
			vfov = (float)DEG_TO_RAD(45.0f);
			hfov = (double)cx / (double)cy * vfov;
		} else {		
			hfov = (float)DEG_TO_RAD(45.0f);
			vfov = (double)cy / (double)cx * hfov;
		}

		// Reset the field of view
		ASSERT(Camera);
		Camera->Set_View_Plane(hfov, vfov);
#if ORTHO_CAMERA
		Camera->Set_View_Plane(Vector2(-ORTHO_WIDTH,-ORTHO_WIDTH),Vector2(ORTHO_WIDTH,ORTHO_WIDTH));
#endif
		PipCamera->Set_View_Plane(hfov,vfov);

		// Force a repaint of the screen
		Repaint_View();
	}
}

void CGraphicView::OnInitialUpdate() 
{
	CView::OnInitialUpdate();
	
	CPhysTestDoc * doc = (CPhysTestDoc *)GetDocument();
	if (doc) {
		// tell the document to create the scene
		doc->Init_Scene();
	}
}

void CGraphicView::Timestep(void)
{
	const float FLY_VELOCITY = 10.0f;

	// Compute the amount of time elapsed for this frame.
	CPhysTestDoc * doc = (CPhysTestDoc *)GetDocument();
	DWORD curtime = ::GetTickCount();
	DWORD elapsedtime = curtime - doc->LastTime;
	if (elapsedtime > 100) {
		elapsedtime = 100;
	}
	float dt = (float)elapsedtime / 1000.0f;
	doc->LastTime = curtime;	

	// fly forward when control is held
	if (::GetAsyncKeyState('A') & 0x8000) {
		Matrix3D transform = Camera->Get_Transform();
		transform.Translate(Vector3(0,0,-FLY_VELOCITY * dt));
		Camera->Set_Transform(transform);
	}

	if (::GetAsyncKeyState('Z') & 0x8000) {
		Matrix3D transform = Camera->Get_Transform();
		transform.Translate(Vector3(0,0,FLY_VELOCITY * dt));
		Camera->Set_Transform(transform);
	}

	// if mode == follow and we have an object selected, look at it
	if (CameraMode == CAMERA_FOLLOW) {
		CMainFrame * wnd = (CMainFrame *)::AfxGetMainWnd();
		PhysClass * obj = wnd->Peek_Selected_Object();
		if (obj) {
			Vector3 target;
			obj->Get_Transform().Get_Translation(&target);
			Vector3 pos;
			Camera->Get_Transform().Get_Translation(&pos);
			Matrix3D tm;
			tm.Look_At(pos,target,0.0f);
			Camera->Set_Transform(tm);
		}
	}

	// if mode == tether and we have an object selected, tie the camera to it
	if ((CameraMode == CAMERA_TETHER) || (CameraMode == CAMERA_RIGID_TETHER)) {
		CMainFrame * wnd = (CMainFrame *)::AfxGetMainWnd();
		PhysClass * obj = wnd->Peek_Selected_Object();
		if (obj) {
			const float TETHER_DIST = 15.0f;
			const float TETHER_ANGLE = DEG_TO_RADF(-20.0f);
			Vector3 pos;
			float zrot = obj->Get_Transform().Get_Z_Rotation();
			obj->Get_Transform().Get_Translation(&pos);	
			
			Matrix3D tm(1);
			
			if (CameraMode == CAMERA_RIGID_TETHER) {
				tm = obj->Get_Transform();
			} else {
				tm.Translate(pos);
				tm.Rotate_Z(zrot);
			}
			
			tm.Rotate_Y(DEG_TO_RADF(-90.0f));
			tm.Rotate_Z(DEG_TO_RADF(-90.0f));
			tm.Rotate_X(TETHER_ANGLE);
			tm.Translate(Vector3(0,0,TETHER_DIST));
			Camera->Set_Transform(tm);
		}
	}
	
	// update the PIP camera transform
	Update_Pip_Camera();

	// Allow the world to simulate (if enabled)
	if (RunSimulation) {
		doc->Scene->Update(1.0f/15.0f , 0);  // 0.05f,0); //dt,0);
	}

	// DEBUG, print the contact point coordinates
#if 0
	Vector3 pt = _LastContactPoint;
	CString string;
	string.Format("Contact Point: %10.5f %10.5f %10.5f   StartBad: %d",pt.X,pt.Y,pt.Z,_StartBad);
	((CMainFrame *)::AfxGetMainWnd())->Set_Status_Bar_Text(string);
#endif
}

void CGraphicView::Repaint_View(void)
{
	static bool _already_painting = false;
	if (_already_painting) return;
	_already_painting = true;

	// Get the document to display
	CPhysTestDoc * doc = (CPhysTestDoc *)GetDocument();

	// Are we in a valid state?
	if (Initialized && doc->Scene)	{

		// Update the W3D frame times according to our elapsed tick count
		//WW3D::Sync(WW3D::Get_Sync_Time() + (ticks_elapsed * m_animationSpeed));

		// Render the scenes
		WW3D::Begin_Render(TRUE, TRUE, Vector3(0,0,0));
		WW3D::Render(doc->Scene,Camera,FALSE,FALSE);
		WW3D::Render(PipScene,PipCamera,FALSE,TRUE);
		WW3D::End_Render();
	}        

	_already_painting = false;
}

void CGraphicView::Set_Active(bool onoff)
{
	Active = onoff;
	if (!Active) { 
		::SetProp(m_hWnd,"Inactive",(HANDLE)1); 
	} else { 
		RemoveProp(m_hWnd,"Inactive"); 
		CPhysTestDoc * doc = (CPhysTestDoc *)GetDocument();
		doc->LastTime = ::GetTickCount();	
	}
}

bool CGraphicView::Is_Collision_Box_Display_Enabled(void)
{
	return DisplayBoxes;
}

void CGraphicView::Enable_Collision_Box_Display(bool onoff)
{
	DisplayBoxes = onoff;
	if (DisplayBoxes) {
		WW3D::Set_Collision_Box_Display_Mask(COLLISION_TYPE_PHYSICAL);
	} else {
		WW3D::Set_Collision_Box_Display_Mask(0);
	}

}

void CGraphicView::OnDestroy() 
{
	// remove our properties
	::RemoveProp(m_hWnd,"Inactive"); 
	::RemoveProp(m_hWnd,"WaitingToProcess");

	CView::OnDestroy();
	
	// Free the camera object
	REF_PTR_RELEASE(Camera);
	REF_PTR_RELEASE(PipScene);
	REF_PTR_RELEASE(PipCamera);
	REF_PTR_RELEASE(Axes);

	// Is there an update thread running?
	if (TimerID == 0) {
		// Stop the timer
		::timeKillEvent((UINT)TimerID);
		TimerID = 0;        
	}

	Initialized = FALSE;    
}


void CGraphicView::OnLButtonDown(UINT nFlags, CPoint point) 
{
	// Capture all mouse messages
	SetCapture();

	// Left mouse button is down
	LMouseDown = TRUE;
	LastPoint = point;

	// Allow the base class to process this message
	CView::OnLButtonDown(nFlags, point);
}

void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point) 
{
	// if both buttons are now up, release the mouse
	if (!RMouseDown) {
		ReleaseCapture();
	}

	// Left mouse button is now up
	LMouseDown = false;

	CView::OnLButtonUp(nFlags, point);
}

void CGraphicView::OnRButtonDown(UINT nFlags, CPoint point) 
{
	// Capture all mouse messages
	SetCapture();

	// Right mouse button is down
	RMouseDown = TRUE;
	LastPoint = point;
	
	CView::OnRButtonDown(nFlags, point);
}

void CGraphicView::OnRButtonUp(UINT nFlags, CPoint point) 
{
	// if both buttons are now up, release the mouse
	if (!LMouseDown) {
		ReleaseCapture();
	}

	// Right mouse button is now up
	RMouseDown = false;
	
	CView::OnRButtonUp(nFlags, point);
}


void CGraphicView::OnMouseMove(UINT nFlags, CPoint point) 
{
	// Get the document to display
	CPhysTestDoc * doc = (CPhysTestDoc *)GetDocument();
	WWASSERT(doc);
#if 0
	PhysicsSceneClass * scene = doc->Scene;
	WWASSERT(scene);
#endif

	// When Only the Left mouse button is down, we "look-around" with
	// vertical locking (rotate about Z until Y points up)
	if ((LMouseDown) && (!RMouseDown)) {

		// Are we in a valid state?
		if (Initialized && doc->Scene) {
			
			RECT rect;
			GetClientRect (&rect);

			float mid_point_x = float(rect.right >> 1);
			float mid_point_y = float(rect.bottom >> 1);

			float last_point_x = ((float)LastPoint.x - mid_point_x) / mid_point_x;
			float last_point_y = (mid_point_y - (float)LastPoint.y) / mid_point_y;

			float point_x = ((float)point.x - mid_point_x) / mid_point_x;
			float point_y = (mid_point_y - (float)point.y) / mid_point_y;

			// invert the axes (being lazy here...)
			point_x = -point_x;
			point_y = -point_y;
			last_point_x = -last_point_x;
			last_point_y = -last_point_y;

			// Rotate around the object (orbit) using a 0.00F - 1.00F percentage of
			// the mouse coordinates
			Quaternion rotation = ::Trackball (last_point_x,last_point_y,point_x,point_y, 0.8F);

			// If we're not in CAMERA_FOLLOW mode, just rotate our view
			if (CameraMode != CAMERA_FOLLOW) {
				
				// Apply the rotation to the camera
				Matrix3D transform = Camera->Get_Transform ();
				Matrix3D::Multiply(transform,Build_Matrix3D(rotation),&transform);

				// Straighten out the Y axis
				Vector3 pos = transform.Get_Translation();
				Vector3 target = transform * Vector3(0,0,-1);
				transform.Look_At(pos,target,0.0f);

				// Rotate and translate the camera
				Camera->Set_Transform (transform);
			
			} else {

				// We're in camera follow mode, this means the camera is always going
				// to point right at the object.  We want to orbit the camera's position
				// around the object.
				Matrix3D camera_tm = Camera->Get_Transform();
				Vector3 relative_obj_pos(0,0,-10);

				CMainFrame * wnd = (CMainFrame *)::AfxGetMainWnd();
				PhysClass * obj = wnd->Peek_Selected_Object();
				if (obj) {

					Vector3 obj_pos;
					obj->Get_Transform().Get_Translation(&obj_pos);
					Matrix3D::Inverse_Transform_Vector(camera_tm,obj_pos,&relative_obj_pos);
	
				} 

				camera_tm.Translate(relative_obj_pos);
				Matrix3D rotation_tm;
				Build_Matrix3D(rotation).Get_Orthogonal_Inverse(rotation_tm);
				Matrix3D::Multiply(camera_tm,rotation_tm,&camera_tm);
				camera_tm.Translate(-relative_obj_pos);

				Camera->Set_Transform(camera_tm);
			}
		}        
	}

	LastPoint = point;
	Update_Pip_Camera();

	CView::OnMouseMove(nFlags, point);
}

void CGraphicView::Update_Pip_Camera(void)
{
	if (PipCamera && Camera) {
		Matrix3D transform = Camera->Get_Transform();
		transform.Set_Translation(Vector3(0,0,0));
		transform.Translate(Vector3(0,0,4));
		PipCamera->Set_Transform(transform);
	}
}