/**
 * @file
 *
 * @brief Console LINFlexD implementation.
 */

/*
 * Copyright (c) 2011-2014 embedded brains GmbH.  All rights reserved.
 *
 *  embedded brains GmbH
 *  Dornierstr. 4
 *  82178 Puchheim
 *  Germany
 *  <rtems@embedded-brains.de>
 *
 * 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.
 */

#include <bsp/console-linflex.h>

#include <bsp.h>
#include <bsp/fatal.h>
#include <bsp/irq.h>

#ifdef MPC55XX_HAS_LINFLEX

mpc55xx_linflex_context mpc55xx_linflex_devices [] = {
  {
    .regs = &LINFLEX0,
    .irq_rxi = MPC55XX_IRQ_LINFLEX_RXI(0),
    .irq_txi = MPC55XX_IRQ_LINFLEX_TXI(0),
    .irq_err = MPC55XX_IRQ_LINFLEX_ERR(0),
    .tx_pcr_register = &((SIU_tag *) &SIUL)->PCR18,
    .tx_pa_value = 1,
    .rx_pcr_register = &((SIU_tag *) &SIUL)->PCR19,
    .rx_psmi_register = &((SIU_tag *) &SIUL)->PSMI31,
    .rx_padsel_value = 0
  }, {
    .regs = &LINFLEX1,
    .irq_rxi = MPC55XX_IRQ_LINFLEX_RXI(1),
    .irq_txi = MPC55XX_IRQ_LINFLEX_TXI(1),
    .irq_err = MPC55XX_IRQ_LINFLEX_ERR(1),
    .tx_pcr_register = &((SIU_tag *) &SIUL)->PCR94,
    .tx_pa_value = 1,
    .rx_pcr_register = &((SIU_tag *) &SIUL)->PCR95,
    .rx_psmi_register = &((SIU_tag *) &SIUL)->PSMI32,
    .rx_padsel_value = 2
  }
};

static void enter_init_mode(volatile LINFLEX_tag *regs)
{
  LINFLEX_LINCR1_32B_tag cr1 = { .R = regs->LINCR1.R };
  cr1.B.SLEEP = 0;
  cr1.B.INIT = 1;
  regs->LINCR1.R = cr1.R;
}

static void enter_active_mode(volatile LINFLEX_tag *regs)
{
  LINFLEX_LINCR1_32B_tag cr1 = { .R = regs->LINCR1.R };
  cr1.B.SLEEP = 0;
  cr1.B.INIT = 0;
  regs->LINCR1.R = cr1.R;
}

static void enter_sleep_mode(volatile LINFLEX_tag *regs)
{
  LINFLEX_LINCR1_32B_tag cr1 = { .R = regs->LINCR1.R };
  cr1.B.SLEEP = 1;
  cr1.B.INIT = 0;
  regs->LINCR1.R = cr1.R;
}

static void mpc55xx_linflex_poll_write(int minor, char c)
{
  mpc55xx_linflex_context *self = console_generic_get_context(minor);
  volatile LINFLEX_tag *regs = self->regs;
  const LINFLEX_UARTSR_32B_tag clear_dtf = { .B = { .DTF_TFF = 1 } };
  rtems_interrupt_level level;
  bool done = false;
  bool wait_for_transmit_done = false;

  rtems_interrupt_disable(level);
  if (self->transmit_nest_level == 0) {
	LINFLEX_LINIER_32B_tag ier = { .R = regs->LINIER.R };

	if (ier.B.DTIE != 0) {
	  ier.B.DTIE = 0;
	  regs->LINIER.R = ier.R;
	  wait_for_transmit_done = !self->transmit_in_progress;
	  self->transmit_nest_level = 1;
	}
  } else {
	++self->transmit_nest_level;
  }
  rtems_interrupt_enable(level);

  while (!done) {
	rtems_interrupt_disable(level);
	bool tx = self->transmit_in_progress;
	if (!tx || (tx && regs->UARTSR.B.DTF_TFF)) {
	  regs->UARTSR.R = clear_dtf.R;
	  regs->BDRL.B.DATA0 = c;
	  self->transmit_in_progress = true;
	  done = true;
	}
	rtems_interrupt_enable(level);
  }

  done = false;
  while (!done) {
	rtems_interrupt_disable(level);
	if (wait_for_transmit_done) {
	  if (regs->UARTSR.B.DTF_TFF) {
		regs->UARTSR.R = clear_dtf.R;
		self->transmit_in_progress = false;
		done = true;
	  }
	} else {
	  done = true;
	}

	if (done && self->transmit_nest_level > 0) {
	  --self->transmit_nest_level;

	  if (self->transmit_nest_level == 0) {
        LINFLEX_LINIER_32B_tag ier = { .R = regs->LINIER.R };

        ier.B.DTIE = 1;
		regs->LINIER.R = ier.R;
	  }
	}
	rtems_interrupt_enable(level);
  }
}

static void mpc55xx_linflex_rx_interrupt_handler(void *arg)
{
  mpc55xx_linflex_context *self = arg;
  volatile LINFLEX_tag *regs = self->regs;
  char c = regs->BDRM.B.DATA4;
  const LINFLEX_UARTSR_32B_tag clear_flags = { .B = { .RMB = 1, .DRF_RFE = 1 } };

  regs->UARTSR.R = clear_flags.R;

  rtems_termios_enqueue_raw_characters(self->tty, &c, 1);
}

static void mpc55xx_linflex_tx_interrupt_handler(void *arg)
{
  mpc55xx_linflex_context *self = arg;
  volatile LINFLEX_tag *regs = self->regs;

  regs->UARTSR.B.DTF_TFF = 1;	/* clear flag */
  self->transmit_in_progress = false;

  rtems_termios_dequeue_characters(self->tty, 1);
}

/*
static void mpc55xx_linflex_err_interrupt_handler(void *arg)
{
  mpc55xx_linflex_context *self = arg;
}
*/

static int mpc55xx_linflex_set_attributes(int minor, const struct termios *t)
{
  mpc55xx_linflex_context *self = console_generic_get_context(minor);
  volatile LINFLEX_tag *regs = self->regs;
  LINFLEX_UARTCR_32B_tag uartcr = { .R = 0 };
  LINFLEX_GCR_32B_tag gcr = { .R = 0 };
  LINFLEX_LINIER_32B_tag ier = { .R = 0 };
  rtems_termios_baud_t br = rtems_termios_baud_to_number(t->c_ospeed);
  LINFLEX_LINFBRR_32B_tag fbrr = { .R = 0 };
  LINFLEX_LINIBRR_32B_tag ibrr = { .R = 0 };

  enter_init_mode(regs);

  /* Set to UART-mode */
  uartcr.B.UART = 1;
  regs->UARTCR.R = uartcr.R;

  /* Set to buffer mode with size 1 */
  uartcr.B.TDFL_TFC = 0;
  uartcr.B.RDFL_RFC0 = 0;
  uartcr.B.RFBM = 0;
  uartcr.B.TFBM = 0;

  /* Enable receiver and transmitter */
  uartcr.B.RXEN = 1;
  uartcr.B.TXEN = 1;

  /* Number of data bits */
  uartcr.B.WL1 = 0;
  if ((t->c_cflag & CSIZE) == CS8) {
	uartcr.B.WL0 = 1;
  } else if ((t->c_cflag & CSIZE) == CS7) {
	uartcr.B.WL0 = 0;
  } else {
    return -1;
  }

  /* Parity */
  uartcr.B.PCE = (t->c_cflag & PARENB) ? 1 : 0;
  uartcr.B.PC1 = 0;
  uartcr.B.PC0 = (t->c_cflag & PARODD) ? 1 : 0;

  /* Stop bits */
  gcr.B.STOP = (t->c_cflag & CSTOPB) ? 1 : 0;

  /* Set control registers */
  regs->UARTCR.R = uartcr.R;
  regs->GCR.R = gcr.R;

  /* Interrupts */
  ier.B.DTIE = 1;
  ier.B.DRIE = 1;
  regs->LINIER.R = ier.R;

  /* Baud rate */
  if (br > 0) {
	uint32_t lfdiv_mult_32 = bsp_clock_speed * 2 / br;
	if((lfdiv_mult_32 & 0x1) != 0) {
		++lfdiv_mult_32;
	}
	fbrr.B.FBR = (lfdiv_mult_32 >> 1) & 0xF;
	ibrr.B.IBR = lfdiv_mult_32 >> 5;
  } else {
    return -1;
  }
  regs->LINFBRR.R = fbrr.R;
  regs->LINIBRR.R = ibrr.R;

  enter_active_mode(regs);

  return 0;
}

static int mpc55xx_linflex_first_open(int major, int minor, void *arg)
{
  rtems_status_code sc = RTEMS_SUCCESSFUL;
  int rv = 0;
  mpc55xx_linflex_context *self = console_generic_get_context(minor);
  struct rtems_termios_tty *tty = console_generic_get_tty_at_open(arg);
  SIU_PCR_tag pcr = { .R = 0 };
  SIUL_PSMI_8B_tag psmi = { .R = 0 };

  self->tty = tty;

  pcr.B.IBE = 1;
  self->rx_pcr_register->R = pcr.R;
  psmi.B.PADSEL = self->rx_padsel_value;
  self->rx_psmi_register->R = psmi.R;
  pcr.R = 0;
  pcr.B.OBE = 1;
  pcr.B.PA = self->tx_pa_value;
  self->tx_pcr_register->R = pcr.R;

  rv = rtems_termios_set_initial_baud(tty, BSP_DEFAULT_BAUD_RATE);
  if (rv != 0) {
    bsp_fatal(MPC55XX_FATAL_CONSOLE_LINFLEX_BAUD);
  }

  rv = mpc55xx_linflex_set_attributes(minor, &tty->termios);
  if (rv != 0) {
    bsp_fatal(MPC55XX_FATAL_CONSOLE_LINFLEX_ATTRIBUTES);
  }

  sc = mpc55xx_interrupt_handler_install(
    self->irq_rxi,
    "LINFlexD RXI",
    RTEMS_INTERRUPT_UNIQUE,
    MPC55XX_INTC_DEFAULT_PRIORITY,
    mpc55xx_linflex_rx_interrupt_handler,
    self
  );
  if (sc != RTEMS_SUCCESSFUL) {
    bsp_fatal(MPC55XX_FATAL_CONSOLE_LINFLEX_RX_IRQ_INSTALL);
  }

  sc = mpc55xx_interrupt_handler_install(
    self->irq_txi,
    "LINFlexD TXI",
    RTEMS_INTERRUPT_UNIQUE,
    MPC55XX_INTC_DEFAULT_PRIORITY,
    mpc55xx_linflex_tx_interrupt_handler,
    self
  );
  if (sc != RTEMS_SUCCESSFUL) {
    bsp_fatal(MPC55XX_FATAL_CONSOLE_LINFLEX_TX_IRQ_INSTALL);
  }

  /*
  sc = mpc55xx_interrupt_handler_install(
    self->irq_err,
    "LINFlexD ERR",
    RTEMS_INTERRUPT_UNIQUE,
    MPC55XX_INTC_DEFAULT_PRIORITY,
    mpc55xx_linflex_err_interrupt_handler,
    self
  );
  if (sc != RTEMS_SUCCESSFUL) {
    bsp_fatal(MPC55XX_FATAL_CONSOLE_LINFLEX_ERR_IRQ_INSTALL);
  }
  */

  return 0;
}

static int mpc55xx_linflex_last_close(int major, int minor, void* arg)
{
  rtems_status_code sc = RTEMS_SUCCESSFUL;
  mpc55xx_linflex_context *self = console_generic_get_context(minor);
  volatile LINFLEX_tag *regs = self->regs;
  SIU_PCR_tag pcr = { .R = 0 };
  SIUL_PSMI_8B_tag psmi = { .R = 0 };

  /* enter initialization mode */
  enter_init_mode(regs);

  /* disable interrupts */
  regs->LINIER.R = 0;

  /* set module to sleep mode */
  enter_sleep_mode(regs);

  sc = rtems_interrupt_handler_remove(
    self->irq_rxi,
    mpc55xx_linflex_rx_interrupt_handler,
    self
  );
  if (sc != RTEMS_SUCCESSFUL) {
    bsp_fatal(MPC55XX_FATAL_CONSOLE_LINFLEX_RX_IRQ_REMOVE);
  }

  sc = rtems_interrupt_handler_remove(
    self->irq_txi,
    mpc55xx_linflex_tx_interrupt_handler,
    self
  );
  if (sc != RTEMS_SUCCESSFUL) {
    bsp_fatal(MPC55XX_FATAL_CONSOLE_LINFLEX_TX_IRQ_REMOVE);
  }

  /*
  sc = rtems_interrupt_handler_remove(
    self->irq_err,
    mpc55xx_linflex_err_interrupt_handler,
    self
  );
  if (sc != RTEMS_SUCCESSFUL) {
    bsp_fatal(MPC55XX_FATAL_CONSOLE_LINFLEX_ERR_IRQ_REMOVE);
  }
  */

  pcr.B.IBE = 1;
  self->rx_pcr_register->R = pcr.R;
  self->tx_pcr_register->R = pcr.R;
  psmi.R = 0;
  self->rx_psmi_register->R = psmi.R;

  self->tty = NULL;

  return 0;
}

static int mpc55xx_linflex_poll_read(int minor)
{
  mpc55xx_linflex_context *self = console_generic_get_context(minor);
  volatile LINFLEX_tag *regs = self->regs;
  rtems_interrupt_level level;
  int c = -1;

  rtems_interrupt_disable(level);
  if (regs->UARTSR.B.DRF_RFE != 0) {
    /* Clear flag */
    regs->UARTSR.B.DRF_RFE = 1;

    /* Read */
    c = regs->BDRM.B.DATA4;
  }
  rtems_interrupt_enable(level);

  return c;
}

static int mpc55xx_linflex_write(int minor, const char *out, size_t n)
{
  if (n > 0) {
    mpc55xx_linflex_context *self = console_generic_get_context(minor);
    volatile LINFLEX_tag *regs = self->regs;

    regs->BDRL.B.DATA0 = out [0];
    self->transmit_in_progress = true;
    /* TODO: send more then one byte */
  }

  return 0;
}

const console_generic_callbacks mpc55xx_linflex_callbacks = {
  .termios_callbacks = {
    .firstOpen = mpc55xx_linflex_first_open,
    .lastClose = mpc55xx_linflex_last_close,
    .write = mpc55xx_linflex_write,
    .setAttributes = mpc55xx_linflex_set_attributes,
    .outputUsesInterrupts = TERMIOS_IRQ_DRIVEN
  },
  .poll_read = mpc55xx_linflex_poll_read,
  .poll_write = mpc55xx_linflex_poll_write
};

#endif /* MPC55XX_HAS_LINFLEX */
