#ifndef _uDRIVE_uSD_G1_h_
#define _uDRIVE_uSD_G1_h_

#include <inttypes.h>

namespace _4Dsystems
{
/**
 * @defgroup uDRIVE uDRIVE-uSD-G1 Module
 * @{
 */ 	

/**
 * @defgroup uDRIVE_RESPONSE uDRIVE-uSD-G1 Message responses
 * @{
 */ 	
/** 
 * ACK tells the host that the command was understood and 
 *  the operation is completed
 **/
#define ACK 0x06
/** 
 * NACK The command sent to the module was incorrect.
 **/
#define NACK 0x15
/**
 * @}
 **/

/**
 * @defgroup uDRIVE_COMANDS uDRIVE-uSD-G1 Commands Opcode
 * @{
 **/
/** Initialize module (autobaud, etc...) **/
#define INITIALIZE_MODULE   0x55
/** Query module version **/
#define VERSION 0x56
/** Extended command **/
#define EXTENDED 0x40
/**
 * @}
 **/

/**
 * @defgroup uDRIVE_EXT_COMANDS uDRIVE-uSD-G1 Extended Commands Opcode
 * @{
 **/

/** Initialize disk drive **/
#define INITIALIZE_DISK 0x69

/** Set Memory Address **/
#define SET_MEMORY_ADDRESS 0x41

/**
 * @defgroup Sector_Commands Sector commands
 * @{
 **/
/** Read sector block data **/
#define READ_SECTOR_BLOCK_DATA 0x52
/** Write sector block data **/
#define WRITE_SECTOR_BLOCK_DATA 0x57
/**
 * @}
 **/

/**
 * @defgroup BYTE_Commands Single byte operations
 * @{
 **/
/** Read single byte **/
#define READ_SINGLE_BYTE 0x72
/** Write single byte **/
#define WRITE_SINGLE_BYTE 0x77
/**
 * @}
 **/

// Let's hope they'll release a new firmware handling fat16...

/**
 * @}
 **/

/**
 * Error values
 **/
enum Error
{
	OK = 0,            /**< No errors                               **/
	COMMAND_ERROR,     /**< The command could not be understood     **/
	UNITIALIZED_ERROR, /**< The module wasn't initialized           **/
	INVALID_DATA       /**< Invalid data (null pointer for example) **/	
};

/**
 * @brief 4D Systems uDrive-uSD-G1 module.
 * You can use any serial implementation (software, hardware). But it
 * has to be a class/structure providing the following methods :
 * 	- uint8_t read()
 * 	- void print(uint8_t)
 * 	- uint8_t available()
 * 
 * Here's a little example.
 * @code
 * int8_t  err;
 * uint8_t buffer[512];
 * 
 * Serial.begin(115200);
 * Serial.flush();
 * 
 * _4Dsystems::uDRIVE_uSD_G1<HardwareSerial> microDrive;
 * 
 * err = microDrive.Initialize(&Serial);
 * if(err == _4Dsystems::OK)
 * {
 *     err = microDrive.ReadSectorBlock(0x00000000, buffer);
 *     if(err == _4Dsystems::OK)
 *     {
 *         // do something
 *     } 
 * }
 * @endcode
 **/
template <typename SerialInterface>
class uDRIVE_uSD_G1
{
	private:
		uint8_t _state; /**< Module state **/
					
		SerialInterface* _serial; /**< Serial interface **/
		
		/**
		 * Wait for the module to send back response.
		 * @note internal method
		 * @return
		 *	- OK if the module sent an ACK.
		 *	- COMMAND_ERROR otherwise.
		 **/
		inline int8_t ValidateCommand()
		{
			int8_t ret;
			/* Wait for ack */
			while( ! _serial->available() )
			{
				delay(10);
			}
			ret = _serial->read();

			/* Was the command understood ? */
			if(ret != ACK)
			{
				return COMMAND_ERROR;
			}
		
			return OK;
		}
		
		/**
		 * Read n bytes from serial
		 * @note internal method
		 * @param iPtr pointer to the byte buffer
		 * @param nElements number of bytes to read
		 **/
		inline void _read(uint8_t *iPtr, size_t nElements)
		{
			while(nElements--)
			{
				while( ! _serial->available() )
				{
					delay(10);
				}
				*iPtr++ = _serial->read();
			}

		}
					
	public:	
		/** Constructor **/
		uDRIVE_uSD_G1() :
			_state(0),
			_serial(NULL)
		{}
		/** Destructor **/
		~uDRIVE_uSD_G1() {}
		
		/**
		 * Initialize module and memory card
		 * @param iSerial serial interface
		 * @return 
		 * 	- OK if the module sent an ACK.
		 *	- COMMAND_ERROR otherwise.
		 **/
		int8_t Initialize(SerialInterface* iSerial)
		{
			int8_t ret;
	
			_state      = 0;
			_serial     = iSerial;

			_serial->print((uint8_t)INITIALIZE_MODULE);
			
			ret = ValidateCommand();
			if(ret != OK)
			{
				return ret;
			}

			_serial->print((uint8_t)EXTENDED);
			_serial->print((uint8_t)INITIALIZE_DISK);

			ret = ValidateCommand();
			if(ret != OK)
			{
				return ret;
			}

			_state = 1;
			return OK;
		}
		
		/**
		 * Read a sector (512 bytes) from the card memory.
		 * @param iAddress 3 bytes sector address. Sector address
		 *        range from 0 to 16,777,215 dependning on the card
		 *        capacity. Each sector is 512 bytes long.
		 * @param iData pointer to the memory where the sector
		 *        will be stored. It must be large enough to store
		 *        a complete sector.
		 * @return
		 *	- INVALID_DATA if iData is NULL.
		 * 	- UNITIALIZED_ERROR if the module wasn't initialized.
		 * 	- OK if the sector was succesfully read.
		 **/
		int8_t ReadSectorBlock(const uint32_t& iAddress, uint8_t* iData)
		{
			uint8_t ret;
			uint16_t byteCount;
			
			/* Sanity check */
			if(iData == NULL)
			{
				return INVALID_DATA;
			}
			
			/* Check state */
			if(!_state)
			{
				return UNITIALIZED_ERROR;
			}
			
			_serial->print((uint8_t)EXTENDED);
			_serial->print((uint8_t)READ_SECTOR_BLOCK_DATA);
			_serial->print((uint8_t)((iAddress >> 16) & 0xff));
			_serial->print((uint8_t)((iAddress >>  8) & 0xff));
			_serial->print((uint8_t)((iAddress      ) & 0xff));
				
			/* Read 512 bytes! (Let's hope the array is large enough) */
			_read(iData, 512);
					
			return OK;
		}

		/**
		 * Download and write 512 bytes to a sector of the card memory.
		 * @param iAddress 3 bytes sector address. Sector address
		 *        range from 0 to 16,777,215 dependning on the card
		 *        capacity. Each sector is 512 bytes long.
		 * @param iData pointer to the data to be written. It must
		 *        contains a least 512 bytes.
		 * @return
		 *	- INVALID_DATA if iData is NULL.
		 *	- COMMAND_ERROR if the module was unable to process the command.
		 * 	- UNITIALIZED_ERROR if the module wasn't initialized.
		 * 	- OK if the write operation was succesful.
		 **/
		int8_t WriteSectorBlock(const uint32_t& iAddress, uint8_t* iData)
		{
			uint16_t byteCount;

			/* Sanity check */
			if(iData == NULL)
			{
				return INVALID_DATA;
			}
			
			/* Check state */
			if(!_state)
			{
				return UNITIALIZED_ERROR;
			}
			
			_serial->print((uint8_t)EXTENDED);
			_serial->print((uint8_t)WRITE_SECTOR_BLOCK_DATA);
			_serial->print((uint8_t)((iAddress >> 16) & 0xff));
			_serial->print((uint8_t)((iAddress >>  8) & 0xff));
			_serial->print((uint8_t)((iAddress      ) & 0xff));
			
					
			/* Write 512 bytes! (Let's hope the array is large enough) */
			for(byteCount=0; byteCount<512; ++byteCount, ++iData)
			{
				_serial->print(*iData);
			}
			
			return ValidateCommand();
		}
					
		/**
		 * Set the memory address pointer for byte wise operations.
		 * This pointer is automatically incremented to the next
		 * memory address after a byte is read or written.
		 * @param iAddress 4 bytes memory address.
		 * @return
		 * 	- UNITIALIZED_ERROR if the module wasn't initialized.
 		 *	- COMMAND_ERROR if the module was unable to process the command.
		 * 	- OK if the memory pointer was succesfully set.
		 **/
		int8_t SetMemoryAddress(const uint32_t& iAddress)
		{
			/* Check state */
			if(!_state)
			{
				return UNITIALIZED_ERROR;
			}
			
			_serial->print((uint8_t)EXTENDED);
			_serial->print((uint8_t)SET_MEMORY_ADDRESS);
			_serial->print((uint8_t)((iAddress >> 24) & 0xff));
			_serial->print((uint8_t)((iAddress >> 16) & 0xff));
			_serial->print((uint8_t)((iAddress >>  8) & 0xff));
			_serial->print((uint8_t)((iAddress      ) & 0xff));
			
			return ValidateCommand();
		}

		/**
		 * Read a single byte.
		 * You must set the memory address location using SetMemoryAddress
		 * before using this method. The memory address location 
		 * pointer is automatically incremented to the next address
		 * location.
		 * @param iByte byte to read
		 * @return
		 * 	- UNITIALIZED_ERROR if the module wasn't initialized.
		 * 	- OK if the memory pointer was succesfully set.
		 **/
		int8_t ReadSingleByte (uint8_t& iByte)
		{
			/* Check state */
			if(!_state)
			{
				return UNITIALIZED_ERROR;
			}
			
			_serial->print((uint8_t)EXTENDED);
			_serial->print((uint8_t)READ_SINGLE_BYTE);
			
			/* TODO : check if a ACK/NACK is sent */
			
			while( ! _serial->available() )
			{
				delay(10);
			}
			
			iByte = _serial->read();
			
			return OK;
		}
		
		/**
		 * Write a single byte.
		 * You must set the memory address location using SetMemoryAddress
		 * before using this method. The memory address location 
		 * pointer is automatically incremented to the next address
		 * location.
		 * @param iByte byte to write
  		 * @return
		 *	- COMMAND_ERROR if the module was unable to process the command.
		 * 	- UNITIALIZED_ERROR if the module wasn't initialized.
		 * 	- OK if the write operation was succesful.
  		 * 
  		 * @warning PmmC firmware rev1 has bug. The internal memory
  		 *          address location is not incremented to the next
  		 *          sector block (fixed in rev2 24/03/2008). So if 
  		 *          you don't plan to upgrade the firmware, or has
  		 *          no mean to you should keep an counter and call
  		 *          SetMemoryAddress when you cross a sector 
  		 *          boundary (ie when 
  		 *          (baseAddress + counter) & 511 == 0).
  		 **/
		int8_t WriteSingleByte(const uint8_t& iByte)
		{
			/* Check state */
			if(!_state)
			{
				return UNITIALIZED_ERROR;
			}
			
			_serial->print((uint8_t)EXTENDED);
			_serial->print((uint8_t)WRITE_SINGLE_BYTE);
					
			_serial->print(iByte);
			
			return ValidateCommand();
		}
		
		
		/**
		 * Devive information
		 **/
		union DeviceInfo
		{
			struct
			{
				/**
				 * Device type. It should be 0x03. 
				 * Otherwise something went wrong :)
				 * 	- 0x00 : micro-OLED
				 * 	- 0x01 : micro-LCD
				 * 	- 0x02 : micro-VGA
				 * 	- 0x03 : micro-Drive
				 **/
				uint8_t device_type;
				/** Hardware revision **/
				uint8_t hardware_rev;
				/** PmmC firmware version **/
				uint8_t PmmC_rev;
				/** This byte is reserved for future use.
				 *  It should be ignored if its value is 0x00.
				 **/
				uint8_t reserved1;
				/** This byte is reserved for future use.
				 *  It should be ignored if its value is 0x00.
				 **/
				uint8_t reserved2;
			};
			uint8_t _data[5];
		};
		
		/**
		 * Request device informations.
		 * @param iInfo Retreived device informations.
		 * @return
		 *	- UNITIALIZED_ERROR if the module wasn't initialized.
		 * 	- OK if the memory pointer was succesfully set.
		 **/
		int8_t RequestDeviceInfo(DeviceInfo& iInfo)
		{
			int8_t ret;

			/* Check state */
			if(!_state)
			{
				return UNITIALIZED_ERROR;
			}

			_serial->print((uint8_t)VERSION);
			_read(iInfo._data, 5);
		
			return OK;
		}
}; // class uDRIVE_uSD_G1
/**
 * @}
 **/

}; // namespace _4Dsystems

#endif // _uDRIVE_uSD_G1_h_
