/*
 * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
 *
 * This file is part of PortaPack.
 *
 * 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, 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; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street,
 * Boston, MA 02110-1301, USA.
 */

#include "cpld_max5.hpp"

#include "jtag.hpp"

#include <cstdint>
#include <array>

namespace cpld {
namespace max5 {

/* Enter ISP:
 * Ensures that the I/O pins transition smoothly from user mode to ISP
 * mode.
 */
void CPLD::enter_isp() {
	/* Enter ISP */
	shift_ir(0x2cc); //(199);
	jtag.runtest_tck(18003);		// 1ms
}

void CPLD::exit_isp() {
	/* Exit ISP? Reset? */
	shift_ir(0x201); //166);
	jtag.runtest_tck(18003);		// 1ms
	shift_ir(0x3FF);
	jtag.runtest_tck(18000);		// 1ms
}

/* Sector erase:
 * Involves shifting in the instruction to erase the device and applying
 * an erase pulse or pulses. The erase pulse is automatically generated
 * internally by waiting in the run, test, or idle state for the
 * specified erase pulse time of 500 ms for the CFM block and 500 ms for
 * each sector of the user flash memory (UFM) block.
 */
void CPLD::bulk_erase() {
	erase_sector(0x0011);
	erase_sector(0x0001);
	erase_sector(0x0000);
}

bool CPLD::program(
	const std::array<uint16_t, 3328>& block_0,
	const std::array<uint16_t, 512>& block_1
) {
	bulk_erase();

	/* Program:
	 * involves shifting in the address, data, and program instruction and
	 * generating the program pulse to program the flash cells. The program
	 * pulse is automatically generated internally by waiting in the run/test/
	 * idle state for the specified program pulse time of 75 μs. This process
	 * is repeated for each address in the CFM and UFM blocks.
	 */
	program_block(0x0000, block_0);
	program_block(0x0001, block_1);

	const auto verify_ok = verify(block_0, block_1);

	if( verify_ok ) {
		/* Do "something". Not sure what, but it happens after verify. */
		/* Starts with a sequence the same as Program: Block 0. */
		/* Perhaps it is a write to tell the CPLD that the bitstream
		 * verified OK, and it's OK to load and execute? And despite only
		 * one bit changing, a write must be a multiple of a particular
		 * length (64 bits)? */
		sector_select(0x0000);
		shift_ir(0x2F4);			// Program
		jtag.runtest_tck(93);		// 5 us

		/* TODO: Use data from cpld_block_0, with appropriate bit(s) changed */
		/* Perhaps this is the "ISP_DONE" bit? */
		jtag.shift_dr(16, block_0[0] & 0xfbff);
		jtag.runtest_tck(1800);		// 100us
		jtag.shift_dr(16, block_0[1]);
		jtag.runtest_tck(1800);		// 100us
		jtag.shift_dr(16, block_0[2]);
		jtag.runtest_tck(1800);		// 100us
		jtag.shift_dr(16, block_0[3]);
		jtag.runtest_tck(1800);		// 100us
	}

	return verify_ok;
}

bool CPLD::verify(
	const std::array<uint16_t, 3328>& block_0,
	const std::array<uint16_t, 512>& block_1
) {
	/* Verify */
	const auto block_0_success = verify_block(0x0000, block_0);
	const auto block_1_success = verify_block(0x0001, block_1);
	return block_0_success && block_1_success;
}

void CPLD::sector_select(const uint16_t id) {
	shift_ir(0x203);			// Sector select
	jtag.runtest_tck(93);		// 5us
	jtag.shift_dr(13, id);		// Sector ID
}

bool CPLD::idcode_ok() {
	shift_ir(Instruction::IDCODE);
	const auto idcode = jtag.shift_dr(32, 0);
	return (idcode == IDCODE);
}

std::array<uint16_t, 5> CPLD::read_silicon_id() {
	sector_select(0x0089);
	shift_ir(0x205);
	jtag.runtest_tck(93);		// 5us

	std::array<uint16_t, 5> silicon_id;
	silicon_id[0] = jtag.shift_dr(16, 0xffff);
	silicon_id[1] = jtag.shift_dr(16, 0xffff);
	silicon_id[2] = jtag.shift_dr(16, 0xffff);
	silicon_id[3] = jtag.shift_dr(16, 0xffff);
	silicon_id[4] = jtag.shift_dr(16, 0xffff);
	return silicon_id;
}

/* Check ID:
 * The silicon ID is checked before any Program or Verify process. The
 * time required to read this silicon ID is relatively small compared to
 * the overall programming time.
 */
bool CPLD::silicon_id_ok() {
	const auto silicon_id = read_silicon_id();

	return (
		(silicon_id[0] == 0x8232) &&
		(silicon_id[1] == 0x2aa2) &&
		(silicon_id[2] == 0x4a82) &&
		(silicon_id[3] == 0x8c0c) &&
		(silicon_id[4] == 0x0000)
	);
}

void CPLD::erase_sector(const uint16_t id) {
	sector_select(id);
	shift_ir(0x2F2);			// Erase pulse
	jtag.runtest_tck(9000003);	// 500ms
}

void CPLD::program_block(
	const uint16_t id,
	const uint16_t* const data,
	const size_t count
) {
	sector_select(id);
	shift_ir(0x2F4);			// Program
	jtag.runtest_tck(93);		// 5us

	for(size_t i=0; i<count; i++) {
		jtag.shift_dr(16, data[i]);
		jtag.runtest_tck(1800);
	}
}

bool CPLD::verify_block(
	const uint16_t id,
	const uint16_t* const data,
	const size_t count
) {
	sector_select(id);
	shift_ir(0x205);			// Read
	jtag.runtest_tck(93);		// 5us

	bool success = true;
	for(size_t i=0; i<count; i++) {
		const auto from_device = jtag.shift_dr(16, 0xffff);
		if( (id == 0) && (i == 0) ) {
			// Account for bit that indicates bitstream is valid.
			if( (from_device & 0xfbff) != (data[i] & 0xfbff) ) {
				success = false;
			}
		} else {
			if( from_device != data[i] ) {
				success = false;
			}
		}
	}
	return success;
}

bool CPLD::is_blank_block(const uint16_t id, const size_t count) {
	sector_select(id);
	shift_ir(0x205);			// Read
	jtag.runtest_tck(93);		// 5us

	bool success = true;
	for(size_t i=0; i<count; i++) {
		const auto from_device = jtag.shift_dr(16, 0xffff);
		if( from_device != 0xffff ) {
			success = false;
		}
	}
	return success;
}

bool CPLD::is_blank() {
	const auto block_0_blank = is_blank_block(0x0000, 3328);
	const auto block_1_blank = is_blank_block(0x0001, 512);
	return block_0_blank && block_1_blank;
}

} /* namespace max5 */
} /* namespace cpld */