/*
    This file is part of libtermui.

    libtermui 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.

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

    Copyright 2010, Alexandre Becoulet <alexandre.becoulet@free.fr>

*/

//#define TERMUI_DEBUG

#include <unistd.h>
#include <assert.h>
#include <errno.h>
#include <arpa/telnet.h>

#include "term_pv.h"
#include "termio_pv.h"

# if defined (CONFIG_LIBTERMUI_SSL) && defined(CONFIG_LIBTERMUI_SOCKET)
# define TELNET_RAW_WRITE(tm, a, b) (tm)->io_telnet.raw_write(tm, a, b) 
# define TELNET_RAW_READ(tm) (tm)->io_telnet.raw_read(tm) 
#elif defined(CONFIG_LIBTERMUI_SOCKET)
# define TELNET_RAW_WRITE(tm, a, b) termui_socket_write(tm, a, b) 
# define TELNET_RAW_READ(tm) termui_socket_read(tm) 
#elif defined(CONFIG_LIBTERMUI_SSL)
# define TELNET_RAW_WRITE(tm, a, b) termui_ssl_write(tm, a, b) 
# define TELNET_RAW_READ(tm) termui_ssl_read(tm) 
#else
# error
#endif

#ifdef CONFIG_LIBTERMUI_SOCKET
static TERMUI_TERMIO_FCN_WRITE(termui_socket_write)
{
  while (size)
    {
      termui_ssize_t res;

      res = write(tm->io_telnet.tr.soc, data, size);

      if (res < 0)
	{
	  tm->ioerr = 1;
	  return;
	}

      assert(res <= size);
      size -= res;
    }
}

static TERMUI_TERMIO_FCN_READ(termui_socket_read)
{
  termui_ssize_t res = 0;
  uint8_t c;

  while (res <= 0)
    {
      res = read(tm->io_telnet.tr.soc, &c, 1);

      if (res < 0)
	{
	  if (errno == EINTR)
	    continue;
	  tm->ioerr = errno;
	  return -1;
	}
    }

#ifdef TERMUI_DEBUG
  printf("read %02x (%3u)\n", c, c);
#endif

  return c;
}

static TERMUI_TERMIO_FCN_POLLFD(termui_socket_pollfd)
{
  return tm->io_telnet.tr.soc;
}
#endif

# ifdef CONFIG_LIBTERMUI_SSL
static TERMUI_TERMIO_FCN_WRITE(termui_ssl_write)
{
  SSL *ssl = tm->io_telnet.tr.ssl;
  int ret = SSL_write(ssl, data, size);

  if (ret <= 0)
    tm->ioerr = SSL_get_error(ssl, ret);
}

static TERMUI_TERMIO_FCN_READ(termui_ssl_read)
{
  SSL *ssl = tm->io_telnet.tr.ssl;
  uint8_t c;
  int ret = SSL_read(ssl, &c, 1);

  if (ret <= 0)
    {
      tm->ioerr = SSL_get_error(ssl, ret);
      return -1;
    }

#ifdef TERMUI_DEBUG
  printf("read %02x (%3u)\n", c, c);
#endif

  return c;
}

static TERMUI_TERMIO_FCN_POLLFD(termui_ssl_pollfd)
{
  return SSL_get_rfd(tm->io_telnet.tr.ssl);
}
# endif

static TERMUI_TERM_FCN_GETSIZE(termui_telnet_getsize)
{
  *x = tm->io_telnet.width;
  *y = tm->io_telnet.height;

  return 0;
}

/* read unsigned 16 bits integer from socket with IAC escaping */

static uint16_t termui_telnet_get_u16(struct termui_term_s *tm)
{
  uint8_t high = termui_io_getc(tm);
  uint8_t low = termui_io_getc(tm);

  return (high << 8) | low;
}

static int16_t temui_term_telnet_aic(struct termui_term_s *tm,
				     uint8_t opt[3])
{
  int_fast16_t res = 0;
  uint8_t opts[3] = { IAC };

  switch (opt[1] = TELNET_RAW_READ(tm))
    {
    case WILL:
      opts[2] = opt[2] = TELNET_RAW_READ(tm);
      switch (opt[2])
	{
	case TELOPT_NAWS:
	  /* we requested these options */
	  break;

	case TELOPT_TTYPE: {
	  opts[1] = SB;
	  TELNET_RAW_WRITE(tm, opts, 3);
	  opts[0] = TELQUAL_SEND;
	  opts[1] = IAC;
	  opts[2] = SE;
	  TELNET_RAW_WRITE(tm, opts, 3);
	  break;
	}

	default:
	  opts[1] = DONT;
	  TELNET_RAW_WRITE(tm, opts, 3);
	  break;
	}
      break;

    case DO:
      opts[2] = opt[2] = TELNET_RAW_READ(tm);
      switch (opt[2])
	{
	case TELOPT_ECHO:
	case TELOPT_SGA:
	  /* we requested these options */
	  break;

	default:
	  opts[1] = WONT;
	  TELNET_RAW_WRITE(tm, opts, 3);
	  break;
	}
      break;

    case SB:	/* option subnegotiation */
      opt[2] = TELNET_RAW_READ(tm);
      switch (opt[2])
	{
	case TELOPT_NAWS: {
	  /* client sent new terminal size */
	  uint16_t w = termui_telnet_get_u16(tm);
	  uint16_t h = termui_telnet_get_u16(tm);

	  if (!w || !h)
	    break;

	  tm->io_telnet.width = w;
	  tm->io_telnet.height = h;

	  /* register telnet getsize method or send resize event */
	  if (tm->s_getsize != termui_telnet_getsize)
	    tm->s_getsize = termui_telnet_getsize;
	  else
	    res = -2;

#ifdef TERMUI_DEBUG
	  printf("telnet term size: %ix%i\n", w, h);
#endif
	  break;
	}

	case TELOPT_TTYPE: {
	  char *ttype = tm->type;
	  termui_strlen_t i;

	  /* TELQUAL_IS */
	  uint8_t c = TELNET_RAW_READ(tm);

	  for (i = 0; !tm->ioerr; i++)
	    {
	      c = TELNET_RAW_READ(tm);

	      if (c == IAC)
		{
		  /* SE */
		  TELNET_RAW_READ(tm);
		  break;
		}

	      if (i < TERMUI_TYPESTR_MAXLEN - 1)
		*ttype++ = c;
	    }

	  *ttype = 0;

#ifdef TERMUI_DEBUG
	  printf("telnet term type:'%s'\n", tm->type);
#endif

	  termui_term_set(tm);
	  break;
	}

	}
      break;

    case WONT:
    case DONT:
      TELNET_RAW_READ(tm);
      /* all option are undefined on our side,
	 assume echo and sga wont be declined by client */
      break;

    case 255:
      res = 255;

    default:
      break;
    }

#ifdef TERMUI_DEBUG
  printf("%3u %3u %3u %i\n", opt[0], opt[1], opt[2], res);
#endif

  return res;
}

static TERMUI_TERMIO_FCN_WRITE(termui_telnet_write)
{
  const uint8_t *data_ = data;

  while (!tm->ioerr && size)
    {
      termui_strlen_t i;

      for (i = 0; i < size && data_[i] != IAC; i++)
	;

      if (i)
	{
	  TELNET_RAW_WRITE(tm, data_, i);
	  size -= i;
	  data_ += i;
	}

      if (i < size && data_[i] == IAC)
	{
	  static const char iac_escape[2] = { IAC, 255 };

	  TELNET_RAW_WRITE(tm, iac_escape, 2);
	  size--;
	  data_++;
	}
    }
}

static TERMUI_TERMIO_FCN_READ(termui_telnet_read)
{
  int_fast16_t ret;

  while (1)
    {
      ret = TELNET_RAW_READ(tm);

      if (tm->ioerr)
	return -1;

      if (ret == IAC)
	{
	  uint8_t opt[3];
	  if (!(ret = temui_term_telnet_aic(tm, opt)))
	    continue;
	}

      return ret;
    }
}

static void termui_telnet_wait_sb(struct termui_term_s *tm, uint8_t option)
{
  uint8_t opt[3];

  do {
    if (TELNET_RAW_READ(tm) == IAC)
      {
	temui_term_telnet_aic(tm, opt);

	if ((opt[1] == SB || opt[1] == WONT) && opt[2] == option)
	  break;
      }
  } while (!tm->ioerr);

}

static void termui_telnet_send_setup(struct termui_term_s *tm)
{
  uint8_t opt[3] = { IAC };

  /* tell the client that the termio will take care to send echo */
  opt[1] = WILL;
  opt[2] = TELOPT_ECHO;
  TELNET_RAW_WRITE(tm, opt, 3);

  /* tell the client we wont send go ahead character */
  opt[1] = WILL;
  opt[2] = TELOPT_SGA;
  TELNET_RAW_WRITE(tm, opt, 3);

  /* suggest use of window size option */
  opt[1] = DO;
  opt[2] = TELOPT_NAWS;
  TELNET_RAW_WRITE(tm, opt, 3);

  termui_telnet_wait_sb(tm, TELOPT_NAWS);

  /* request terminal type */
  opt[1] = DO;
  opt[2] = TELOPT_TTYPE;
  TELNET_RAW_WRITE(tm, opt, 3);

  termui_telnet_wait_sb(tm, TELOPT_TTYPE);
}

static TERMUI_TERMIO_FCN_CLEANUP(termui_telnet_cleanup)
{
}

#ifdef CONFIG_LIBTERMUI_SOCKET
termui_err_t
termui_telnet_io_init(struct termui_term_s *tm, int32_t socket)
{
  memset(tm, 0, sizeof(*tm));
  tm->io_telnet.tr.soc = socket;

  /* register tty operations in term object */
  tm->s_write = termui_telnet_write;
  tm->s_read = termui_telnet_read;
  tm->s_pollfd = termui_socket_pollfd;
  tm->s_cleanup = termui_telnet_cleanup;

# ifdef CONFIG_LIBTERMUI_SSL
  tm->io_telnet.raw_read = termui_socket_read;
  tm->io_telnet.raw_write = termui_socket_write;
# endif
  tm->s_getsize = (termui_termio_getsize_t*)term_err;

  /* default to dumb terminal */
  termui_term_set_none(tm);

  termui_telnet_send_setup(tm);

  return tm->ioerr;
}
#endif

#ifdef CONFIG_LIBTERMUI_SSL
termui_err_t
termui_telnet_ssl_io_init(struct termui_term_s *tm, SSL *ssl)
{
  memset(tm, 0, sizeof(*tm));
  tm->io_telnet.tr.ssl = ssl;

  /* register tty operations in term object */
  tm->s_write = termui_telnet_write;
  tm->s_read = termui_telnet_read;
  tm->s_pollfd = termui_ssl_pollfd;
  tm->s_cleanup = termui_telnet_cleanup;

# ifdef CONFIG_LIBTERMUI_SOCKET
  tm->io_telnet.raw_read = termui_ssl_read;
  tm->io_telnet.raw_write = termui_ssl_write;
# endif

  tm->s_getsize = (termui_termio_getsize_t*)term_err;

  /* default to dumb terminal */
  termui_term_set_none(tm);

  termui_telnet_send_setup(tm);

  return tm->ioerr;
}
#endif

