/*
 *  Copyright (C) 2002-2021  The DOSBox 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 the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include <assert.h>
#include <stdlib.h>
#include <string.h>

#include "dosbox.h"
#include "config.h"
#include "logging.h"
#include "setup.h"
#include "cross.h"
#include "control.h"

#include "paging.h"
#include "mem.h"

#include "voodoo.h"
#include "pci_bus.h"
#include "voodoo_interface.h"

class VOODOO;
static VOODOO* voodoo_dev;

static uint32_t assigned_lfb = 0;
static uint32_t voodoo_current_lfb=(VOODOO_INITIAL_LFB&0xffff0000);

static bool voodoo_pci_enabled = false;
static MEM_Callout_t voodoo_lfb_cb = MEM_Callout_t_none;

PageHandler* voodoo_lfb_memio_cb(MEM_CalloutObject &co,Bitu phys_page) {
	(void)phys_page;//UNUSED
	(void)co;//UNUSED
	if (voodoo_current_lfb == 0)
		return NULL;

	return VOODOO_GetPageHandler();
}

void voodoo_lfb_cb_free(void) {
	if (voodoo_lfb_cb != MEM_Callout_t_none) {
		MEM_FreeCallout(voodoo_lfb_cb);
		voodoo_lfb_cb = MEM_Callout_t_none;
	}
}

void voodoo_lfb_cb_init() {
	if (voodoo_lfb_cb == MEM_Callout_t_none) {
		voodoo_lfb_cb = MEM_AllocateCallout(MEM_TYPE_PCI);
		if (voodoo_lfb_cb == MEM_Callout_t_none) E_Exit("Unable to allocate voodoo cb for LFB");
	}

	{
		MEM_CalloutObject *cb = MEM_GetCallout(voodoo_lfb_cb);

		assert(cb != NULL);

		cb->Uninstall();

		if (voodoo_current_lfb != 0 && voodoo_pci_enabled) {
			/* VOODOO_PAGES is a power of two already */
			LOG_MSG("VOODOO LFB now at %x",voodoo_current_lfb);
			cb->Install(voodoo_current_lfb>>12UL,MEMMASK_Combine(MEMMASK_FULL,MEMMASK_Range(VOODOO_PAGES)),voodoo_lfb_memio_cb);
		}
		else {
			LOG_MSG("VOODOO LFB disabled");
		}

		MEM_PutCallout(cb);
	}
}

class VOODOO:public Module_base{
	private:
		Bits emulation_type;
	public:
		VOODOO(Section* configuration):Module_base(configuration){
			emulation_type=-1;

			Section_prop * section=static_cast<Section_prop *>(configuration);
			std::string voodoo_type_str(section->Get_string("voodoo_card"));
			if (voodoo_type_str=="false") {
				emulation_type=0;
			} else if (voodoo_type_str=="software") {
				emulation_type=1;
#if C_OPENGL
			} else if ((voodoo_type_str=="opengl") || (voodoo_type_str=="auto")) {
				emulation_type=2;
#else
			} else if (voodoo_type_str=="auto") {
				emulation_type=1;
#endif
			} else {
				emulation_type=0;
			}

			Bits card_type = 1;
			bool max_voodoomem = true;
			if (section->Get_bool("voodoo_maxmem"))
				max_voodoomem = true;
			else
				max_voodoomem = false;

			bool needs_pci_device = false;

			switch (emulation_type) {
				case 1:
				case 2:
					Voodoo_Initialize(emulation_type, card_type, max_voodoomem);
					needs_pci_device = true;
					break;
				default:
					break;
			}

			if (needs_pci_device) {
				LOG(LOG_MISC,LOG_DEBUG)("Voodoo 3Dfx LFB at 0x%lx",(unsigned long)voodoo_current_lfb);
				PCI_AddSST_Device((Bitu)card_type);
			}
		}

		~VOODOO(){
			PCI_RemoveSST_Device();

			if (emulation_type<=0) return;

			switch (emulation_type) {
				case 1:
				case 2:
					Voodoo_Shut_Down();
					break;
				default:
					break;
			}

			emulation_type=-1;
		}

		void PCI_InitEnable(Bitu val) {
			switch (emulation_type) {
				case 1:
				case 2:
					Voodoo_PCI_InitEnable(val);
					break;
				default:
					break;
			}
		}

		void PCI_Enable(bool enable) {
			switch (emulation_type) {
				case 1:
				case 2:
					Voodoo_PCI_Enable(enable);
					break;
				default:
					break;
			}
		}

		PageHandler* GetPageHandler() {
			switch (emulation_type) {
				case 1:
				case 2:
					return Voodoo_GetPageHandler();
				default:
					break;
			}
			return NULL;
		}

};


void VOODOO_PCI_InitEnable(Bitu val) {
	if (voodoo_dev!=NULL) {
		voodoo_dev->PCI_InitEnable(val);
	}
}

void VOODOO_PCI_Enable(bool enable) {
	if (voodoo_dev!=NULL) {
		voodoo_dev->PCI_Enable(enable);
	}
}


void VOODOO_PCI_SetLFB(uint32_t lfbaddr) {
	lfbaddr &= VOODOO_LFB_ALIGNMASK;

	if (lfbaddr == voodoo_current_lfb)
		return;

	voodoo_current_lfb = lfbaddr;
	voodoo_lfb_cb_init();
}

bool VOODOO_PCI_CheckLFBPage(Bitu page) {
	if (voodoo_current_lfb != 0 &&
		(page>=(voodoo_current_lfb>>12)) &&
		(page<(voodoo_current_lfb>>12)+VOODOO_PAGES))
		return true;
	return false;
}

PageHandler* VOODOO_GetPageHandler() {
	if (voodoo_dev!=NULL) {
		return voodoo_dev->GetPageHandler();
	}
	return NULL;
}


void VOODOO_Destroy(Section* /*sec*/) {
	if (voodoo_dev!=NULL) {
		delete voodoo_dev;
		voodoo_dev=NULL;
	}
}

void VOODOO_OnPowerOn(Section* /*sec*/) {
	if (voodoo_dev == NULL) {
		voodoo_pci_enabled = true;

		voodoo_current_lfb = VOODOO_INITIAL_LFB & VOODOO_LFB_ALIGNMASK;

		/* must not overlap system RAM */
		if (voodoo_current_lfb < (MEM_TotalPages()*4096))
			voodoo_current_lfb = (MEM_TotalPages()*4096);
		if (voodoo_current_lfb < assigned_lfb)
			voodoo_current_lfb = assigned_lfb;

		voodoo_current_lfb += ~VOODOO_LFB_ALIGNMASK; /* round up */
		voodoo_current_lfb &= VOODOO_LFB_ALIGNMASK;

		voodoo_dev = new VOODOO(control->GetSection("voodoo"));

		voodoo_lfb_cb_init();
	}
}

void VOODOO_Init() {
	LOG(LOG_MISC,LOG_DEBUG)("Initializing Voodoo/3DFX emulation");

	/* the purpose of this is so that, if everything is crammed up at the top to make room for system RAM, the S3 and 3Dfx do not conflict */
	assigned_lfb = MEM_HardwareAllocate("Voodoo 3Dfx",16ul << 20ul/*16MB*/);

	AddExitFunction(AddExitFunctionFuncPair(VOODOO_Destroy),true);
	AddVMEventFunction(VM_EVENT_POWERON,AddVMEventFunctionFuncPair(VOODOO_OnPowerOn));
}
