/*  *************************************************************************

    DM9000A/B/BI (8-bit mode) driver for AVR CPUs
    Copyright (C) 2009  Philipp Kerling

    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 3 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, see <http://www.gnu.org/licenses/>.

   ************************************************************************* */

// #define HAVE_LCD

#ifdef HAVE_LCD
# include "lcd.h"
#endif
#include "dm9000.h"
//#include "global.h"

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

#define _IOR_ON()   { DM9000_IOR_PORT &= ~(1 << DM9000_IOR_PIN); }
#define _IOR_OFF()  { DM9000_IOR_PORT |=  (1 << DM9000_IOR_PIN); }
#define _IOW_ON()   { DM9000_IOW_PORT &= ~(1 << DM9000_IOW_PIN); }
#define _IOW_OFF()  { DM9000_IOW_PORT |=  (1 << DM9000_IOW_PIN); }
#define _REG_DATA() { DM9000_CMD_PORT |=  (1 << DM9000_CMD_PIN); }
#define _REG_INDEX(){ DM9000_CMD_PORT &= ~(1 << DM9000_CMD_PIN); }
#define _CS_ON()    { DM9000_CS_PORT  &= ~(1 << DM9000_CS_PIN); }
#define _CS_OFF()   { DM9000_CS_PORT  |=  (1 << DM9000_CS_PIN); }

#define _DO_DATA_IN() { DM9000_DDR_REG = 0; DM9000_DATA_OUT_PORT = 0xFF; }
#define _DATA_OUT(z)  { DM9000_DATA_OUT_PORT = z; DM9000_DDR_REG = 0xFF; }

#define _DO_CS() { _CS_ON(); _CS_OFF(); }

#define _SET_REGISTER(y) { _REG_INDEX(); _IOW_ON(); _DATA_OUT(y); _DO_CS(); _IOW_OFF(); }

uint8_t ethernet_read_register(uint8_t reg)
{
	uint8_t regval = 0;
	_SET_REGISTER(reg);
	// DM9000 IOW-to-IOR-delay should be satisfied by the two following commands already, so no extra NOP

	_DO_DATA_IN();
	_CS_ON();
	_REG_DATA();
	_IOR_ON();
	// IMPORTANT! Let values propagate to register (sync delay)
	asm volatile ("nop");
	// Get value
	regval = DM9000_DATA_IN_PORT;
	_IOR_OFF();
	_CS_OFF();

	return regval;
}

void ethernet_write_register(DM9000_REG reg, uint8_t val)
{
	_SET_REGISTER(reg);

	_REG_DATA();
	_IOW_ON();
	// Set value
	_DATA_OUT(val);
	_DO_CS();
	_IOW_OFF();
}

uint16_t ethernet_read_phy_register(DM9000_PHYREG reg)
{
	// FIXME: throw error
	if (reg > 0b00111111)
		return 0;
	// Write register address
	ethernet_write_register(DM9_EPAR, 0b01000000 + reg);
	// PHY Register Read Command
	ethernet_write_register(DM9_EPCR, 0b00001100);

	// Wait for completion
	uint8_t watchdog = 0;
	uint8_t epcr;
	do
	{
		_delay_us(1.0);
		epcr = ethernet_read_register(DM9_EPCR);
		watchdog++;
	} while (watchdog < 200 && (epcr & (0b00000001)));
	_delay_us(5.0);

	// Clear command
	ethernet_write_register(DM9_EPCR, 0b00001000);

	// Get data
	uint16_t retval = ethernet_read_register(DM9_EPDRL) + (ethernet_read_register(DM9_EPDRH) << 8);
	return retval;
}

void ethernet_write_phy_register(DM9000_PHYREG reg, uint16_t val)
{
	// FIXME: throw error
	if (reg > 0b00111111)
		return;
	// Write register address
	ethernet_write_register(DM9_EPAR, 0b01000000 + reg);
	// Write data
	ethernet_write_register(DM9_EPDRL, (uint8_t) (val));
	ethernet_write_register(DM9_EPDRH, (uint8_t) (val >> 8));
	// PHY Register Write Command
	ethernet_write_register(DM9_EPCR, 0b00001010);

	// Wait for completion
	uint8_t watchdog = 0;
	uint8_t epcr;
	do
	{
		_delay_us(1.0);
		epcr = ethernet_read_register(DM9_EPCR);
		watchdog++;
	} while (watchdog < 200 && (epcr & (0b00000001)));
	_delay_us(5.0);

	// Clear command
	ethernet_write_register(DM9_EPCR, 0b00001000);
}

static void _ethernet_reset(void)
{
	ethernet_write_register(DM9_NCR, 0b00000001);

	uint8_t watchdog = 0;
	uint8_t ncr = 0;
	do
	{
		_delay_ms(1.0);
		ncr = ethernet_read_register(DM9_NCR);
		watchdog++;
	} while (watchdog < 200 && (ncr & 0b00000001));

#ifdef HAVE_LCD
	if (watchdog >= 200)
	{
		cli();
		lcd_return_home();
		lcd_write_string("!! DM9000 is not  !!");
		lcd_set_dd_ram_address(LCD_DD_RAM_SECOND_ROW);
		lcd_write_string("!!   responding   !!");
		for (;;)
			asm volatile ("nop");
	}
#endif
}

static void _ethernet_reset_phy(void)
{
	ethernet_write_phy_register(DM9PHY_BMCR, 0b1000000000000000);

	uint8_t watchdog = 0;
	uint16_t bmcr = 0;
	do
	{
		_delay_us(10.0);
		bmcr = ethernet_read_phy_register(DM9PHY_BMCR);
		watchdog++;
	} while (watchdog < 200 && (bmcr & 0b1000000000000000));

#ifdef HAVE_LCD
	if (watchdog >= 200)
	{
		cli();
		lcd_return_home();
		lcd_write_string("DM9000 PHY is not");
		lcd_set_dd_ram_address(LCD_DD_RAM_SECOND_ROW);
		lcd_write_string("answering");
		for (;;)
			asm volatile ("nop");
	}
#endif
}

void ethernet_set_mac(uint8_t m1, uint8_t m2, uint8_t m3, uint8_t m4, uint8_t m5, uint8_t m6)
{
	ethernet_write_register(DM9_PAR1, m1);
	ethernet_write_register(DM9_PAR2, m2);
	ethernet_write_register(DM9_PAR3, m3);
	ethernet_write_register(DM9_PAR4, m4);
	ethernet_write_register(DM9_PAR5, m5);
	ethernet_write_register(DM9_PAR6, m6);
}

void ethernet_activate_rx(void)
{
	uint8_t rcr = ethernet_read_register(DM9_RCR);
	ethernet_write_register(DM9_RCR, rcr | 0b00000001);
}

void ethernet_deactivate_rx(void)
{
	uint8_t rcr = ethernet_read_register(DM9_RCR);
	ethernet_write_register(DM9_RCR, rcr & ~0b00000001);
}

void ethernet_init_chip(void)
{
	// Power up PHY: Clear PHYPD
	ethernet_write_register(DM9_GPR, 0b00000000);
	// Wait for PHY
	_delay_ms(60.0);

	_ethernet_reset();
	_ethernet_reset_phy();

	// Set normal mode: Clear LBK
	ethernet_write_register(DM9_NCR, 0b00000000);

	// Special mode
	ethernet_write_register(DM9_SMCR, 0);

#ifdef HAVE_LCD
	// Query IO mode
	uint8_t isr = ethernet_read_register(DM9_ISR);
	if (!(isr & (1 << DM9ISR_IOMODE)) || (isr & (1 << DM9ISR_RESERVED)))
	{
		lcd_display_clear();
		lcd_return_home();
		cli();
		lcd_write_string("DM9000 IO width is");
		lcd_set_dd_ram_address(LCD_DD_RAM_SECOND_ROW);
		lcd_write_string("not 8 bit (");
		if (isr & (1 << DM9ISR_IOMODE))
			lcd_write_char('1');
		else
			lcd_write_char('0');
		if (isr & (1 << DM9ISR_RESERVED))
			lcd_write_char('1');
		else
			lcd_write_char('0');
		lcd_write_char(')');
		for (;;) asm volatile ("nop");
	}
#endif

	// Activate pointer wrap: Set PAR
	// Activate interrupts: packet received, packet transmitted, receive overflow, receive overflow counter overflow, transmit under-run
	ethernet_write_register(DM9_IMR, 0b10011111);

	ethernet_set_mac(NET_MAC_1, NET_MAC_2, NET_MAC_3, NET_MAC_4, NET_MAC_5, NET_MAC_6);

	// Enable flow control
	ethernet_write_register(DM9_FCR, 0b00101001);

	// Write hash table
	ethernet_write_register(DM9_MAR1, 0);
	ethernet_write_register(DM9_MAR2, 0);
	ethernet_write_register(DM9_MAR3, 0);
	ethernet_write_register(DM9_MAR4, 0);
	ethernet_write_register(DM9_MAR5, 0);
	ethernet_write_register(DM9_MAR6, 0);
	ethernet_write_register(DM9_MAR7, 0);
	// Receive broadcast
	ethernet_write_register(DM9_MAR8, 0x80);

	// Activate UDP and IP checksum generation
	ethernet_write_register(DM9_TCSCR, 0b00000101);

	// Activate receive checksum checking
	ethernet_write_register(DM9_RCSCSR, 0b00000011);
	// Discard packet on CRC error or length > 1522 bytes
	ethernet_write_register(DM9_RCR, 0b00110000);

	// Back pressure
	ethernet_write_register(DM9_BPTR, 0b00100101);
	ethernet_write_register(DM9_FCTR, 0b00100100);

	// Activate re-transmission at late collision
	ethernet_write_register(DM9_TCR2, 0b01000000);

	// Clear flags: TX1END, TX2END, WAKEST
	ethernet_write_register(DM9_NSR, 0b00101100);
	// PR, PT, ROS, ROO, UDRUN, LNKCHNG
	ethernet_write_register(DM9_ISR, 0b00111111);

	// Setup PHY: Advertise flow control, 100BASE-TX/10BASE-T Full/Half duplex
	                                              //10111100001
	ethernet_write_phy_register(DM9PHY_ANAR, 0b0000010111100001);
	// Restart auto-negotiation
//	                                             11001000000000
	ethernet_write_phy_register(DM9PHY_BMCR, 0b0011001100000000);
}

uint8_t ethernet_is_packet_ready(void)
{
	// Take a peek
	// Dummy read
	_SET_REGISTER(DM9_MRCMDX);
	_REG_DATA();
	_DO_DATA_IN();
	_CS_ON();
	_IOR_ON();
	asm volatile ("nop");
	_IOR_OFF();
	_CS_OFF();

	_CS_ON();
	_IOR_ON();
	asm volatile ("nop");
	uint8_t b = DM9000_DATA_IN_PORT;
	_IOR_OFF();
	_CS_OFF();

	if ((b & 0b00000011) == 0b01)
		return 1;

	return 0;
}

void ethernet_rx_sync(uint8_t* data, const uint16_t len)
{
	// Memory Data Read Command with Address Increment Register
	_SET_REGISTER(DM9_MRCMD);

	_REG_DATA();
	_DO_DATA_IN();

	uint16_t i;
	for (i = 0; i < len; i++)
	{
		// Read DATA port
		_CS_ON();
		_IOR_ON();
		asm volatile ("nop");
		*data++ = DM9000_DATA_IN_PORT;
		_IOR_OFF();
		_CS_OFF();
	}
}

void ethernet_dump_rx_sync(const uint16_t len)
{
	uint16_t mrr = (ethernet_read_register(DM9_MRRH) << 8) + (ethernet_read_register(DM9_MRRL));
	mrr += len;
	// !!!!! Wrap around?
	// Have to receive 1 byte to check MAC header
	ethernet_write_register(DM9_MRRL, (uint8_t) mrr);
	ethernet_write_register(DM9_MRRH, (uint8_t) (mrr >> 8));
}


uint16_t _g_tx_len_counter = 0;

void ethernet_buffer_tx_sync(const uint8_t* const data, const uint16_t len)
{
	// Memory Data Write Command with Address Increment Register
	_SET_REGISTER(DM9_MWCMD);
	_REG_DATA();

	uint16_t i = 0;
	const uint8_t* data_ptr = data;
	for (i = 0; i < len; i++)
	{
		// Set DATA port
		_IOW_ON();
		_DATA_OUT(*data_ptr++);
		_DO_CS();
		_IOW_OFF();
	}

	_g_tx_len_counter += len;
}

void ethernet_tx_async(void)
{
	uint8_t watchdog = 0;
	uint8_t tcr = ethernet_read_register(DM9_TCR);
	while (watchdog < 200 && (tcr & 0b00000001))
	{
		// FIXME: If this fails, try to use NSR and TX1END/TX2END instead
		tcr = ethernet_read_register(DM9_TCR);
		watchdog++;
		_delay_us(1.0);
	}
	if (watchdog >= 200)
	{
		watchdog = 0;
		do
		{
			// Wait for 5s
			_delay_ms(25.0);
			// FIXME: If this fails, try to use NSR and TX1END/TX2END instead
			tcr = ethernet_read_register(DM9_TCR);
			watchdog++;
		} while (watchdog < 200 && (tcr & 0b00000001));
#ifdef HAVE_LCD
		if (watchdog >= 200)
		{
			cli();
			lcd_return_home();
			lcd_write_string("DM9000 TX timed");
			lcd_set_dd_ram_address(LCD_DD_RAM_SECOND_ROW);
			lcd_write_string("out");
			for (;;)
				asm volatile ("nop");
		}
#endif
	}

	// Clear flags
//	ethernet_write_register(DM9_NSR, (0 << 2) | (0 << 3));

	// Set packet length
	ethernet_write_register(DM9_TXPLH, (uint8_t) (_g_tx_len_counter >> 8));
	ethernet_write_register(DM9_TXPLL, (uint8_t) _g_tx_len_counter);

	_g_tx_len_counter = 0;

	// Activate transmission: Set TXREQ
	ethernet_write_register(DM9_TCR, 0b00000001);
}