/*
This library/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.  
*/

#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/neutrino.h>
#include <hw/pci.h>
#include <unistd.h>
#include <hw/inout.h>
#include <sys/mman.h>
#include <errno.h>
#include <pthread.h>


unsigned lastbus = 0;

int checkPciPresent()
{
	unsigned version;
	unsigned hardware;
	int result = pci_present(&lastbus,&version,&hardware);
	if (result == PCI_SUCCESS)
	{
		printf("PCI BIOS present\n");
		printf("Last PCI bus : %u\n",lastbus);
		printf("PCI Version : %u\n",version);
		printf("PCI HW code: 0x%x\n",hardware);
		return 1;
	}
	else
	{
		printf("PCI NOT PRESENT\n");
		return 0;
	}

}

void printSMIFlags(_Uint32t val)
{
	printf("SMI_EN set flags: ");

	//The following information comes from 7-series-chipset-pch-datasheet.pdf (section 13.8.3.7)
	if (val & (1u << 0)) printf("GBL_SMI_EN ");
	if (val & (1u << 1)) printf("EOS ");
	if (val & (1u << 2)) printf("BIOS_EN ");
	if (val & (1u << 3)) printf("LEGACY_USB_EN ");
	if (val & (1u << 4)) printf("SLP_SMI_EN ");
	if (val & (1u << 5)) printf("APMC_EN ");
	if (val & (1u << 6)) printf("SWSMI_TMR_EN ");
	if (val & (1u << 7)) printf("BIOS_RLS ");
	if (val & (1u << 11)) printf("MCSMI_EN ");
	if (val & (1u << 13)) printf("TCO_EN ");
	if (val & (1u << 14)) printf("PERIODIC_EN ");
	if (val & (1u << 17)) printf("LEGACY_USB2_EN ");
	if (val & (1u << 18)) printf("INTEL_USB2_EN ");
	if (val & (1u << 27)) printf("GPIO_UNLOCK_SMI_EN ");
	if (val & (1u << 31)) printf("xHCI_SMI_EN ");

	printf("\n");
}

int main()
{
	int result = 0;
	int pciH = 0;
	printf("SMI fix program executing...\n");


	//Since we are doing firmware level PCI stuff running at a high priority is good to not get interrupted by other applications
	int threadPolicy = SCHED_FIFO;
	pthread_t t = -1;
	t = pthread_self();
	struct sched_param param;
	param.sched_priority = 200;
	pthread_setschedparam(t,threadPolicy,&param);

	//This is required to be able to do IO operations
	int priv = ThreadCtl(_NTO_TCTL_IO ,0);
	if (priv == -1)
	{
		printf("Unable to set io priv's\n");
		return -1;
	}

	pciH = pci_attach( 0 );
	if (pciH == -1)
	{
		printf("Could not connect to PCI server\n");
		return -1;
	}

	//pci_rescan_bus(); //should not be needed

	if (checkPciPresent() == 0)
	{
		pci_detach(pciH);
		return -1;
	}

	struct pci_dev_info inf;
	int pidx;
	memset( &inf, 0, sizeof( inf ) );
	pidx = 0;

	/* Using pci -vv output it was determine that the device D31:F0 (Bridge PCI/ISA)
	 * has the following vendor/device code
	 *
	 * these numbers will have to be tuned for every platform.
	 *
	 * Note: a better implementation might be able to somehow scan all PCI devices and find the correct one.
	 */
	inf.VendorId = 0x8086;
	inf.DeviceId = 0x9c43;


	void *h = pci_attach_device( NULL, /*PCI_INIT_ALL |*/ PCI_SEARCH_VENDEV, pidx, &inf );
	if (h != NULL)
	{
		printf("Ven/Dev: 0x%x:0x%x\n",inf.VendorId,inf.DeviceId);
		
		int result;
		_Uint32t pmbase;
		result = pci_read_config32(inf.BusNumber,inf.DevFunc,0x40,1,&pmbase);

		//the last bit in pmbase must be set!
		if ((result == PCI_SUCCESS)&&(pmbase & 0x01))
		{
			pmbase = pmbase & 0xFF80;
			printf("PMBASE: 0x%x\n",pmbase);

			//time to map:
			//void *base = 0;
			uintptr_t  base = 0;
			base = mmap_device_io(128,pmbase);
			//mmap_device_io maps the io at the same address so that is a good extra check.
			//Note: I am not sure if this is ALWAYS the case (documentation isn't clear)
			if ((base != MAP_DEVICE_FAILED)&&((_Uint32t)base == pmbase))
			{
				//printf("PMBASE area mapped (0x%x)\n",base);

				_Uint32t val = in32(pmbase+0x30);
				printf("SMI_EN val: 0x%x\n",val);
				//if val is all 0's or all 1's it normally indicates an incorrect IO read
				if ((val != 0)&&(val != 0xFFFFFFFF))
				{
					printSMIFlags(val);

					/* The next big question is which on of the flags to clear
					 * Essentially this requires some experimentation.
					 * Notes:
					 * 1. if at all possible do not globally disable SMI! (should not be required)
					 * 2. some of the flags might not be clearable by software (TCO_EN for example on my setup)
					 * 3. Probably anything related to timers and/or emulation is suspect to being problematic
					 *
					 * In my case the solution was to clear SWSMI_TMR_EN (bit 6)
					 */
					val = val & (~(1 << 6));
					out32(pmbase+0x30,val);

					//test if the write was successful:
					_Uint32t val2;
					val2 = in32(pmbase+0x30);
					printf("SMI_EN new value:: 0x%x\n",val2);
					printSMIFlags(val2);

					if (val != val2)
						printf("ERROR UPDATING SMI_EN REGISTER!\n");
					else
						printf("SMI_EN successfully updated\n");

				}
				else
				{
					printf("Failed to read SMI_EN\n");
				}
			}
			else
			{
				printf("MMAP Failed!\n");
			}
			//unmap IO space
			if (base != MAP_DEVICE_FAILED)
			{
				int r = munmap_device_io(base,128);
				if (r == -1)
					printf("Error unmapping device IO\n");
			}

		}
		else
		{
			printf("Failed to read PMBASE\n");
		}

		//detach from device
		pci_detach_device(h);
	}
	else
	{
		printf("Failed to find PCI device\n");
	}
	//close pci
	pci_detach(pciH);

	printf("DONE!\n");
	return 0;
}
