#pragma once

#include "binary_data.hpp"
#include "ines_header.hpp"

#include "chocolat/system/stream_measure.hpp"
#include "chocolat/system/file_path.hpp"
#include "chocolat/system/file_not_found_exception.hpp"

#include "chocolat/string/number_format.hpp"

#include <fstream>
#include <boost/shared_ptr.hpp>

class ByteVector
{
private:
	ByteVector(){};
public:
	typedef boost::shared_ptr<std::vector<Byte> > shared_ptr;
};
/** This image serve byte code for query of address.*/
class MemoryImage
{
public:
	typedef boost::shared_ptr<MemoryImage> shared_ptr;
private:
	MemoryAddress m_start_address;
	ByteVector::shared_ptr m_vector;
public:
	ByteVector::shared_ptr getVector()const
	{
		return m_vector;
	}
	bool includes(MemoryAddress address) const
	{
		bool result = startAddress() <= address && address < endAddress();
		return result;
	}
	
	Byte at(MemoryAddress address)const
	{
		return (*m_vector)[address-m_start_address];
	}
	void push(Byte byte)
	{
		m_vector->push_back(byte);
	}
	Byte operator[](unsigned index)const
	{
		return (*m_vector)[index];
	}
	MemoryImage& operator<<(Byte byte)
	{
		push(byte);
		return *this;
	}
	Word wordLowHigh(unsigned low, unsigned high)const
	{
		return low % 0x100 + (high % 0x100) * 0x100;
	}
	Word getWordAt(MemoryAddress address)const
	{
		return wordLowHigh(at(address), at(address+1));
	}
	unsigned size()const
	{
		return m_vector->size();
	}
	bool empty()const
	{
		return m_vector->empty();
	}
	MemoryImage(MemoryAddress start_address)
			:m_start_address(start_address),
			 m_vector(new std::vector<unsigned char>)
	{
		
	}
	MemoryAddress startAddress()const
	{
		return m_start_address;
	}
	/*this is invalid address like vector.end() */
	MemoryAddress endAddress()const
	{
		return m_start_address + m_vector->size();
	}
};
/** This object separate byte code by ines header infomation.
 *  Other component can see byte code by memory address.
 */
class NesProgramImage
{
private:
	MemorySize m_file_size, m_rest_size;
	MemoryAddress m_start_address;
	
	/** these images are products of loader.*/
	MemoryImage::shared_ptr m_PRG_ROM;
	MemoryImage::shared_ptr m_CHR_ROM;
	boost::shared_ptr<INesHeader> m_header;
public:
	boost::shared_ptr<INesHeader> getHeader()
	{
		return m_header;
	}
	boost::shared_ptr<MemoryImage> getPRG_ROM()
	{
		return m_PRG_ROM;
	}
	boost::shared_ptr<MemoryImage> getCHR_ROM()
	{
		return m_CHR_ROM;
	}
	/** load from file
	 * default is calculated automatically.
	 */
	void loadFile(FilePath file_name, MemoryAddress start_address=0x10000)
	{
		m_start_address = start_address;
		if (!file_name.exists()){
			throw FileNotFoundException(file_name);
		}
		std::ifstream fin(file_name.c_str(),
						  std::ifstream::in | std::ifstream::binary);
		loadStream(fin);
	}
	void loadHeader(std::istream& is)
	{
		m_header.reset(new INesHeader);
		m_header->read(is);
		if (m_header->bad()){
			throw InvalidNesFormatException("iNes header missing");
		}
		if (m_start_address > 0xFFFF) {
			m_start_address = 0x10000 - m_header->sizeOfPRG();
		}
	}
	void loadPRG_ROM(std::istream& is)
	{
		m_PRG_ROM.reset(new MemoryImage(m_start_address));
		for (unsigned i=0; i<m_header->sizeOfPRG();++i) {
			if (is.eof()){
				throw InvalidNesFormatException("imcomplete PRG-ROM");
			}
			m_PRG_ROM->push(is.get());
		}
	}
	void loadCHR_ROM(std::istream& is)
	{
		m_CHR_ROM.reset(new MemoryImage(0));
		for (unsigned i=0; i<m_header->sizeOfCHR();++i){
			if (is.eof()){
				throw InvalidNesFormatException("imcomplete CHR-ROM");
			}
			m_CHR_ROM->push(is.get());
		}
	}
	/** load from stream */
	void loadStream(std::istream& is)
	{
		StreamMeasure measure(is);
		m_file_size = measure.size();
		loadHeader(is);
		loadPRG_ROM(is);
		loadCHR_ROM(is);
		m_rest_size = measure.rest();
	}
	void displayFileInfomation(std::ostream& os)const
	{
		os << "File size 0x" << m_file_size<<std::endl;
		m_header->dump(os);
		HexadecimalFormat prg_size(m_PRG_ROM->size());
		HexadecimalFormat chr_size(m_CHR_ROM->size());
		os << "PRG-ROM loaded " << prg_size.width(6) << std::endl;
		os << "CHR-ROM loaded " << chr_size.width(6) << std::endl;
		os << "rest "<< m_rest_size <<std::endl;
		os << "start address " << HexadecimalFormat(m_start_address)
		   << std::endl;
	}
	/** This query method is return the address of NMI interrupt vector.*/
	MemoryAddress getNMIAddress()const
	{
		return m_PRG_ROM->endAddress() - 6;
	}
	/** This query method is return the value of NMI interrupt vector.*/
	MemoryAddress getNMIHandler()const
	{
		// TODO It is changed when FDS mapper
		return m_PRG_ROM->getWordAt(getNMIAddress());
	}
	/** This query method is return the address of Reset interrupt vector.*/
	MemoryAddress getResetAddress()const
	{
		return m_PRG_ROM->endAddress() - 4;
	}
	/** This query method is return the value of Reset interrupt vector.*/
	MemoryAddress getResetHandler()const
	{
		// TODO It is changed when FDS mapper
		return m_PRG_ROM->getWordAt(getResetAddress());
	}
	/** This query method is return the address of IRQ interrupt vector.*/
	MemoryAddress getIRQAddress()const
	{
		return m_PRG_ROM->endAddress() - 2;
	}
	/** This query method is return the value of IRQ interrupt vector.*/
	MemoryAddress getIRQHandler()const
	{
		// TODO It is changed when FDS mapper
		return m_PRG_ROM->getWordAt(getIRQAddress());
	}
	/** dump loaded file infomation. */
	void dump(std::ostream& os)const
	{
		displayFileInfomation(os);
	}
};
