/*
 *  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.
 */


#ifndef DOSBOX_DMA_H
#define DOSBOX_DMA_H

enum DMAEvent {
	DMA_REACHED_TC,
	DMA_MASKED,
	DMA_UNMASKED,
	DMA_READ_COUNTER, /* the guest is reading the counter, which if accuracy is required, means process up to current time */
//	DMA_TRANSFEREND, this shouldn't really be a signal
};

enum DMATransfer {
    DMAT_VERIFY=0,
    DMAT_WRITE=1,
    DMAT_READ=2
};

enum DMAActor { /* i.e. who masked/unmasked the register */
    DMAA_NONE=0,
    DMAA_CONTROLLER=1,
    DMAA_GUEST=2,
    DMAA_REISSUE=3
};

const char *DMAActorStr(const DMAActor a);

class DmaChannel;
typedef void (* DMA_CallBack)(DmaChannel * chan,DMAEvent event);

class DmaChannel {
	public:
		uint32_t pagebase;
		uint16_t baseaddr;
		uint32_t curraddr;
		uint16_t basecnt;
		uint16_t currcnt;
		uint8_t channum;
		uint8_t pagenum;
		uint8_t DMA16_PAGESHIFT;
		uint32_t DMA16_ADDRMASK;
		uint8_t DMA16;
		uint8_t transfer_mode;
		DMAActor masked_by;
		bool increment;
		bool autoinit;
		bool masked;
		bool tcount;
		bool request;
		DMA_CallBack callback;

		// additional PC-98 proprietary feature:
		//  auto "bank" increment on DMA wraparound.
		//
		//  I/O port 29h:
		//    bits [7:4] = 0
		//    bits [3:2] = increment mode   0=64KB wraparound (no incr)  1=1MB boundary wrap   2=invalid   3=16MB boundary wrap
		//    bits [1:0] = which DMA channel to set
		//
		//  This value is set by:
		//    0 = 0x00
		//    1 = 0x0F
		//    2 = 0xF0 (probably why it's invalid)
		//    3 = 0xFF
		//
		// TODO: Does this setting stick or does it reset after normal legacy programming?
		// TODO: When the bank auto increments does it increment the actual register or just
		//       an internal copy?
		uint8_t page_bank_increment_wraparound = 0u;

		// user data for emulation
		uint64_t userData = 0;

		void page_bank_increment(void) { // to be called on DMA wraparound
			if (page_bank_increment_wraparound != 0u) {
				// FIXME: Improve this.
				// Currently this code assumes that the auto increment in PC-98 modifies the
				// register value (and therefore visible to the guest). Change this code if
				// that model is wrong.
				const uint8_t add =
					increment ? 0x01u : 0xFFu;
				const uint8_t nv =
					( pagenum        & (~page_bank_increment_wraparound)) +
					((pagenum + add) & ( page_bank_increment_wraparound));
				SetPage(nv);
			}
		}

		DmaChannel(uint8_t num, bool dma16);
		void DoCallBack(DMAEvent event) {
			if (callback)	(*callback)(this,event);
		}
		void SetMask(bool _mask) {
			masked=_mask;
			DoCallBack(masked ? DMA_MASKED : DMA_UNMASKED);
		}
		void Set128KMode(bool en) {
			// 128KB mode (legacy ISA) (en=true):
			//    page shift = 1        (discard bit 0 of page register)
			//    addr mask = 0x1FFFF   (all bits 0-15 become bits 1-16, bit 15 of addr takes the place of page register bit 0)
			// 64KB mode (modern PCI including Intel chipsets) (en=false):
			//    page shift = 0        (all 8 bits of page register are used)
			//    addr mask = 0xFFFF    (discard bit 15, bits 0-14 become bits 1-15 on ISA bus)
			DMA16_PAGESHIFT = (en && DMA16) ? 0x1 : 0x0; // nonzero if we're to discard bit 0 of page register
			DMA16_ADDRMASK = (1UL << ((en && DMA16) ? 17UL : 16UL)) - 1UL; // nonzero if (addrreg << 1) to cover 128KB, zero if (addrreg << 1) to discard MSB, limit to 64KB
		}
		void Register_Callback(DMA_CallBack _cb) { 
			callback = _cb; 
			masked_by = DMAA_REISSUE;
			SetMask(masked);
			if (callback) Raise_Request();
			else Clear_Request();
		}
		void ReachedTC(void) {
			tcount=true;
			DoCallBack(DMA_REACHED_TC);
		}
		void SetPage(uint8_t val) {
			pagenum=val;
			pagebase=(uint32_t)(pagenum >> DMA16_PAGESHIFT) << (uint32_t)((uint8_t)16u + DMA16_PAGESHIFT);
		}
		void Raise_Request(void) {
			request=true;
		}
		void Clear_Request(void) {
			request=false;
		}
		Bitu Read(Bitu want, uint8_t * buffer);
		Bitu Write(Bitu want, uint8_t * buffer);

		void SaveState( std::ostream& stream );
		void LoadState( std::istream& stream );
};

class DmaController {
	private:
		uint8_t ctrlnum;
		bool flipflop;
		DmaChannel* DmaChannels[4] = {};
	public:
		IO_ReadHandleObject DMA_ReadHandler[0x15];
		IO_WriteHandleObject DMA_WriteHandler[0x15];
		DmaController(uint8_t num) {
			flipflop = false;
			ctrlnum = num;		/* first or second DMA controller */
			for(uint8_t i=0;i<4;i++) {
				DmaChannels[i] = new DmaChannel(i+ctrlnum*4,ctrlnum==1);
			}
		}
		~DmaController(void) {
			for(uint8_t i=0;i<4;i++) {
				delete DmaChannels[i];
			}
		}
		DmaChannel * GetChannel(uint8_t chan) {
			if (chan<4) return DmaChannels[chan];
			else return NULL;
		}
		void WriteControllerReg(Bitu reg,Bitu val,Bitu len);
		Bitu ReadControllerReg(Bitu reg,Bitu len);

		void SaveState( std::ostream& stream );
		void LoadState( std::istream& stream );
};

DmaChannel * GetDMAChannel(uint8_t chan);

void CloseSecondDMAController(void);
bool SecondDMAControllerAvailable(void);

void DMA_SetWrapping(uint32_t wrap);

#endif
