/*
 *  Copyright (C) 2002-2017  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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */


class CacheBlock {
public:
	void Clear(void);
	void LinkTo(Bitu index,CacheBlock * toblock) {
		assert(toblock);
		link[index].to=toblock;
		link[index].next=toblock->link[index].from;
		toblock->link[index].from=this;
	}
	struct {
		Bit16u start,end;				//Where the page is the original code
		CodePageHandler * handler;		//Page containing this code
	} page;
	struct {
		Bit8u * start;					//Where in the cache are we
		Bitu size;
		CacheBlock * next;
		Bit8u * wmapmask;
		Bit16u maskstart;
		Bit16u masklen;
	} cache;
	struct {
		Bitu index;
		CacheBlock * next;
	} hash;
	struct {
		CacheBlock * to;
		CacheBlock * next;
		CacheBlock * from;
	} link[2];
	CacheBlock * crossblock;
};

static struct {
	struct {
		CacheBlock * first;
		CacheBlock * active;
		CacheBlock * free;
		CacheBlock * running;
	} block;
	Bit8u * pos;
	CodePageHandler * free_pages;
	CodePageHandler * used_pages;
	CodePageHandler * last_page;
} cache;

static CacheBlock link_blocks[2];

class CodePageHandler : public PageHandler {
public:
	CodePageHandler() {
		invalidation_map=NULL;
	}
	void SetupAt(Bitu _phys_page,PageHandler * _old_pagehandler) {
		phys_page=_phys_page;
		old_pagehandler=_old_pagehandler;
		flags=old_pagehandler->flags|PFLAG_HASCODE;
		flags&=~PFLAG_WRITEABLE;
		active_blocks=0;
		active_count=16;
		memset(&hash_map,0,sizeof(hash_map));
		memset(&write_map,0,sizeof(write_map));
		if (invalidation_map!=NULL) {
			free(invalidation_map);
			invalidation_map=NULL;
		}
	}
	bool InvalidateRange(Bitu start,Bitu end) {
		Bits index=1+(end>>DYN_HASH_SHIFT);
		bool is_current_block=false;
		Bit32u ip_point=SegPhys(cs)+reg_eip;
		ip_point=(PAGING_GetPhysicalPage(ip_point)-(phys_page<<12))+(ip_point&0xfff);
		while (index>=0) {
			Bitu map=0;
			for (Bitu count=start;count<=end;count++) map+=write_map[count];
			if (!map) return is_current_block;
			CacheBlock * block=hash_map[index];
			while (block) {
				CacheBlock * nextblock=block->hash.next;
				if (start<=block->page.end && end>=block->page.start) {
					if (ip_point<=block->page.end && ip_point>=block->page.start) is_current_block=true;
					block->Clear();
				}
				block=nextblock;
			}
			index--;
		}
		return is_current_block;
	}
	void writeb(PhysPt addr,Bitu val){
		if (GCC_UNLIKELY(old_pagehandler->flags&PFLAG_HASROM)) return;
		if (GCC_UNLIKELY((old_pagehandler->flags&PFLAG_READABLE)!=PFLAG_READABLE)) {
			E_Exit("wb:non-readable code page found that is no ROM page");
		}
		addr&=4095;
		if (host_readb(hostmem+addr)==(Bit8u)val) return;
		host_writeb(hostmem+addr,val);
		if (!*(Bit8u*)&write_map[addr]) {
			if (active_blocks) return;
			active_count--;
			if (!active_count) Release();
			return;
		} else if (!invalidation_map) {
			invalidation_map=(Bit8u*)malloc(4096);
			memset(invalidation_map,0,4096);
		}
		invalidation_map[addr]++;
		InvalidateRange(addr,addr);
	}
	void writew(PhysPt addr,Bitu val){
		if (GCC_UNLIKELY(old_pagehandler->flags&PFLAG_HASROM)) return;
		if (GCC_UNLIKELY((old_pagehandler->flags&PFLAG_READABLE)!=PFLAG_READABLE)) {
			E_Exit("ww:non-readable code page found that is no ROM page");
		}
		addr&=4095;
		if (host_readw(hostmem+addr)==(Bit16u)val) return;
		host_writew(hostmem+addr,val);
		if (!*(Bit16u*)&write_map[addr]) {
			if (active_blocks) return;
			active_count--;
			if (!active_count) Release();
			return;
		} else if (!invalidation_map) {
			invalidation_map=(Bit8u*)malloc(4096);
			memset(invalidation_map,0,4096);
		}
		(*(Bit16u*)&invalidation_map[addr])+=0x101;
		InvalidateRange(addr,addr+1);
	}
	void writed(PhysPt addr,Bitu val){
		if (GCC_UNLIKELY(old_pagehandler->flags&PFLAG_HASROM)) return;
		if (GCC_UNLIKELY((old_pagehandler->flags&PFLAG_READABLE)!=PFLAG_READABLE)) {
			E_Exit("wd:non-readable code page found that is no ROM page");
		}
		addr&=4095;
		if (host_readd(hostmem+addr)==(Bit32u)val) return;
		host_writed(hostmem+addr,val);
		if (!*(Bit32u*)&write_map[addr]) {
			if (active_blocks) return;
			active_count--;
			if (!active_count) Release();
			return;
		} else if (!invalidation_map) {
			invalidation_map=(Bit8u*)malloc(4096);
			memset(invalidation_map,0,4096);
		}
		(*(Bit32u*)&invalidation_map[addr])+=0x1010101;
		InvalidateRange(addr,addr+3);
	}
	bool writeb_checked(PhysPt addr,Bitu val) {
		if (GCC_UNLIKELY(old_pagehandler->flags&PFLAG_HASROM)) return false;
		if (GCC_UNLIKELY((old_pagehandler->flags&PFLAG_READABLE)!=PFLAG_READABLE)) {
			E_Exit("cb:non-readable code page found that is no ROM page");
		}
		addr&=4095;
		if (host_readb(hostmem+addr)==(Bit8u)val) return false;
		if (!*(Bit8u*)&write_map[addr]) {
			if (!active_blocks) {
				active_count--;
				if (!active_count) Release();
			}
		} else {
			if (!invalidation_map) {
				invalidation_map=(Bit8u*)malloc(4096);
				memset(invalidation_map,0,4096);
			}
			invalidation_map[addr]++;
			if (InvalidateRange(addr,addr)) {
				cpu.exception.which=SMC_CURRENT_BLOCK;
				return true;
			}
		}
		host_writeb(hostmem+addr,val);
		return false;
	}
	bool writew_checked(PhysPt addr,Bitu val) {
		if (GCC_UNLIKELY(old_pagehandler->flags&PFLAG_HASROM)) return false;
		if (GCC_UNLIKELY((old_pagehandler->flags&PFLAG_READABLE)!=PFLAG_READABLE)) {
			E_Exit("cw:non-readable code page found that is no ROM page");
		}
		addr&=4095;
		if (host_readw(hostmem+addr)==(Bit16u)val) return false;
		if (!*(Bit16u*)&write_map[addr]) {
			if (!active_blocks) {
				active_count--;
				if (!active_count) Release();
			}
		} else {
			if (!invalidation_map) {
				invalidation_map=(Bit8u*)malloc(4096);
				memset(invalidation_map,0,4096);
			}
			(*(Bit16u*)&invalidation_map[addr])+=0x101;
			if (InvalidateRange(addr,addr+1)) {
				cpu.exception.which=SMC_CURRENT_BLOCK;
				return true;
			}
		}
		host_writew(hostmem+addr,val);
		return false;
	}
	bool writed_checked(PhysPt addr,Bitu val) {
		if (GCC_UNLIKELY(old_pagehandler->flags&PFLAG_HASROM)) return false;
		if (GCC_UNLIKELY((old_pagehandler->flags&PFLAG_READABLE)!=PFLAG_READABLE)) {
			E_Exit("cd:non-readable code page found that is no ROM page");
		}
		addr&=4095;
		if (host_readd(hostmem+addr)==(Bit32u)val) return false;
		if (!*(Bit32u*)&write_map[addr]) {
			if (!active_blocks) {
				active_count--;
				if (!active_count) Release();
			}
		} else {
			if (!invalidation_map) {
				invalidation_map=(Bit8u*)malloc(4096);
				memset(invalidation_map,0,4096);
			}
			(*(Bit32u*)&invalidation_map[addr])+=0x1010101;
			if (InvalidateRange(addr,addr+3)) {
				cpu.exception.which=SMC_CURRENT_BLOCK;
				return true;
			}
		}
		host_writed(hostmem+addr,val);
		return false;
	}
    void AddCacheBlock(CacheBlock * block) {
		Bitu index=1+(block->page.start>>DYN_HASH_SHIFT);
		block->hash.next=hash_map[index];
		block->hash.index=index;
		hash_map[index]=block;
		block->page.handler=this;
		active_blocks++;
	}
    void AddCrossBlock(CacheBlock * block) {
		block->hash.next=hash_map[0];
		block->hash.index=0;
		hash_map[0]=block;
		block->page.handler=this;
		active_blocks++;
	}
	void DelCacheBlock(CacheBlock * block) {
		active_blocks--;
		active_count=16;
		CacheBlock * * where=&hash_map[block->hash.index];
		while (*where!=block) {
			where=&((*where)->hash.next);
			//Will crash if a block isn't found, which should never happen.
		}
		*where=block->hash.next;
		if (GCC_UNLIKELY(block->cache.wmapmask!=NULL)) {
			for (Bitu i=block->page.start;i<block->cache.maskstart;i++) {
				if (write_map[i]) write_map[i]--;
			}
			Bitu maskct=0;
			for (Bitu i=block->cache.maskstart;i<=block->page.end;i++,maskct++) {
				if (write_map[i]) {
					if ((maskct>=block->cache.masklen) || (!block->cache.wmapmask[maskct])) write_map[i]--;
				}
			}
			free(block->cache.wmapmask);
			block->cache.wmapmask=NULL;
		} else {
			for (Bitu i=block->page.start;i<=block->page.end;i++) {
				if (write_map[i]) write_map[i]--;
			}
		}
	}
	void Release(void) {
		MEM_SetPageHandler(phys_page,1,old_pagehandler);
		PAGING_ClearTLB();
		if (prev) prev->next=next;
		else cache.used_pages=next;
		if (next) next->prev=prev;
		else cache.last_page=prev;
		next=cache.free_pages;
		cache.free_pages=this;
		prev=0;
	}
	void ClearRelease(void) {
		for (Bitu index=0;index<(1+DYN_PAGE_HASH);index++) {
			CacheBlock * block=hash_map[index];
			while (block) {
				CacheBlock * nextblock=block->hash.next;
				block->page.handler=0;			//No need, full clear
				block->Clear();
				block=nextblock;
			}
		}
		Release();
	}
	CacheBlock * FindCacheBlock(Bitu start) {
		CacheBlock * block=hash_map[1+(start>>DYN_HASH_SHIFT)];
		while (block) {
			if (block->page.start==start) return block;
			block=block->hash.next;
		}
		return 0;
	}
	HostPt GetHostReadPt(Bitu phys_page) { 
		hostmem=old_pagehandler->GetHostReadPt(phys_page);
		return hostmem;
	}
	HostPt GetHostWritePt(Bitu phys_page) { 
		return GetHostReadPt( phys_page );
	}
public:
	Bit8u write_map[4096];
	Bit8u * invalidation_map;
	CodePageHandler * next, * prev;
private:
	PageHandler * old_pagehandler;
	CacheBlock * hash_map[1+DYN_PAGE_HASH];
	Bitu active_blocks;
	Bitu active_count;
	HostPt hostmem;	
	Bitu phys_page;
};


static INLINE void cache_addunsedblock(CacheBlock * block) {
	block->cache.next=cache.block.free;
	cache.block.free=block;
}

static CacheBlock * cache_getblock(void) {
	CacheBlock * ret=cache.block.free;
	if (!ret) E_Exit("Ran out of CacheBlocks" );
	cache.block.free=ret->cache.next;
	ret->cache.next=0;
	return ret;
}

void CacheBlock::Clear(void) {
	Bitu ind;
	/* Check if this is not a cross page block */
	if (hash.index) for (ind=0;ind<2;ind++) {
		CacheBlock * fromlink=link[ind].from;
		link[ind].from=0;
		while (fromlink) {
			CacheBlock * nextlink=fromlink->link[ind].next;
			fromlink->link[ind].next=0;
			fromlink->link[ind].to=&link_blocks[ind];
			fromlink=nextlink;
		}
		if (link[ind].to!=&link_blocks[ind]) {
			CacheBlock * * wherelink=&link[ind].to->link[ind].from;
			while (*wherelink != this && *wherelink) {
				wherelink = &(*wherelink)->link[ind].next;
			}
			if(*wherelink) 
				*wherelink = (*wherelink)->link[ind].next;
			else
				LOG(LOG_CPU,LOG_ERROR)("Cache anomaly. please investigate");
		}
	} else 
		cache_addunsedblock(this);
	if (crossblock) {
		crossblock->crossblock=0;
		crossblock->Clear();
		crossblock=0;
	}
	if (page.handler) {
		page.handler->DelCacheBlock(this);
		page.handler=0;
	}
	if (cache.wmapmask){
		free(cache.wmapmask);
		cache.wmapmask=NULL;
	}
}


static CacheBlock * cache_openblock(void) {
	CacheBlock * block=cache.block.active;
	/* check for enough space in this block */
	Bitu size=block->cache.size;
	CacheBlock * nextblock=block->cache.next;
	if (block->page.handler) 
		block->Clear();
	while (size<CACHE_MAXSIZE) {
		if (!nextblock) 
			goto skipresize;
		size+=nextblock->cache.size;
		CacheBlock * tempblock=nextblock->cache.next;
		if (nextblock->page.handler) 
			nextblock->Clear();
		cache_addunsedblock(nextblock);
		nextblock=tempblock;
	}
skipresize:
	block->cache.size=size;
	block->cache.next=nextblock;
	cache.pos=block->cache.start;
	return block;
}

static void cache_closeblock(void) {
	CacheBlock * block=cache.block.active;
	block->link[0].to=&link_blocks[0];
	block->link[1].to=&link_blocks[1];
	block->link[0].from=0;
	block->link[1].from=0;
	block->link[0].next=0;
	block->link[1].next=0;
	/* Close the block with correct alignments */
	Bitu written=cache.pos-block->cache.start;
	if (written>block->cache.size) {
		if (!block->cache.next) {
			if (written>block->cache.size+CACHE_MAXSIZE) E_Exit("CacheBlock overrun 1 %d",written-block->cache.size);	
		} else E_Exit("CacheBlock overrun 2 written %d size %d",written,block->cache.size);	
	} else {
		Bitu new_size;
		Bitu left=block->cache.size-written;
		/* Smaller than cache align then don't bother to resize */
		if (left>CACHE_ALIGN) {
			new_size=((written-1)|(CACHE_ALIGN-1))+1;
			CacheBlock * newblock=cache_getblock();
			newblock->cache.start=block->cache.start+new_size;
			newblock->cache.size=block->cache.size-new_size;
			newblock->cache.next=block->cache.next;
			block->cache.next=newblock;
			block->cache.size=new_size;
		}
	}
	/* Advance the active block pointer */
	if (!block->cache.next) {
//		LOG_MSG("Cache full restarting");
		cache.block.active=cache.block.first;
	} else {
		cache.block.active=block->cache.next;
	}
}

static INLINE void cache_addb(Bit8u val) {
	*cache.pos++=val;
}

static INLINE void cache_addw(Bit16u val) {
	*(Bit16u*)cache.pos=val;
	cache.pos+=2;
}

static INLINE void cache_addd(Bit32u val) {
	*(Bit32u*)cache.pos=val;
	cache.pos+=4;
}


static void gen_return(BlockReturn retcode);

static Bit8u * cache_code_start_ptr=NULL;
static Bit8u * cache_code=NULL;
static Bit8u * cache_code_link_blocks=NULL;
static CacheBlock * cache_blocks=NULL;

/* Define temporary pagesize so the MPROTECT case and the regular case share as much code as possible */
#if (C_HAVE_MPROTECT)
#define PAGESIZE_TEMP PAGESIZE
#else 
#define PAGESIZE_TEMP 4096
#endif

static bool cache_initialized = false;

static void cache_init(bool enable) {
	Bits i;
	if (enable) {
		if (cache_initialized) return;
		cache_initialized = true;
		if (cache_blocks == NULL) {
			cache_blocks=(CacheBlock*)malloc(CACHE_BLOCKS*sizeof(CacheBlock));
			if(!cache_blocks) E_Exit("Allocating cache_blocks has failed");
			memset(cache_blocks,0,sizeof(CacheBlock)*CACHE_BLOCKS);
			cache.block.free=&cache_blocks[0];
			for (i=0;i<CACHE_BLOCKS-1;i++) {
				cache_blocks[i].link[0].to=(CacheBlock *)1;
				cache_blocks[i].link[1].to=(CacheBlock *)1;
				cache_blocks[i].cache.next=&cache_blocks[i+1];
			}
		}
		if (cache_code_start_ptr==NULL) {
#if defined (WIN32)
			cache_code_start_ptr=(Bit8u*)VirtualAlloc(0,CACHE_TOTAL+CACHE_MAXSIZE+PAGESIZE_TEMP-1+PAGESIZE_TEMP,
				MEM_COMMIT,PAGE_EXECUTE_READWRITE);
			if (!cache_code_start_ptr)
				cache_code_start_ptr=(Bit8u*)malloc(CACHE_TOTAL+CACHE_MAXSIZE+PAGESIZE_TEMP-1+PAGESIZE_TEMP);
#else
			cache_code_start_ptr=(Bit8u*)malloc(CACHE_TOTAL+CACHE_MAXSIZE+PAGESIZE_TEMP-1+PAGESIZE_TEMP);
#endif
			if(!cache_code_start_ptr) E_Exit("Allocating dynamic core cache memory failed");

			cache_code=(Bit8u*)(((Bitu)cache_code_start_ptr + PAGESIZE_TEMP-1) & ~(PAGESIZE_TEMP-1)); //Bitu is same size as a pointer.

			cache_code_link_blocks=cache_code;
			cache_code+=PAGESIZE_TEMP;

#if (C_HAVE_MPROTECT)
			if(mprotect(cache_code_link_blocks,CACHE_TOTAL+CACHE_MAXSIZE+PAGESIZE_TEMP,PROT_WRITE|PROT_READ|PROT_EXEC))
				LOG_MSG("Setting excute permission on the code cache has failed!");
#endif
			CacheBlock * block=cache_getblock();
			cache.block.first=block;
			cache.block.active=block;
			block->cache.start=&cache_code[0];
			block->cache.size=CACHE_TOTAL;
			block->cache.next=0;								//Last block in the list
		}
		/* Setup the default blocks for block linkage returns */
		cache.pos=&cache_code_link_blocks[0];
		link_blocks[0].cache.start=cache.pos;
		gen_return(BR_Link1);
		cache.pos=&cache_code_link_blocks[32];
		link_blocks[1].cache.start=cache.pos;
		gen_return(BR_Link2);
		cache.free_pages=0;
		cache.last_page=0;
		cache.used_pages=0;
		/* Setup the code pages */
		for (i=0;i<CACHE_PAGES;i++) {
			CodePageHandler * newpage=new CodePageHandler();
			newpage->next=cache.free_pages;
			cache.free_pages=newpage;
		}
	}
}

static void cache_close(void) {
/*	for (;;) {
		if (cache.used_pages) {
			CodePageHandler * cpage=cache.used_pages;
			CodePageHandler * npage=cache.used_pages->next;
			cpage->ClearRelease();
			delete cpage;
			cache.used_pages=npage;
		} else break;
	}
	if (cache_blocks != NULL) {
		free(cache_blocks);
		cache_blocks = NULL;
	}
	if (cache_code_start_ptr != NULL) {
		### care: under windows VirtualFree() has to be used if
		###       VirtualAlloc was used for memory allocation
		free(cache_code_start_ptr);
		cache_code_start_ptr = NULL;
	}
	cache_code = NULL;
	cache_code_link_blocks = NULL;
	cache_initialized = false; */
}

static void cache_reset(void) {
	if (cache_initialized) {
		for (;;) {
			if (cache.used_pages) {
				CodePageHandler * cpage=cache.used_pages;
				CodePageHandler * npage=cache.used_pages->next;
				cpage->ClearRelease();
				delete cpage;
				cache.used_pages=npage;
			} else break;
		}

		if (cache_blocks == NULL) {
			cache_blocks=(CacheBlock*)malloc(CACHE_BLOCKS*sizeof(CacheBlock));
			if(!cache_blocks) E_Exit("Allocating cache_blocks has failed");
		}
		memset(cache_blocks,0,sizeof(CacheBlock)*CACHE_BLOCKS);
		cache.block.free=&cache_blocks[0];
		for (Bits i=0;i<CACHE_BLOCKS-1;i++) {
			cache_blocks[i].link[0].to=(CacheBlock *)1;
			cache_blocks[i].link[1].to=(CacheBlock *)1;
			cache_blocks[i].cache.next=&cache_blocks[i+1];
		}

		if (cache_code_start_ptr==NULL) {
#if defined (WIN32)
			cache_code_start_ptr=(Bit8u*)VirtualAlloc(0,CACHE_TOTAL+CACHE_MAXSIZE+PAGESIZE_TEMP-1+PAGESIZE_TEMP,
				MEM_COMMIT,PAGE_EXECUTE_READWRITE);
			if (!cache_code_start_ptr)
				cache_code_start_ptr=(Bit8u*)malloc(CACHE_TOTAL+CACHE_MAXSIZE+PAGESIZE_TEMP-1+PAGESIZE_TEMP);
#else
			cache_code_start_ptr=(Bit8u*)malloc(CACHE_TOTAL+CACHE_MAXSIZE+PAGESIZE_TEMP-1+PAGESIZE_TEMP);
#endif
			if (!cache_code_start_ptr) E_Exit("Allocating dynamic core cache memory failed");

			cache_code=(Bit8u*)(((Bitu)cache_code_start_ptr + PAGESIZE_TEMP-1) & ~(PAGESIZE_TEMP-1)); //Bitu is same size as a pointer.

			cache_code_link_blocks=cache_code;
			cache_code+=PAGESIZE_TEMP;

#if (C_HAVE_MPROTECT)
			if(mprotect(cache_code_link_blocks,CACHE_TOTAL+CACHE_MAXSIZE+PAGESIZE_TEMP,PROT_WRITE|PROT_READ|PROT_EXEC))
				LOG_MSG("Setting excute permission on the code cache has failed!");
#endif
		}

		CacheBlock * block=cache_getblock();
		cache.block.first=block;
		cache.block.active=block;
		block->cache.start=&cache_code[0];
		block->cache.size=CACHE_TOTAL;
		block->cache.next=0;								//Last block in the list

		/* Setup the default blocks for block linkage returns */
		cache.pos=&cache_code_link_blocks[0];
		link_blocks[0].cache.start=cache.pos;
		gen_return(BR_Link1);
		cache.pos=&cache_code_link_blocks[32];
		link_blocks[1].cache.start=cache.pos;
		gen_return(BR_Link2);
		cache.free_pages=0;
		cache.last_page=0;
		cache.used_pages=0;
		/* Setup the code pages */
		for (Bitu i=0;i<CACHE_PAGES;i++) {
			CodePageHandler * newpage=new CodePageHandler();
			newpage->next=cache.free_pages;
			cache.free_pages=newpage;
		}
	}
}
