/*************************************************************************/
/* Copyright (C) 2007 by Debarshi Ray                                    */
/*                                                                       */
/* GNU Inetutils 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, or (at your option)   */
/* any later version.                                                    */
/*                                                                       */
/* GNU Inetutils is distributed in the hope that it will be useful,      */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of        */
/* MERCHANTABILITY or FITNESS FOR 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 GNU Inetutils; see the file COPYING.  If not, write        */
/* to the Free Software Foundation, Inc., 51 Franklin Street,            */
/* Fifth Floor, Boston, MA 02110-1301 USA                                */
/*                                                                       */
/* Compile:                                                              */
/*   $ gcc gopher.c -lreadline -ltermcap                                 */
/*************************************************************************/


#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <errno.h>
#include <fcntl.h>
#include <memory.h>
#include <netdb.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <readline/readline.h>

struct _cmd
{
  char *c_name;                      /* name of command */
  char *c_help;                      /* help string */
  bool c_conn;                       /* must be connected to use command */
  void (*c_handler) (int, char **);  /* command handler */
};
typedef struct _cmd cmd;

void cmdscanner (void);
int fillbuffer (int sd);
int findeol (char *, int, int);
cmd *getcmd (char *);
void makeargv (void);

void list_handler (int, char **);
void open_handler (int, char **);
void quit_handler (int, char **);

enum {
  CHUNK = 1024
};

char *default_prompt = "gopher> ";

char listhelp[] = "";
char openhelp[] = "";
char quithelp[] = "";

cmd cmdtab[] = {
  {"ls", listhelp, true, list_handler},
  {"open", openhelp, false, open_handler},
  {"quit", quithelp, false, quit_handler}
};

bool connected = false;
char *buffer = NULL;
char *line = NULL;
char *margv[20];
char *prompt = NULL;
int margc = 0;
int nbuffer = 0;
struct hostent *host = NULL;

#define NCMDS (sizeof (cmdtab) / sizeof (cmdtab[0]))
#define NMARGV (sizeof (margv) / sizeof (margv[0]))

int
main (int argc, char *argv[])
{
  prompt = default_prompt;

  for (;;)
    cmdscanner ();

  return 0;
}

void
cmdscanner (void)
{
  cmd *c;

  if (line)
    {
      free (line);
      line = NULL;
    }

  line = readline (prompt);
  if (!line)
    quit_handler (margc, margv);

  if (*line)
    add_history (line);

  makeargv();
  if (margc == 0)
    return;

  c = getcmd (margv[0]);
  if (c == (cmd *) -1)
    {
      fputs ("?Ambiguous command\n", stderr);
      return;
    }
  else if (c == NULL)
    {
      fputs ("?Invalid command\n", stderr);
      return;
    }
  else if (c->c_conn && !connected)
    {
      fputs ("Not connected.\n", stderr);
      return;
    }

  (*c->c_handler) (margc, margv);

  return;
}

int
fillbuffer (int sd)
{
  int nread;
  int tread = 0;

  if (buffer)
    {
      free (buffer);
      buffer = NULL;
      nbuffer = 0 ;
    }

  nbuffer = CHUNK;
  buffer = (char *) malloc (sizeof (char) * nbuffer);
  if (buffer == NULL)
    return -1;

  for (;;)
    {
      nread = read (sd, buffer + tread, CHUNK);
      if (nread == 0)
        break;
      tread += nread;
      if (tread > nbuffer - CHUNK) /* There should be space to read the */
        {                          /* next CHUNK characters.            */
          nbuffer += 2 * CHUNK;
          buffer = (char *) realloc (buffer, sizeof (char) * nbuffer);
          if (buffer == NULL)
            return -1;
        }
    }

  return tread;
}

int
findeol (char *str, int size, int offset)
{
  int i;

  for (i = offset; i < size - 1; i++)
    {
      if (str[i] == '\r' && str[i+1] == '\n')
        return i;
    }

  return -1;
}

cmd *
getcmd (char *name)
{
  int i;
  int match;
  int nmatches = 0;

  for (i = 0; i < NCMDS; i++)
    {
      /* Is it an exact match? */
      if (strcmp (name, cmdtab[i].c_name) == 0)
        return &cmdtab[i];

      /* Or is it a prefix? */
      else if (strncmp (name, cmdtab[i].c_name, strlen (name)) == 0)
        {
          match = i;
          nmatches++;
        }
    }

  if (nmatches == 1)
    return &cmdtab[match];

  if (nmatches > 1)
    return (cmd *) -1;

  return NULL;
}

void
makeargv (void)
{
  int base;
  int i;
  int len;
  int lenmarg;

  for (i = 0; i < margc; i++)
    {
      free (margv[i]);
      margv[i] = NULL;
    }

  len = strlen (line);

  base = 0;
  margc = 0;
  for (i = 0; i < len; i++)
    {
      if (line[i] == ' ')
        {
          if (margc == NMARGV)
            break;
          lenmarg = i - base;
          margv[margc] = (char *) malloc (sizeof (char) * (lenmarg + 1));
          strncpy (margv[margc], line + base, lenmarg);
          margv[margc][lenmarg] = '\0';
          margc++;
          base = i + 1;
        }
    }

  if (margc < NMARGV && len > 0)
    {
      lenmarg = i - base;
      margv[margc] = (char *) malloc (sizeof (char) * (lenmarg + 1));
      strncpy (margv[margc], line + base, lenmarg);
      margv[margc][lenmarg] = '\0';
      margc++;
      base = i + 1;
    }

  return;
}

void
list_handler (int argc, char *argv[])
{
  int base;
  int i;
  int offset;
  int sd;
  int status;
  int tread;
  struct sockaddr_in addr;

  if (!connected)
    {
      fputs ("Not connected\n", stderr);
      return;
    }

  sd = socket (PF_INET, SOCK_STREAM, 0);
  if (sd == -1)
    quit_handler (argc, argv);

  memset ((void *) &addr, '\0', sizeof (addr));
  addr.sin_family = AF_INET;
  addr.sin_port = htons (70);

  memmove ((void *) &addr.sin_addr, (void *) host->h_addr, host->h_length);

  if (connect (sd, (struct sockaddr *) &addr, sizeof (addr)) == -1)
    quit_handler (argc, argv);

  write (sd, "\r\n", 2);

  tread = fillbuffer (sd);
  if (tread == -1)
    quit_handler (argc, argv);

  close (sd);

  base = 0;
  for (;;)
    {
      offset = findeol (buffer, tread, base);
      if (offset == -1)
        break;

      if (buffer[base] == 'i')
        {
          for (i = base + 1; i < offset; i++)
            {
              if (buffer[i] == '\t')
                {
                  putchar ('\n');
                  break;
                }
              putchar (buffer[i]);
            }
        }
      else if (buffer[base] == '0')
        {
          for (i = base + 1; i < offset; i++)
            {
              if (buffer[i] == '\t')
                {
                  putchar ('\n');
                  break;
                }
              putchar (buffer[i]);
            }
        }
      else if (buffer[base] == '1')
        {
          for (i = base + 1; i < offset; i++)
            {
              if (buffer[i] == '\t')
                {
                  putchar ('/');
                  putchar ('\n');
                  break;
                }
              putchar (buffer[i]);
            }
        }

      base = offset + 2;
    }

  return;
}

void
open_handler (int argc, char *argv[])
{
  if (argc < 2)
    return;

  host = gethostbyname (argv[1]);
  if (host == NULL)
    {
      fprintf (stderr, "%s: Unknown host\n", argv[1]);
      return;
    }

  connected = true;
}

void
quit_handler (int argc, char *argv[])
{
  int i;

  if (buffer)
    {
      free (buffer);
      buffer = NULL;
    }

  if (line)
    {
      free (line);
      line = NULL;
    }

  for (i = 0; i < argc; i++)
    {
      free (argv[i]);
      argv[i] = NULL;
    }

  exit (EXIT_SUCCESS);
}