/*
 * "Copyright (c) 2008 The Regents of the University  of California.
 * All rights reserved."
 *
 * Permission to use, copy, modify, and distribute this software and its
 * documentation for any purpose, without fee, and without written agreement is
 * hereby granted, provided that the above copyright notice, the following
 * two paragraphs and the author appear in all copies of this software.
 *
 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR
 * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT
 * OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE UNIVERSITY OF
 * CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS
 * ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATION TO
 * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS."
 *
 */
/*
 * @author Stephen Dawson-Haggerty <stevedh@eecs.berkeley.edu>
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdarg.h>

#include "vty.h"
#include "../logging.h"

#define max(X,Y)  (((X) > (Y)) ?  (X) : (Y))

typedef enum {
  FALSE = 0,
  TRUE  = 1,
} bool;

enum {
  VTY_REMOVE_PENDING = 0x1,
};

struct vty_client {
  int                 flags;
  int                 readfd;
  int                 writefd;
  struct sockaddr_in6 ep;
  struct vty_client  *next;
  
  int                 buf_off;
  unsigned char       buf[2][1024];
  int                 argc;
  char                *argv[N_ARGS];
};

static int sock = -1;
static struct vty_client *conns = NULL;
static struct vty_cmd_table *cmd_tab;
static char prompt_str[40];

int vty_init(struct vty_cmd_table * cmds, short port) {
  struct sockaddr_in6 si_me = {
    .sin6_family = AF_INET6,
    .sin6_port = htons(port),
    .sin6_addr = IN6ADDR_ANY_INIT,
  };
  struct vty_client *tty_client;
  int len, yes = 1;

  conns = NULL;
  cmd_tab = cmds;

  strncpy(prompt_str, "blip:", 5);
  gethostname(prompt_str+5, sizeof(prompt_str)- 5);
  len=strlen(prompt_str+5);
  prompt_str[len+5]='>';
  prompt_str[len+6]=' ';
  prompt_str[len+7]='\0';

  if (isatty(fileno(stdin))) {
    setbuf(stdin, NULL);
    tty_client = (struct vty_client *)malloc(sizeof(struct vty_client));
    memset(tty_client, 0, sizeof(struct vty_client));
    tty_client->flags = 0;
    tty_client->readfd = fileno(stdin);
    tty_client->writefd = fileno(stdout);
    tty_client->next = NULL;
    conns = tty_client;
  }

  sock = socket(PF_INET6, SOCK_STREAM, 0);
  if (sock < 0) {
    log_fatal_perror("vty: socket");
    return -1;
  }

  if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) {
    log_fatal_perror("vty: setsockopt");
    goto abort;
  }

  if (bind(sock, (struct sockaddr *)&si_me, sizeof(si_me)) < 0) {
    log_fatal_perror("vty: bind");
    goto abort;
  }

  if (listen(sock, 2) < 0) {
    log_fatal_perror("vty: listen");
    goto abort;
  }

  return 0;
 abort:
  close(sock);
  sock = -1;
  return -1;
}

int vty_add_fds(fd_set *fds) {
  int maxfd;
  struct vty_client *cur;
  if (sock >= 0) FD_SET(sock, fds);
  maxfd = sock;

  for (cur = conns; cur != NULL; cur = cur->next) {
    if (cur->flags & VTY_REMOVE_PENDING) continue;
    FD_SET(cur->readfd, fds);
    maxfd = max(maxfd, cur->readfd);
  }
  return maxfd;
}

static void vty_print_string(struct vty_client *c, const char *fmt, ...) {
  char buf[1024];
  int len;
  va_list ap;
  va_start(ap, fmt);
  len = vsnprintf(buf, 1024, fmt, ap);
  len = write(c->writefd, buf, len);
}

static void prompt(struct vty_client *c) {
  vty_print_string(c, prompt_str);
}

static void vty_new_connection() {
  char addr_buf[512];
  struct vty_client * c = malloc(sizeof(struct vty_client));
  socklen_t len;
  if (c == NULL) return;

  len = sizeof(struct sockaddr_in6);
  c->readfd = c->writefd = accept(sock, (struct sockaddr *)&c->ep, &len);
  if (c->readfd < 0) {
    error("Accept failed!\n");
    log_fatal_perror(0);
    free(c);
    return;
  }
  c->buf_off = 0;
  memset(c->buf, 0, sizeof(c->buf));

  inet_ntop(AF_INET6, c->ep.sin6_addr.s6_addr, addr_buf, 512);
  info("VTY: new connection accepted from %s\n", addr_buf);
  vty_print_string(c, "Welcome to the blip console!\r\n");
  vty_print_string(c, " type 'help' to print the command options\r\n");
  prompt(c);
  c->flags = 0;
  c->next = conns;
  conns = c;
}

void vty_close_connection(struct vty_client *c) {
  close(c->readfd);
  if (c->readfd != c->writefd) close(c->writefd);
  c->flags |= VTY_REMOVE_PENDING;
}


void vty_dispatch_command(struct vty_client *c) {
  int i;

  if (c->argc > 0) {
    for (i = 0; i < cmd_tab->n; i++) {
      if (strncmp(c->argv[0], cmd_tab->table[i].name, 
                  strlen(cmd_tab->table[i].name) + 1) == 0) {
        cmd_tab->table[i].fn(c->writefd, c->argc, c->argv);
        break;
      }
      
      if (strncmp(c->argv[0], "quit", 4) == 0) {
        vty_close_connection(c);
        return;
      }
    }
    if (i == cmd_tab->n) {
      vty_print_string(c, "vty: %s: command not found\r\n", c->argv[0]);
    }
  }
  prompt(c);
}

void vty_handle_data(struct vty_client *c) {
  int len, i;
  char addr_buf[512];
  bool prompt_pending = FALSE;
  len = read(c->readfd, c->buf[0] + c->buf_off, sizeof(c->buf) - c->buf_off);
  if (len <= 0) {
    inet_ntop(AF_INET6, c->ep.sin6_addr.s6_addr, addr_buf, 512);
    warn("Invalid read on connection from %s: closing\n", addr_buf);
    vty_close_connection(c);
  }
  c->buf_off += len;
  // try to scan the whole line we're building up and remove/process
  // any telnet escapes in there
  
  for (i = 0; i < c->buf_off; i++) {
    int escape_len;

    if (c->buf[0][i] == 255) {
      escape_len = 2;
      // process and remove a command;
      // the command code is in buf[i+1]
      switch (c->buf[0][i+1]) {
      case TELNET_INTERRUPT:
        // ignore the command buffer we've accumulated
        memmove(&c->buf[0][0], &c->buf[0][i+2], c->buf_off - i - 2);
        c->buf_off -= i + 2;
        i = -1;
        prompt_pending = TRUE;
        continue;
      }
      if (c->buf[0][i+1] >= 250) {
        unsigned char response[3];
        // we don't do __anything__
        response[0] = 255;
        response[1] = TELNET_WONT;
        response[2] = c->buf[0][i+2];
        len = write(c->writefd, response, 3);
        escape_len++;
      }

      // this isn't like the most efficient parser ever, but since we
      // don't ask for anything and reply to everyone with DONT it seems okay.
      memmove(&c->buf[0][i], &c->buf[0][i+escape_len], 
              c->buf_off - (i + escape_len));
      c->buf_off -= escape_len;
      // restart processing at the same place
      i--;
    } else if (c->buf[0][i] == 4) {
      // EOT : make C-d work
      vty_close_connection(c);
    }
    // technically, clients are supposed to send \r\n as a newline.
    // however, the client in busybox (an important one) doesn't
    // escape the terminal input and so only sends \n.
    if (/* i > 0 && c->buf[i-1] == '\r' && */ c->buf[0][i] == '\n') {

      if (!(i <= 1 && (c->buf[0][i] == '\n' || c->buf[0][i] == '\r'))) {
        c->buf[0][i] = '\0';
        memcpy(c->buf[1], c->buf[0], i + 1);
        init_argv((char *)c->buf[1], i + 1, c->argv, &c->argc);
      }

      prompt_pending = FALSE;
      vty_dispatch_command(c);

      // start a new command at the head of the buffer
      memmove(&c->buf[0][0], &c->buf[0][i+1], c->buf_off - i - 1);
      c->buf_off -= i + 1;
      i = -1;
    }
  }

  
  if (prompt_pending) {
    vty_print_string(c, "\r\n");
    prompt(c);
    prompt_pending = FALSE;
  }
}

int vty_process(fd_set *fds) {
  struct vty_client *prev, *cur, *next;
  if (sock >= 0 && FD_ISSET(sock, fds)) {
    vty_new_connection();
  }
  for (cur = conns; cur != NULL; cur = cur->next) {
    if (FD_ISSET(cur->readfd, fds)) {
      vty_handle_data(cur);
    }
  }

  prev = NULL;
  cur = conns;
  while (cur != NULL) {
    next = cur->next;
    if (cur->flags & VTY_REMOVE_PENDING) {
      char addr_buf[512];
      inet_ntop(AF_INET6, cur->ep.sin6_addr.s6_addr, addr_buf, 512);
      info("VTY: removing connection with endpoint %s\n", addr_buf);
      free(cur);
      if (cur == conns) conns = next;
      if (prev != NULL) prev->next = next;
    } else {
      prev = cur;
    }
    cur = next;
  }

  return 0;
}

void vty_shutdown() {
  struct vty_client *cur = conns, *next;
  if (sock >= 0) close(sock);

  while (cur != NULL) {
    next = cur->next;
    if (!(cur->flags & VTY_REMOVE_PENDING)) {
      close(cur->readfd);
      if (cur->readfd != cur->writefd) close(cur->writefd);
    }
    free(cur);
    cur = next;
  }
}
