/*!
 * \file alcd.c
 * \brief
 *    A target independent Alpharithmetic LCD driver
 *
 * Author: Christos Choutouridis AEM: 8997
 * email : <cchoutou@ece.auth.gr>
 *
 */

#include "alcd.h"

static  int _inc_x (alcd_t *alcd);
static  int _dec_x (alcd_t *alcd);
static void _inc_y (alcd_t *alcd);
static void _dec_y (alcd_t *alcd);
static void _set_bus (alcd_t *alcd, int8_t db);
static void _write_data (alcd_t *alcd, int8_t data);
static void _command (alcd_t *alcd, uint8_t c);
static void _character (alcd_t *alcd, uint8_t c);
static void _set_cursor (alcd_t *alcd, uint8_t x, uint8_t y);


/*!
 * \brief
 *    increase cursor's x position. Positions start from 1.
 *    If the cursor loops returns 1, else returns 0.
 * \param  alcd   pointer to active alcd.
 * \return        Change line status
 */
static int _inc_x (alcd_t *alcd) {
   int ret=0;

   if (++alcd->c.x > alcd->columns) {
      alcd->c.x = 1;
      ret = 1;
   }
   return ret;
}

/*!
 * \brief
 *    Decrease cursor's x position. Positions start from 1.
 *    If the cursor loops returns 1, else returns 0.
 * \param  alcd   pointer to active alcd.
 * \return none
 */
static int _dec_x (alcd_t *alcd) {
   int ret = 0;

   if (--alcd->c.x < 1) {
      alcd->c.x = alcd->columns;
      ret = 1;
   }
   return ret;
}

/*!
 * \brief
 *    increase cursor's y position. Positions start from 1.
 * \param  alcd   pointer to active alcd.
 * \return none
 */
static void _inc_y (alcd_t *alcd) {
   if (++alcd->c.y > alcd->lines) {
      alcd->c.y = 1;
   }
}

/*!
 * \brief
 *    Decrease cursor's y position. Positions start from 1.
 * \param  alcd   pointer to active alcd.
 * \return none
 */
static void _dec_y (alcd_t *alcd) {
   if (--alcd->c.y < 1) {
      alcd->c.y = alcd->lines;
   }
}

/*!
 * \brief
 *    Update the bus I/O and send it to the device.
 * \param  alcd   pointer to active alcd.
 * \param  db     the bus data.
 * \return none
 */
static void _set_bus (alcd_t *alcd, int8_t db)
{
   alcd->io.db4 (db & 0x01);   //Update port
   alcd->io.db5 (db & 0x02);
   alcd->io.db6 (db & 0x04);
   alcd->io.db7 (db & 0x08);
   jf_delay_us (10);     // Wait to settle

   alcd->io.en (1);      // Pulse out the data
   jf_delay_us (10);
   alcd->io.en (0);
   jf_delay_us (10);     // Data hold
}

/*!
 * \brief
 *    Write the byte date to the bus using _set_bus ()
 * \param  alcd   pointer to active alcd.
 * \param  data   the data byte.
 * \return none
 */
static void _write_data (alcd_t *alcd, int8_t data)
{
   _set_bus (alcd, data >> 4);
   _set_bus (alcd, data & 0x0F);
}

/*!
 * \brief
 *    Send a command to alcd
 * \param  alcd   pointer to active alcd.
 * \param  c      the command byte.
 * \return none
 */
static void _command (alcd_t *alcd, uint8_t c)
{
   alcd->io.rs(0);         // Enter command mode
   jf_delay_us (100);      // Wait
   _write_data (alcd, c);  // Send
}

/*!
 * \brief
 *    Send a character to alcd
 * \param  alcd   pointer to active alcd.
 * \param  c      the character byte.
 * \return none
 */
static void _character (alcd_t *alcd, uint8_t c)
{
   alcd->io.rs(1);         // Enter character mode
   jf_delay_us (100);      // Wait
   _write_data (alcd, c);  // Send
}

/*!
 * \brief
 *    Set the Cursor to LCD's position line (y), column (x) starts from 1,2,...n
 * \param  alcd   pointer to active alcd.
 * \param  x      the x position.
 * \param  y      the y position.
 * \return none
 */
static void _set_cursor (alcd_t *alcd, uint8_t x, uint8_t y)
{
   uint8_t cmd;

   alcd->c.x = x;             // Update alcd data
   alcd->c.y = y;

   // Calculate address
   switch (y) {
      default:
      case 1: cmd = 0x0;   break;
      case 2: cmd = 0x40;  break;
      case 3: cmd = 0x0 + alcd->columns;  break;
      case 4: cmd = 0x40 + alcd->columns; break;
   }
   cmd |= (x - 1);

   // Calculate command
   cmd |= LCD_DDRAMMask;

   // Command out the alcd
   _command (alcd, cmd);
}


/*
 * ============================ Public Functions ============================
 */

/*
 * Link and Glue functions
 */

/*!
 * \brief
 *    Link driver's db4 pin function to io struct.
 * \param  alcd   pointer to active alcd.
 * \param  pfun   driver's DB4 pin function
 * \return none
 */
inline void alcd_link_db4 (alcd_t *alcd, drv_pinout_ft pfun) { alcd->io.db4 = pfun; }

/*!
 * \brief
 *    Link driver's db4 pin function to io struct.
 * \param  alcd   pointer to active alcd.
 * \param  pfun   driver's DB5 pin function
 * \return none
 */
inline void alcd_link_db5 (alcd_t *alcd, drv_pinout_ft pfun) { alcd->io.db5 = pfun; }

/*!
 * \brief
 *    Link driver's db4 pin function to io struct.
 * \param  alcd   pointer to active alcd.
 * \param  pfun   driver's DB6 pin function
 * \return none
 */
inline void alcd_link_db6 (alcd_t *alcd, drv_pinout_ft pfun) { alcd->io.db6 = pfun; }

/*!
 * \brief
 *    Link driver's db4 pin function to io struct.
 * \param  alcd   pointer to active alcd.
 * \param  pfun   driver's DB7 pin function
 * \return none
 */
inline void alcd_link_db7 (alcd_t *alcd, drv_pinout_ft pfun) { alcd->io.db7 = pfun; }

/*!
 * \brief
 *    Link driver's db4 pin function to io struct.
 * \param  alcd   pointer to active alcd.
 * \param  pfun   driver's RS pin function
 * \return none
 */
inline void alcd_link_rs (alcd_t *alcd, drv_pinout_ft pfun) { alcd->io.rs = pfun; }

/*!
 * \brief
 *    Link driver's db4 pin function to io struct.
 * \param  alcd   pointer to active alcd.
 * \param  pfun   driver's EN pin function
 * \return none
 */
inline void alcd_link_en (alcd_t *alcd, drv_pinout_ft pfun) { alcd->io.en = pfun; }

/*!
 * \brief
 *    Link driver's db4 pin function to io struct.
 * \param  alcd   pointer to active alcd.
 * \param  pfun   driver's BL pin function
 * \return none
 */
inline void alcd_link_bl (alcd_t *alcd, drv_pinout_ft pfun) { alcd->io.bl = pfun; }

/*!
 * \brief
 *    Send an ascii character to alcd.
 * \param  alcd   pointer to active alcd.
 * \param  ch     the character to send
 * \return the character send.
 *
 * \note
 *    This is the driver's "putchar()" functionality to glue.
 *    Tailor this function to redirect stdout to alcd.
 */
int alcd_putchar (alcd_t *alcd, int ch)
{
   alcd->status = DRV_BUSY;
   // LCD Character dispatcher
   switch (ch) {
      case 0:
         // don't send null termination to device
         break;
      case '\n':
         _inc_y (alcd);
         //break; This "no break" is intentional
      case '\r':
         _set_cursor (alcd, 1, alcd->c.y);
         break;
      case '\v':
         alcd->c.x = alcd->c.y = 1;
         _command (alcd, LCD_RETHOME);
         jf_delay_us(2000);
         break;
      case '\f':
         //alcd->c.x = alcd->c.y = 1;
         _set_cursor (alcd, 1, 1);
           //_command (alcd, LCD_CLRSCR);
           //jf_delay_us(5000);
         //_command (alcd, LCD_RETHOME);
           //_set_cursor (alcd, alcd->c.x, alcd->c.y);
         jf_delay_us(2000);
         break;
      case '\b':
         if (_dec_x (alcd))   _dec_y (alcd);
         _set_cursor (alcd, alcd->c.x, alcd->c.y);
         _character (alcd, ' ');
         _set_cursor (alcd, alcd->c.x, alcd->c.y);
         break;
      default:
         _character (alcd, ch);
         // Increase cursor and loop inside the same line
         if (_inc_x (alcd))   _set_cursor (alcd, alcd->c.x, alcd->c.y);
         break;
   }

   // Restore status
   alcd->status = DRV_READY;

   //ANSI C (C99) compatible mode
   return ch;
}


/*
 * Set functions
 */

/*!
 * \brief
 *    Set the number of lines for the attached lcd display
 * \param  alcd   pointer to active alcd.
 * \param  lines  The number of lines (usually 2 or 4)
 * \return None.
 */
void alcd_set_lines (alcd_t *alcd, int lines) {
   alcd->lines = lines;
}

/*!
 * \brief
 *    Set the number of columns for the attached lcd display
 * \param  alcd   pointer to active alcd.
 * \param  lines  The number of columns (usually 16 or 20)
 * \return None.
 */
void alcd_set_columns (alcd_t *alcd, int columns) {
   alcd->columns = columns;
}

/*
 * User Functions
 */

/*!
 * \brief
 *    De-initialize the alcd.
 * \param  alcd   pointer to active alcd.
 * \return none
 */
void alcd_deinit (alcd_t *alcd)
{
   memset ((void*)alcd, 0, sizeof (alcd_t));
   /*!<
    * This leaves the status DRV_NOINIT
    */
}

/*!
 * \brief
 *    Initialize the alcd.
 * \param  alcd   pointer to active alcd.
 * \return Zero on success, non zero on error
 */
drv_status_en alcd_init (alcd_t *alcd, alcd_funset_en fs)
{
   #define _lcd_assert(_x)  if (!_x) return alcd->status = DRV_ERROR;

   drv_status_en st = jf_probe ();

   if (st == DRV_NODEV || st == DRV_BUSY)
      return alcd->status = DRV_ERROR;
   _lcd_assert (alcd->io.db4);
   _lcd_assert (alcd->io.db5);
   _lcd_assert (alcd->io.db6);
   _lcd_assert (alcd->io.db7);
   _lcd_assert (alcd->io.rs);
   _lcd_assert (alcd->io.en);
   //_lcd_assert (alcd->io.bl);

   /*
    * We are here, so all its OK. We can (re)initialize alcd.
    */
   alcd->status = DRV_NOINIT;
   alcd->c.x = alcd->c.y = 1;
   alcd->io.en (0);
   alcd->io.rs (0);
   jf_delay_us (100000);

   //Pre-Init phase 8bit at this point
   _set_bus (alcd, 0x3);
      jf_delay_us(50000);
   _set_bus (alcd, 0x3);
      jf_delay_us(5000);
   _set_bus (alcd, 0x3);
      jf_delay_us(5000);

   _set_bus (alcd, 0x2);              //4bit selection
      jf_delay_us(10000);

   _command (alcd, fs);     //4bit selection and Function Set
      jf_delay_us(5000);
   _command (alcd, LCD_DISP_OFF);    //Display Off Control 4bit for now on
      jf_delay_us(5000);
   _command (alcd, LCD_CLRSCR);      //Clear Display
      jf_delay_us(5000);
   _command (alcd, LCD_ENTRYMODE);   //Entry Mode Set
      jf_delay_us(5000);

   _command (alcd, LCD_RETHOME);
      jf_delay_us(10000);
   _command (alcd, LCD_DISP_ON);
      jf_delay_us(5000);
   //alcd_backlight (alcd, 1);
   return alcd->status = DRV_READY;

   #undef _lcd_assert
}

/*!
 * \brief
 *    Enables and disables the lcd backlight.
 * \param  alcd   pointer to active alcd.
 * \param  on
 *    \arg 0      disable the backlight
 *    \arg 1      enable the backlight
 * \return none
 */
void alcd_backlight (alcd_t *alcd, uint8_t on) {
   if (alcd->io.bl)
      alcd->io.bl ((on)?1:0);
}

/*!
 * \brief
 *    Enables and disables the entire lcd (+backlight).
 * \param  alcd   pointer to active alcd.
 * \param  on
 *    \arg 0      disable the backlight
 *    \arg 1      enable the backlight
 * \return none
 */
void alcd_enable (alcd_t *alcd, uint8_t on)
{
   if (on) {
      _command (alcd, LCD_DISP_ON);
      alcd_backlight (alcd, 1);
   } else {
      _command (alcd, LCD_DISP_OFF);
      alcd_backlight (alcd, 0);
   }
}

/*!
 * \brief
 *    Clears screen and returns cursor at home position (1,1).
 * \param  alcd   pointer to active alcd.
 * \return none
 */
void alcd_cls (alcd_t *alcd)
{
   _command(alcd, LCD_CLRSCR);
   jf_delay_us(2000);
   _command (alcd, LCD_RETHOME);
   jf_delay_us(2000);
}

/*!
 * \brief
 *    Shift alcd left or right for a \a pos characters.
 * \param  alcd   pointer to active alcd.
 * \param  pos    The number of position to shift.
 *    A positive number shifts lcd data to left, so screen shows the data in the right.
 *    A negative number shifts lcd data to right, so screen shows the data in the left.
 * \return none
 */
void alcd_shift (alcd_t *alcd, int pos)
{
   uint8_t i, cmd = LCD_SHIFT_LEFT;

   if (pos<0) {
      pos = -pos;
      cmd = LCD_SHIFT_RIGHT;
   }
   for (i=0 ; i<pos ; ++i) {
      _command (alcd, cmd);
      jf_delay_us(100);
   }
}

// Allows us to fill the first 8 CGRAM locations
// with custom characters
void alcd_createChar (alcd_t *alcd, uint8_t location, uint8_t charmap[])
{
   location &= 0x7;        // we only have 8 locations 0-7
   _command (alcd, LCD_SETCGRAMADDR | (location << 3));

   for (int i=0; i<8; i++) {
      _character (alcd, charmap[i]);
   }
}

