/*
 *  RTEMS driver for Minimac2 ethernet IP-core of Milkymist SoC
 *
 *  The license and distribution terms for this file may be
 *  found in the file LICENSE in this distribution or at
 *  http://www.rtems.org/license/LICENSE.
 *
 *  COPYRIGHT (c) Yann Sionneau <yann.sionneau@telecom-sudparis.eu> (GSoC 2010)
 *  Telecom SudParis, France
 *  Copyright (C) 2011 Sebastien Bourdeauducq
 */

#include <machine/rtems-bsd-kernel-space.h>
#define RTEMS_STATUS_CHECKS_USE_PRINTK

#include <bsp.h>
#include <bsp/irq-generic.h>
#include <stdio.h>
#include <string.h>
#include <rtems/bspIo.h>
#include <rtems/rtems_bsdnet.h>
#include <rtems/status-checks.h>
#include <sys/param.h>
#include <sys/mbuf.h>
#include <sys/socket.h>
#include <sys/sockio.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <netinet/in.h>
#include <netinet/if_ether.h>
#include <rtems.h>
#include "bspopts.h"
#include "../include/system_conf.h"
#include "network.h"

#define CTS_EVENT             RTEMS_EVENT_1
#define RX_EVENT              RTEMS_EVENT_1
#define START_TRANSMIT_EVENT  RTEMS_EVENT_2

static struct arpcom arpcom;
static rtems_id rx_daemon_id;
static rtems_id tx_daemon_id;

static void minimac_init(void *arg);
static int minimac_ioctl(struct ifnet *ifp, ioctl_command_t command,
  caddr_t data);
static void minimac_start(struct ifnet *ifp);

static void rx_daemon(void *arg);
static void tx_daemon(void *arg);
static rtems_isr rx_interrupt_handler(rtems_vector_number vector);
static rtems_isr tx_interrupt_handler(rtems_vector_number vector);

static bool validate_mac(const char *m)
{
  int i;
  
  for(i=0;i<6;i++)
    if((m[i] != 0x00) && (m[i] != 0xff))
      return true;
  return false;
}

static const char *get_mac_address(void)
{
  const char *flash_mac = (const char *)FLASH_OFFSET_MAC_ADDRESS;
  static const char fallback_mac[6] = { 0x10, 0xe2, 0xd5, 0x00, 0x00, 0x00 };
  
  if(validate_mac(flash_mac))
    return flash_mac;
  else {
    printk("Warning: using fallback MAC address\n");
    return fallback_mac;
  }
}

int rtems_minimac_driver_attach(struct rtems_bsdnet_ifconfig *config,
  int attaching)
{
  struct ifnet *ifp;
  rtems_isr_entry dummy;
  int i;
  static int registered;
  uint8_t *tx_buffer = (uint8_t *)MINIMAC_TX_BASE;

  if(!attaching) {
    printk("Minimac driver cannot be detached.\n");
    return 0;
  }

  ifp = &(arpcom.ac_if);

  if(registered) {
    printk("Minimac driver already in use.\n");
    return 0;
  }
  registered = 1;

  memcpy(arpcom.ac_enaddr, get_mac_address(), 6);
  ifp->if_mtu = ETHERMTU;
  ifp->if_unit = 0;
  ifp->if_name = "minimac";
  ifp->if_init = minimac_init;
  ifp->if_ioctl = minimac_ioctl;
  ifp->if_start = minimac_start;
  ifp->if_output = ether_output;
  ifp->if_flags = IFF_BROADCAST | IFF_SIMPLEX;
  ifp->if_snd.ifq_maxlen = ifqmaxlen;

  if_attach(ifp);
  ether_ifattach(ifp);

  rx_daemon_id = rtems_bsdnet_newproc("mrxd", 4096, rx_daemon, NULL);
  tx_daemon_id = rtems_bsdnet_newproc("mtxd", 4096, tx_daemon, NULL);
  rtems_interrupt_catch(rx_interrupt_handler, MM_IRQ_ETHRX, &dummy);
  rtems_interrupt_catch(tx_interrupt_handler, MM_IRQ_ETHTX, &dummy);
  
  MM_WRITE(MM_MINIMAC_STATE0, MINIMAC_STATE_LOADED);
  MM_WRITE(MM_MINIMAC_STATE1, MINIMAC_STATE_LOADED);

  for(i=0;i<7; i++)
    tx_buffer[i] = 0x55;
  tx_buffer[7] = 0xd5;
  MM_WRITE(MM_MINIMAC_SETUP, 0);
  rtems_bsdnet_event_send(tx_daemon_id, CTS_EVENT);
  
  bsp_interrupt_vector_enable(MM_IRQ_ETHRX);
  bsp_interrupt_vector_enable(MM_IRQ_ETHTX);

  return 1;
}

static void minimac_start(struct ifnet *ifp)
{
  rtems_bsdnet_event_send(tx_daemon_id, START_TRANSMIT_EVENT);
  ifp->if_flags |= IFF_OACTIVE;
}

static void minimac_init(void *arg)
{
  struct ifnet *ifp = &arpcom.ac_if;
  ifp->if_flags |= IFF_RUNNING;
}

static void minimac_stop(void)
{
  struct ifnet *ifp = &arpcom.ac_if;
  ifp->if_flags &= ~IFF_RUNNING;
}

static int minimac_ioctl(struct ifnet *ifp, ioctl_command_t command,
  caddr_t data)
{
  int error;
  
  error = 0;
  switch (command) {
    case SIOCGIFADDR:
    case SIOCSIFADDR:
      ether_ioctl(ifp, command, data);
      break;

    case SIOCSIFFLAGS:
      switch(ifp->if_flags & (IFF_UP | IFF_RUNNING)) {
        case IFF_RUNNING:
          minimac_stop();
          break;
        case IFF_UP:
          minimac_init(NULL);
          break;
        case IFF_UP | IFF_RUNNING:
          minimac_stop();
          minimac_init(NULL);
          break;
        default:
          break;
      }
      break;

    default:
      error = EINVAL;
      break;
  }
  return error;
}


static rtems_isr rx_interrupt_handler(rtems_vector_number vector)
{
  /* Deassert IRQ line.
   * The RX daemon will then read all the slots we marked as empty.
   */
  if(MM_READ(MM_MINIMAC_STATE0) == MINIMAC_STATE_PENDING)
    MM_WRITE(MM_MINIMAC_STATE0, MINIMAC_STATE_EMPTY);
  if(MM_READ(MM_MINIMAC_STATE1) == MINIMAC_STATE_PENDING)
    MM_WRITE(MM_MINIMAC_STATE1, MINIMAC_STATE_EMPTY);

  rtems_bsdnet_event_send(rx_daemon_id, RX_EVENT);

  lm32_interrupt_ack(1 << MM_IRQ_ETHRX);
}

static void receive_packet(uint8_t *buffer, int length)
{
  struct ifnet *ifp = &arpcom.ac_if;
  struct mbuf *m;
  struct ether_header *eh;
  uint32_t computed_crc, net_crc;
  
  if(length < 64) {
    printk("Warning: Ethernet packet too short\n");
    return;
  }

  length -= 4; /* strip CRC */
  net_crc = ((uint32_t)buffer[length])
    | ((uint32_t)buffer[length+1] << 8)
    | ((uint32_t)buffer[length+2] << 16)
    | ((uint32_t)buffer[length+3] << 24);
  length -= 8; /* strip preamble */
  computed_crc = ether_crc32_le(&buffer[8], length) ^ 0xffffffff;
  if(computed_crc == net_crc) {
    MGETHDR(m, M_WAIT, MT_DATA);
    MCLGET(m, M_WAIT);
    length -= sizeof(struct ether_header); /* strip Ethernet header */
    memcpy(m->m_data, &buffer[8+sizeof(struct ether_header)], length);
    m->m_len = m->m_pkthdr.len = length;
    m->m_pkthdr.rcvif = ifp;
    eh = (struct ether_header *)&buffer[8];
    ether_input(ifp, eh, m);
  } else
    printk("Ethernet CRC error: got %08x expected %08x (len=%d)\n",
      net_crc, computed_crc, length);
}

static void rx_daemon(void *arg)
{
  rtems_event_set events;
  
  while(1) {
    rtems_bsdnet_event_receive(RX_EVENT, RTEMS_EVENT_ANY | RTEMS_WAIT,
      RTEMS_NO_TIMEOUT, &events);

    if(MM_READ(MM_MINIMAC_STATE0) == MINIMAC_STATE_EMPTY) {
      receive_packet((uint8_t *)MINIMAC_RX0_BASE, MM_READ(MM_MINIMAC_COUNT0));
      MM_WRITE(MM_MINIMAC_STATE0, MINIMAC_STATE_LOADED);
    }
    if(MM_READ(MM_MINIMAC_STATE1) == MINIMAC_STATE_EMPTY) {
      receive_packet((uint8_t *)MINIMAC_RX1_BASE, MM_READ(MM_MINIMAC_COUNT1));
      MM_WRITE(MM_MINIMAC_STATE1, MINIMAC_STATE_LOADED);
    }
  }
}

/* RTEMS apparently doesn't support m_length() ... */
static int copy_mbuf_chain(struct mbuf *m, uint8_t *target)
{
  int len;

  len = 0;
  while(m != NULL) {
    if(m->m_len > 0) {
      m_copydata(m, 0, m->m_len, (caddr_t)(target + len));
      len += m->m_len;
    }
    m = m->m_next;
  }
  return len;
}

static void send_packet(struct ifnet *ifp, struct mbuf *m)
{
  unsigned int len;
  unsigned int crc;
  uint8_t *tx_buffer = (uint8_t *)(MINIMAC_TX_BASE+8);
  
  len = copy_mbuf_chain(m, tx_buffer);
  for(;len<60;len++)
    tx_buffer[len] = 0x00; // Padding

  crc = ether_crc32_le(tx_buffer, len) ^ 0xffffffff;

  tx_buffer[len] = crc & 0xff;
  tx_buffer[len+1] = (crc & 0xff00) >> 8;
  tx_buffer[len+2] = (crc & 0xff0000) >> 16;
  tx_buffer[len+3] = crc >> 24;

  len += 4; // We add 4 bytes of CRC32

  MM_WRITE(MM_MINIMAC_TXCOUNT, len + 8);
}

static rtems_isr tx_interrupt_handler(rtems_vector_number vector)
{
  lm32_interrupt_ack(1 << MM_IRQ_ETHTX);
  rtems_bsdnet_event_send(tx_daemon_id, CTS_EVENT);
}

static void tx_daemon(void *arg)
{
  struct ifnet *ifp = &arpcom.ac_if;
  rtems_event_set events;
  struct mbuf *m;
  
  while(1) {
    rtems_bsdnet_event_receive(START_TRANSMIT_EVENT,
      RTEMS_EVENT_ANY | RTEMS_WAIT, RTEMS_NO_TIMEOUT, &events);
    while(1) {
      IF_DEQUEUE(&ifp->if_snd, m);
      if(m == NULL)
        break;
      rtems_bsdnet_event_receive(CTS_EVENT, RTEMS_EVENT_ANY | RTEMS_WAIT,
        RTEMS_NO_TIMEOUT, &events);
      send_packet(ifp, m);
      m_freem(m);
    }
    ifp->if_flags &= ~IFF_OACTIVE;
  }
}
