/*
 * m4cartridge.cpp
 *
 *  Created on: Nov 5, 2018
 *      Author: flyinghead
 *  Plagiarized from mame (mame/src/mame/machine/naomim4.cpp)
 *  // license:BSD-3-Clause
 *  // copyright-holders:Olivier Galibert,Andreas Naive
 */

#include "m4cartridge.h"
#include "serialize.h"


// Decoder for M4-type NAOMI cart encryption

// In hardware, the decryption is managed by the XC3S50 Xilinx Spartan FPGA (IC2)
// and the annexed PIC16C621A PIC MCU (IC3).
// - The FPGA control the clock line of the security PIC.
// - The protocol between the FPGA and the MCU is nibble-based, though it hasn't been RE for now.
// - The decryption algorithm is clearly nibble-based too.

// The decryption algorithm itself implements a stream cipher built on top of a 16-bits block cipher.
// The underlying block-cipher is a SP-network of 2 rounds (both identical in structure). In every
// round, the substitution phase is done using 4 fixed 4-to-4 sboxes acting on every nibble. The permutation
// phase is indeed a nibble-based linear combination.
// With that block cipher, a stream cipher is constructed by feeding the output result of the 1st round
// of a certain 16-bits block as a whitening value for the next block. The cart dependent data used by
// the algorithm is a 32-bits key stored in the PIC16C621A. The hardware auto-reset the feed value
// to the cart-based IV every 16 blocks (32 bytes); that reset is not address-based, but index-based.

const u8 k_sboxes[4][16] = {
	{9,8,2,11,1,14,5,15,12,6,0,3,7,13,10,4},
	{2,10,0,15,14,1,11,3,7,12,13,8,4,9,5,6},
	{4,11,3,8,7,2,15,13,1,5,14,9,6,12,0,10},
	{1,13,8,2,0,5,6,14,4,11,15,10,12,3,7,9}
};

// from S29GL512N datasheet
static u8 cfidata[] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x51,0x00,0x52,0x00,0x59,0x00,0x02,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x27,0x00,0x36,0x00,0x00,0x00,0x00,0x00,0x07,0x00,
0x07,0x00,0x0a,0x00,0x00,0x00,0x01,0x00,0x05,0x00,0x04,0x00,0x00,0x00,0x1a,0x00,0x02,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x01,0x00,0xff,0x00,0x01,0x00,0x00,0x00,
0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x50,0x00,0x52,0x00,0x49,0x00,0x31,0x00,0x33,0x00,0x10,0x00,0x02,0x00,0x01,0x00,0x00,0x00,0x08,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0xb5,0x00,0xc5,0x00,0x04,0x00,
0x01,0x00
};

void M4Cartridge::device_start()
{
	if (m4id == 0)
	{
		INFO_LOG(NAOMI, "Warning: M4 ID not provided\n");
		m4id = 0x5504;
	}

	if (m_key_data != nullptr)
	{
		subkey1 = (m_key_data[0x5e2] << 8) | m_key_data[0x5e0];
		subkey2 = (m_key_data[0x5e6] << 8) | m_key_data[0x5e4];
	}
	else
	{
		WARN_LOG(NAOMI, "Missing M4 key");
	}

	enc_init();
}

void M4Cartridge::enc_init()
{
	for (int round_input = 0; round_input < 0x10000; round_input++)
	{
		u8 input_nibble[4];
		u8 output_nibble[4];

		for (int nibble_idx = 0; nibble_idx < 4; ++nibble_idx) {
			input_nibble[nibble_idx] = (round_input >> (nibble_idx*4)) & 0xf;
			output_nibble[nibble_idx] = 0;
		}

		u8 aux_nibble = input_nibble[3];
		for (int nibble_idx = 0; nibble_idx < 4; ++nibble_idx) { // 4 s-boxes per round
			aux_nibble ^= k_sboxes[nibble_idx][input_nibble[nibble_idx]];
			for (int i = 0; i < 4; ++i)  // diffusion of the bits
				output_nibble[(nibble_idx - i) & 3] |= aux_nibble & (1 << i);
		}

		u16 result = 0;
		for (int nibble_idx = 0; nibble_idx < 4; ++nibble_idx)
			result |= (output_nibble[nibble_idx] << (4 * nibble_idx));

		one_round[round_input] = result;
	}
}

void M4Cartridge::device_reset()
{
	rom_cur_address = 0;
	buffer_actual_size = 0;
	encryption = false;
	cfi_mode = false;
	counter = 0;
	iv = 0;
	xfer_ready = false;
}

void M4Cartridge::DmaOffsetChanged(u32 dma_offset)
{
	xfer_ready = false;
}

void M4Cartridge::PioOffsetChanged(u32 pio_offset)
{
	encryption = pio_offset & 0x40000000;
	xfer_ready = false;
}


bool M4Cartridge::Read(u32 offset, u32 size, void *dst) {
	if (cfi_mode)
	{
		u32 fpr_num = m4id & 0x7f;

		if (((offset >> 26) & 0x07) < fpr_num) {
			*(u16 *)dst = *(u16 *)&cfidata[offset & 0xffff];
			return true;
		}
	}
	if (!xfer_ready)
	{
		rom_cur_address = RomPioOffset & 0x1ffffffe;
		if (encryption)
		{
			//printf("M4 CRYPT m4id %x skey1 %x skey2 %x RomPioOffset %08x\n", m4id, subkey1, subkey2, RomPioOffset);
			enc_reset();
			enc_fill();
		}
		xfer_ready = true;
	}
	if (encryption)
	{
		switch (size)
		{
		case 2:
			*(u16 *)dst = *(u16 *)buffer;
			break;
		case 4:
			*(u32 *)dst = *(u32 *)buffer;
			break;
		}
		if (RomPioAutoIncrement)
			AdvancePtr(size);

		return true;
	}
	else
		return NaomiCartridge::Read(offset & 0x1ffffffe, size, dst);
}

void *M4Cartridge::GetDmaPtr(u32 &size)
{
	static u8 retzero[2] = { 0, 0 };

	if (cfi_mode) {
		u32 fpr_num = m4id & 0x7f;

		if (((DmaOffset >> 26) & 0x07) < fpr_num) {
			size = std::min(size, 2u);
			return &cfidata[DmaOffset & 0xffff];
		}
	}

	if (!xfer_ready)
	{
		rom_cur_address = DmaOffset & 0x1ffffffe;
		if (encryption)
		{
			//printf("M4 CRYPT m4id %x skey1 %x skey2 %x DmaOffset %08x\n", m4id, subkey1, subkey2, DmaOffset);
			enc_reset();
			enc_fill();
		}
		xfer_ready = true;
	}
	if (encryption)
	{
		size = std::min(size, buffer_actual_size);
		return buffer;
	}
	else
	{
		if ((DmaOffset & 0x1ffffffe) < RomSize)
		{
			size = std::min(size, RomSize - (DmaOffset & 0x1ffffffe));
			return RomPtr + (DmaOffset & 0x1ffffffe);
		}
		else
		{
			size = 2;
			return retzero;
		}
	}
}

void M4Cartridge::AdvancePtr(u32 size)
{
	if (encryption)
	{
		if (size < buffer_actual_size) {
			memmove(buffer, buffer + size, buffer_actual_size - size);
			buffer_actual_size -= size;
		}
		else {
			buffer_actual_size = 0;
		}
		enc_fill();
	}
	else {
		NaomiCartridge::AdvancePtr(size);
	}
}

void M4Cartridge::enc_reset()
{
	buffer_actual_size = 0;
	iv = 0;
	counter = 0;
}

u16 M4Cartridge::decrypt_one_round(u16 word, u16 subkey)
{
	return one_round[word ^ subkey] ^ subkey ;
}

u16 M4Cartridge::decrypt(u16 enc)
{
	u16 dec = iv;
	iv = decrypt_one_round(enc ^ iv, subkey1);
	dec ^= decrypt_one_round(iv, subkey2);
	counter++;
	if (counter == 16) {
		counter = 0;
		iv = 0;
	}

	return dec;
}

void M4Cartridge::enc_fill()
{
	const u8 *base = RomPtr + rom_cur_address;
	while (buffer_actual_size < sizeof(buffer))
	{
		u16 dec = decrypt(base[0] | (base[1] << 8));

		buffer[buffer_actual_size++] = dec;
		buffer[buffer_actual_size++] = dec >> 8;

		base += 2;
		rom_cur_address += 2;
	}
//	printf("Decrypted M4 data:\n");
//	for (int i = 0; i < buffer_actual_size; i++)
//	{
//		printf("%c ", buffer[i]);
//		if ((i + 1) % 16 == 0)
//			printf("\n");
//	}
//	printf("\n");


}

bool M4Cartridge::Write(u32 offset, u32 size, u32 data)
{
	if (((offset&0xffff) == 0x00aa) && (data == 0x0098))
		cfi_mode = true;
	if (((offset&0xffff) == 0x0000) && (data == 0x00f0))
		cfi_mode = false;
	return true;	// ?
}

M4Cartridge::~M4Cartridge()
{
	free(m_key_data);
}

bool M4Cartridge::GetBootId(RomBootID *bootId)
{
	if (RomSize < sizeof(RomBootID))
		return false;
	RomBootID *pBootId = (RomBootID *)RomPtr;
	if (memcmp(pBootId->boardName, "NAOMI", 5)
			&& memcmp(pBootId->boardName, "Naomi2", 6)
			&& memcmp(pBootId->boardName, "SystemSP", 8))
	{
		rom_cur_address = 0;
		enc_reset();
		enc_fill();
		pBootId = (RomBootID *)buffer;
	}
	memcpy(bootId, pBootId, sizeof(RomBootID));

	return true;
}

void M4Cartridge::Serialize(Serializer& ser) const
{
	ser << buffer;
	ser << rom_cur_address;
	ser << buffer_actual_size;
	ser << iv;
	ser << counter;
	ser << encryption;
	ser << cfi_mode;
	ser << xfer_ready;

	NaomiCartridge::Serialize(ser);
}

void M4Cartridge::Deserialize(Deserializer& deser)
{
	deser >> buffer;
	deser.skip(32768 - sizeof(buffer), Deserializer::V52);
	deser >> rom_cur_address;
	deser >> buffer_actual_size;
	buffer_actual_size = std::min<u32>(buffer_actual_size, sizeof(buffer));
	deser >> iv;
	deser >> counter;
	deser >> encryption;
	deser >> cfi_mode;
	deser >> xfer_ready;

	NaomiCartridge::Deserialize(deser);
}
