/*
 * Multifile Browser Copyright (c) 2020-2025, James Bailie.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 *     * The name of James Bailie may not be used to endorse or promote
 * products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/types.h>
#include <sys/param.h>
#include <sys/event.h>

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>

#include <db.h>
#include <term.h>
#include <curses.h>
#include <termios.h>
#include <libgen.h>

#include "frameclient.h"

struct fc_ssl *conn;
char *hostname, *port = "8000";

DB *db = NULL;

char *af, *ab, *ce, *cm, *sc, *sf, *sr, *rc, *cl, *vi, *ve;

int nlines = 0, ncols = 0, offset = 0, r = 0, receiving = -1,
    first = 1, is_text = 0, is_help = 0, ignore = 0;

recno_t y = 1;

unsigned char search_term[ 32 ], filename[ MAXPATHLEN + 1 ];
unsigned int search_len = 0, search_dir = 0, total = 0, count = 0, namelen = 0;

struct termios canon_termios;
volatile int sigwinch = 0;

#define DIRS 64

struct
{
   unsigned char name[ MAXPATHLEN ];
   unsigned int len;
} directories[ DIRS ];

int directory = 0;

int db_open();
void db_close();
void start();

void set_channels( unsigned int len, unsigned char *data )
{
   unsigned char *ptr;

   for( ptr = data; len && *ptr; ++ptr, --len )
      if ( *ptr == ':' )
         break;

   if ( ! len || ! *ptr )
   {
      fprintf( stderr, "set_channels(): channel limit not present in server advertisement: %s.\n", data );
      exit( 1 );
   }

   if ( ! strtol(( const char *)++ptr, NULL, 10 ))
   {
      fputs( "set_channels(): server advertizes channel limit of 0.\n", stderr );
      exit( 1 );
   }
}

void sigwinch_handler( int signo )
{
   ++sigwinch;
}

void init_term()
{
   struct termios termios;

   if ( setupterm( NULL, STDOUT_FILENO, NULL ) != OK )
      return;

   af = tigetstr( "setaf" );
   ab = tigetstr( "setab" );
   ce = tigetstr( "el" );
   cm = tigetstr( "cup" );
   sf = tigetstr( "ind" );
   sr = tigetstr( "ri" );
   sc = tigetstr( "sc" );
   rc = tigetstr( "rc" );
   cl = tigetstr( "clear" );
   vi = tigetstr( "civis" );
   ve = tigetstr( "cnorm" );

   if ( af == ( char *)-1 || ce == ( char *)-1 || cm == ( char *)-1 || sf == ( char *)-1 ||
        sr == ( char *)-1 || sc == ( char *)-1 || rc == ( char *)-1 || ab == ( char *)-1 ||
        cl == ( char *)-1 || vi == ( char *)-1 || ve == ( char *)-1 )
   {
      fputs( "init_term(): terminal lacks needed capability.\n", stderr );
      exit( 1 );
   }

   if ( tcgetattr( 0, &canon_termios ) < 0 )
   {
      fprintf( stderr, "init_term: tcgetattr: %s.\n", strerror( errno ));
      exit( 1 );
   }

   termios = canon_termios;
   cfmakeraw( &termios );
   termios.c_cc[ VMIN ] = 1;
   termios.c_cc[ VTIME ] = 0;

   if ( tcsetattr( 0, TCSANOW, &termios ) < 0 )
   {
      fprintf( stderr, "init_term(): tcsetattr: %s.\n", strerror( errno ));
      exit( 1 );
   }

   signal( SIGWINCH, sigwinch_handler );
   putp( vi );
   fflush( stdout );
}

int screensize()
{
   struct winsize winsize;

   if ( ioctl( STDOUT_FILENO, TIOCGWINSZ, &winsize ) < 0 )
   {
      fprintf( stderr, "screensize(): %s.\n", strerror( errno ));
      return 1;
   }

   nlines = winsize.ws_row;
   ncols  = winsize.ws_col;

   return 0;
}

void deinit()
{
   db_close();
   screensize();

   putp( ve );
   putp( tgoto( cm, 0, nlines - 1 ));
   fflush( stdout );

   if ( tcsetattr( 0, TCSANOW, &canon_termios ) < 0 )
      fprintf( stderr, "deinit(): tcsetattr: %s.\n", strerror( errno ));
}

void mf_connect()
{
   unsigned char buffer[ fc_max_frame ];
   unsigned int len;

   if (( conn = fc_connect_to_server( hostname, port, ignore )) == NULL )
   {
      fputs( fc_error, stderr );
      exit( 1 );
   }

   if (( len = fc_read_frame( conn, buffer )) <= 0 )
   {
      if ( len < 0 )
         fputs( fc_error, stderr );

      exit( 1 );
   }

   set_channels( len, buffer );
}

void init()
{
   if ( db_open() )
      exit( 1 );

   bzero( directories, sizeof( directories ));
   init_term();
   atexit( deinit );
}

int db_open()
{
   void *ptr;
   mode_t mode;

   if (( ptr = setmode( "0600" )) == NULL )
   {
      fprintf( stderr, "db_open(): setmode(): %s.\n", strerror( errno ));
      return 1;
   }

   mode = getmode( ptr, 0 );
   free( ptr );

   if (( db = dbopen( NULL, O_EXCL | O_EXLOCK | O_RDWR | O_CREAT, mode, DB_RECNO, NULL )) == NULL )
   {
      fprintf( stderr, "db_open(): %s.\n", strerror( errno ));
      return 1;
   }

   return 0;
}

void db_close()
{
   db->close( db );
}

int db_insert( recno_t lineno, unsigned char *line, unsigned int len )
{
   DBT key, value;

   key.data = &lineno;
   key.size = sizeof( lineno );

   value.data = line;
   value.size = len;

   if ( db->put( db, &key, &value, R_SETCURSOR ) < 0 )
   {
      fprintf( stderr, "db_insert(): db->put(): %s.\n", strerror( errno ));
      return 1;
   }

   return 0;
}

recno_t db_lastline()
{
   DBT key, value;
   int result;

   if (( result = db->seq( db, &key, &value, R_LAST )) == -1 )
      fprintf( stderr, "db_lastline(): db->seq: %s.\n", strerror( errno ));
   else if ( result == 1 )
      return 0;

   return *( recno_t *)key.data;
}

void db_clear()
{
   DBT key;
   recno_t idx;

   idx = 1;
   key.data = &idx;
   key.size = sizeof( idx );

   while( ! db->del( db, &key, 0 ))
      ;
}

int db_retrieve( recno_t lineno, unsigned char **line, unsigned int *len )
{
   DBT key, value;
   int result = 1;

   key.data = &lineno;
   key.size = sizeof( lineno );

   if (( result = db->get( db, &key, &value, 0 )) < 0 )
      fprintf( stderr, "db_retrieve(): db->get: %s.\n", strerror( errno ));

   else if ( result == 1 )
      fprintf( stderr, "db_retrieve(): db->get: key does not exist: %u.\n", lineno );
   else
   {
      *line = value.data;
      *len  = value.size;
      result = 0;
   }

   return result;
}

unsigned int utf8_len( unsigned char *str, unsigned int clen )
{
   unsigned int len;
   unsigned char *ptr;

   for( len = 0, ptr = str; clen > 0; ++ptr, --clen )
      if (( *ptr & 0xC0 ) != 0x80 )
         ++len;

   return len;
}

unsigned char *utf8_offset( unsigned char *data, recno_t size, unsigned int *len )
{
   unsigned char *ptr;
   int i, n;

   for( i = 0, ptr = data; size > 0 && i < offset; )
   {
      ++i;

      if (( n = *ptr & 0xC0 ) < 0xC0 )
      {
         ++ptr;
         --size;
         continue;
      }

      if (( n = *ptr & 0xE0 ) == 0xC0 )
      {
         ptr += 2;
         size -= 2;
         continue;
      }

      if (( n = *ptr & 0xF0 ) == 0xE0 )
      {
         ptr += 3;
         size -= 3;
         continue;
      }

      ptr += 4;
      size -= 4;
   }

   if ( size <= 0 )
      return NULL;

   if ( len != NULL )
      *len = i + utf8_len( ptr, size );

   return ptr;
}

unsigned char *print_utf8_char( unsigned char *ptr )
{
   int n, inc;

   if (( n = *ptr & 0xC0 ) < 0xC0 )
      inc = 1;
   else if (( *ptr & 0xE0 ) == 0xC0 )
      inc = 2;
   else if (( *ptr & 0xF0 ) == 0xE0 )
      inc = 3;
   else
      inc = 4;

   fwrite( ptr, 1, inc, stdout );
   return ptr + inc;
}

void print_line( unsigned int y, int highlight )
{
   unsigned char *line, *ptr;
   unsigned int len, n, utf8len, color = 0;

   if ( db_retrieve( y, &line, &len ) || ! len )
      return;

   if (( ptr = utf8_offset( line, len, &utf8len )) == NULL )
      return;

   if ( ! is_text && ! highlight && line[ len - 1 ] == '/' )
   {
      putp( tparm( af, COLOR_GREEN ));
      ++color;
   }

   for( n = 0, utf8len -= offset; utf8len > 0 && n < ncols; ++n, --utf8len )
      ptr = print_utf8_char( ptr );

   if ( color )
   {
      putp( tparm( af, COLOR_WHITE ));
      putp( tparm( ab, COLOR_BLACK ));
   }
}

void print_highlighted()
{
   putp( tgoto( cm, 0, r ));
   putp( ce );
   putp( tparm( af, COLOR_YELLOW ));
   print_line( y, 1 );
   putp( tparm( af, COLOR_WHITE ));
   putp( tgoto( cm, 0, r ));
}

int screenful( recno_t idx )
{
   unsigned int i;
   recno_t last;

   if ( screensize())
      return 1;

   putp( cl );

   for( i = 0, last = db_lastline(); idx <= last && i < nlines; ++idx, ++i )
   {
      putp( tgoto( cm, 0, i ));
      putp( ce );
      print_line( idx, 0 );
   }

   while( i < nlines )
   {
      putp( tgoto( cm, 0, i++ ));
      putp( ce );
      putchar( '~' );
   }

   if ( last )
      print_highlighted();
   else
      putp( tgoto( cm, 0, 0 ));

   fflush( stdout );
   return 0;
}

int validate_utf8( unsigned char *ptr, unsigned int len )
{
   while( len )
   {
      unsigned char n;

      if ( ! *ptr )
         goto ERROR;

      if (( n = ( *ptr & 0xC0 )) < 0xC0 )
      {
         if ( n == 0x80 )
            goto ERROR;

         --len;
         ++ptr;
         continue;
      }

      else if (( *ptr & 0xE0 ) == 0xC0 )
      {
         if ( len < 2 || ( ptr[ 1 ] & 0xC0 ) != 0x80 )
            goto ERROR;

         len -= 2;
         ptr += 2;
         continue;
      }

      else if (( *ptr & 0xF0 ) == 0xE0 )
      {
         if ( len < 3 || ( ptr[ 1 ] & 0xC0 ) != 0x80 || ( ptr[ 2 ] & 0xC0 ) != 0x80 )
            goto ERROR;

         len -= 3;
         ptr += 3;
         continue;
      }

      else if (( *ptr & 0xF8 ) != 0xF0 )
         goto ERROR;

      if ( len < 4 || ( ptr[ 1 ] & 0xC0 ) != 0x80 || ( ptr[ 2 ] & 0xC0 ) != 0x80 || ( ptr[ 3 ] & 0xC0 ) != 0x80 )
         goto ERROR;

      len -= 4;
      ptr += 4;
   }

   return 0;

ERROR:
   fputs( "validate_utf8(): bad UTF-8.\n", stderr );
   return 1;
}

int convert_date( unsigned char *data, unsigned int len, unsigned int *outlen )
{
   int path = 0;
   time_t t;
   struct tm *lt;
   unsigned int mlen, olen, idx;
   unsigned char dbuf[ MAXPATHLEN + 128 ], *ptr1, *ptr2;

   for( ptr1 = data, mlen = len, olen = 0; mlen; ++ptr1, --mlen, ++olen )
      if ( *ptr1 == ':' )
         break;

   if ( ! mlen )
   {
      fputs( "convert_date(): no modification time found in directory entry.\n", stderr );
      return 1;
   }

   t = strtol(( char *)data, NULL, 10 );
   lt = localtime( &t );
   idx = strftime(( char *)&dbuf, sizeof( dbuf ), "%d %b %Y %H:%M", lt );

   for( ptr1 = &data[ olen ], ptr2 = &dbuf[ idx ], mlen = len - olen, olen = 0;
        mlen;
        ++ptr1, ++ptr2, --mlen )
   {
      ++olen;

      if ( ! path )
      {
         if ( *ptr1 == '/' )
            ++path;
         else if ( *ptr1 == ':' )
         {
            *ptr2 = '\t';
            continue;
         }
      }

      *ptr2 = *ptr1;
   }

   olen += idx;
   *outlen = olen;

   ptr1 = data;
   ptr2 = dbuf;

   while( olen-- )
      *ptr1++ = *ptr2++;

   return 0;
}

int retrieve_directory( unsigned char *dir, unsigned int dlen )
{
   unsigned int len;
   unsigned char buffer[ fc_max_frame ], idx;
   recno_t lineno;

   if ( dlen > MAXPATHLEN )
   {
      beep();
      fflush( stdout );
      return 0;
   }

   bcopy( dir, buffer, dlen );
   buffer[ dlen ] = '\0';

   if (( len = fc_send_frame( conn, buffer, dlen + 1 )))
   {
      if ( len == -1 )
         fputs( fc_error, stderr );

      if ( len == -2 )
         return 2;

      return 1;
   }

   if (( len = fc_read_frame( conn, buffer )) <= 0 )
   {
      if ( len == -1 )
         fputs( fc_error, stderr );

      if ( ! len )
         return 2;

      return 1;
   }

   if ( !( strtol(( char *)&buffer[ 1 ], NULL, 10 )))
   {
      putp( tgoto( cm, 0, r ));
      putp( tparm( af, COLOR_YELLOW ));
      fputs( "[ server could not access entity ]", stdout );
      putp( tparm( af, COLOR_WHITE ));
      fflush( stdout );
      return 0;
   }

   db_clear();
   count = 0;
   lineno = 0;

   for( ; ; )
   {
      if (( len = fc_read_frame( conn, buffer )) <= 0 )
      {
         if ( len == -1 )
            fputs( fc_error, stderr );

         if ( ! len )
            return 2;

         return 1;
      }

      if ( !( idx = buffer[ 0 ] ))
         break;

      putp( tgoto( cm, 0, 0 ));
      putp( tparm( af, COLOR_YELLOW ));
      fprintf( stdout, "[ %u ]", ++count );
      putp( tparm( af, COLOR_WHITE ));
      fflush( stdout );

      if ( validate_utf8( &buffer[ 1 ], len - 2 ) || convert_date( &buffer[ 1 ], len - 2, &len ) ||
           db_insert( ++lineno, &buffer[ 1 ], len ))
         return 1;
   }

   return 0;
}

void scroll_up()
{
   putp( sc );
   putp( tgoto( cm, 0, nlines - 1 ));
   putp( sf );
   putp( rc );
}

void scroll_down()
{
   putp( sc );
   putp( tgoto( cm, 0, 0 ));
   putp( sr );
   putp( rc );
}

void forw_line()
{
   recno_t last;

   if ( !( last = db_lastline()) || y == last )
      return;

   putp( tgoto( cm, 0, r ));
   putp( ce );
   print_line( y, 0 );

   ++y;

   if ( r < nlines - 1 )
   {
      ++r;
      print_highlighted();
   }
   else
   {
      scroll_up();
      print_highlighted();
   }

   putp( tgoto( cm, 0, r ));
   fflush( stdout );
}

void back_line()
{
   if ( y == 1 )
      return;

   putp( tgoto( cm, 0, r ));
   putp( ce );
   print_line( y, 0 );

   --y;

   if ( r )
   {
      --r;
      putp( tgoto( cm, 0, r ));
      putp( ce );
      print_highlighted();
   }
   else
   {
      scroll_down();
      putp( tgoto( cm, 0, nlines - 1 ));
      putp( tgoto( cm, 0, r ));
      putp( ce );
      print_highlighted();
   }

   putp( tgoto( cm, 0, r ));
   fflush( stdout );
}

void forw_screen()
{
   unsigned int n;

   for( n = 0; n < nlines - 1; ++n )
      forw_line();
}

void back_screen()
{
   unsigned int n;

   for( n = 0; n < nlines - 1; ++n )
      back_line();
}

int forw_char()
{
   ++offset;

   if ( screenful( y - r ))
      return 1;

   return 0;
}

int back_char()
{
   if ( ! offset )
      return 0;

   --offset;

   if ( screenful( y - r ))
      return 1;

   return 0;
}

int goto_match_forw( unsigned char *key, unsigned int keylen )
{
   unsigned char *line, *ptr1, *ptr2;
   unsigned int len1, len2;
   recno_t idx, last;

   if ( !( last = db_lastline()))
      goto EMPTY;

   for( idx = y + 1; idx <= last; ++idx )
   {
      if ( db_retrieve( idx, &line, &len1 ))
         return 1;

      ptr1 = line;

AGAIN:
      while( len1 && *ptr1 != *key )
      {
         --len1;
         ++ptr1;
      }

      if ( ! len1 )
         continue;

      ptr2 = key;
      len2 = keylen;

      while( len1 && len2 && *ptr1 == *ptr2 )
      {
         --len1;
         --len2;
         ++ptr1;
         ++ptr2;
      }

      if ( len2 )
      {
         if ( len1 )
            goto AGAIN;

         continue;
      }

      while( y != idx )
         forw_line();

      return 0;
   }

EMPTY:
   beep();
   fflush( stdout );
   return 0;
}

int goto_match_back( unsigned char *key, unsigned int keylen )
{
   unsigned char *line, *ptr1, *ptr2;
   unsigned int len1, len2;
   recno_t idx, last;

   if ( !( last = db_lastline()))
      goto EMPTY;

   for( idx = y - 1; idx > 0; --idx )
   {
      if ( db_retrieve( idx, &line, &len1 ))
         return 1;

      ptr1 = line;

AGAIN:
      while( len1 && *ptr1 != *key )
      {
         --len1;
         ++ptr1;
      }

      if ( ! len1 )
         continue;

      ptr2 = key;
      len2 = keylen;

      while( len1 && len2 && *ptr1 == *ptr2 )
      {
         --len1;
         --len2;
         ++ptr1;
         ++ptr2;
      }

      if ( len2 )
      {
         if ( len1 )
            goto AGAIN;

         continue;
      }

      while( y != idx )
         back_line();

      return 0;
   }

EMPTY:
   beep();
   fflush( stdout );
   return 0;
}

int search( int forw )
{
   char c;
   int i;
   unsigned char *ptr;

   putp( tgoto( cm, 0, nlines - 1 ));
   putp( ce );
   putp( ve );
   fputc(( forw ? '/' : '?' ), stdout );
   fflush( stdout );

   search_dir = forw;
   search_len = 0;
   ptr = search_term;

   while(( i = read( STDIN_FILENO, &c, 1 )) > 0 )
   {
      switch( c )
      {
         case '\r':
            goto EXIT;

         /* C-h, Backspace */
         case 8:
            if ( search_len )
            {
               --ptr;
               --search_len;
            }
            break;

         /* C-u, C-w */
         case 21:
         case 23:
            search_len = 0;
            ptr = search_term;
            break;

         default:
            *ptr++ = c;
            ++search_len;
      }

      putp( tgoto( cm, 0, nlines - 1 ));
      putp( ce );
      fputc(( forw ? '/' : '?' ), stdout );
      fwrite( search_term, search_len, 1, stdout );
      fflush( stdout );

      if ( search_len == sizeof( search_term ))
         break;
   }

EXIT:
   putp( tgoto( cm, 0, nlines - 1 ));
   putp( ce );
   putp( vi );
   fflush( stdout );

   if ( search_len )
      return ( forw ? goto_match_forw( search_term, search_len ) :
                      goto_match_back( search_term, search_len ));

   if ( i < 0 )
      fprintf( stderr, "search(): read( STDIN_FILENO ): %s.\n", strerror( errno ));

   if ( ! i )
      return 1;

   beep();
   fflush( stdout );
   return 0;
}

int next_match()
{
   if ( ! search_len )
   {
      beep();
      fflush( stdout );
      return 0;
   }

   return ( search_dir ? goto_match_forw( search_term, search_len ) :
                         goto_match_back( search_term, search_len ));
}

int previous_match()
{
   if ( ! search_len )
   {
      beep();
      fflush( stdout );
      return 0;
   }

   return ( search_dir ? goto_match_back( search_term, search_len ) :
                         goto_match_forw( search_term, search_len ));
}

int next_dir()
{
   unsigned int len, once;
   unsigned char *line;
   recno_t last, idx;

   if ( is_text || !( last = db_lastline()))
      return 0;

   once = 0;
   idx = ( y < last ? y : 0 );

AGAIN:
   do
   {
      ++idx;

      if ( db_retrieve( idx, &line, &len ))
         return 1;

      if ( len && line[ len - 1 ] == '/' )
         break;
   }
   while( idx < last );

   if ( len && line[ len - 1 ] == '/' )
   {
      while( y < idx )
         forw_line();

      while( y > idx )
         back_line();

      return 0;
   }

   if ( ++once < 2 )
   {
      idx = 0;
      goto AGAIN;
   }

   return 0;
}

int compare( unsigned char *ptr1, unsigned int len1, unsigned char *ptr2, unsigned int len2 )
{
   while( len1 && len2 && *ptr1 == *ptr2 )
   {
      ++ptr1;
      ++ptr2;

      --len1;
      --len2;
   }

   if ( ! len1 && ! len2 )
      return 1;

   return 0;
}

int push_directory( unsigned char *line, unsigned int len )
{
   int rs;

   if ( directory == DIRS )
   {
      beep();
      fflush( stdout );
      return 0;
   }

   while( len && *line != '/' )
   {
      --len;
      ++line;
   }

   if ( ! len )
      return 0;

   if ( ! directory )
   {
      if ( len == 1 && *line == '/' )
         return 0;
   }
   else if ( compare( line, len, directories[ directory - 1 ].name, directories[ directory - 1 ].len ))
      return 0;

   bcopy( line, directories[ directory ].name, len );
   directories[ directory ].len = len;

   if (( rs = retrieve_directory( directories[ directory ].name, directories[ directory ].len )))
      return rs;

   ++directory;

   r = 0;
   y = 1;

   return screenful( 1 );
}

int text_pop_directory()
{
   int rs = 0;

   is_text = is_help = 0;

   if ( ! directory )
      rs = retrieve_directory(( unsigned char *)"/", 1 );
   else
      rs = retrieve_directory( directories[ directory - 1 ].name, directories[ directory - 1 ].len );

   if ( rs )
      return rs;

   r = 0;
   y = 1;

   if ( screenful( 1 ))
      return 1;

   if ( namelen )
      rs = goto_match_forw( filename, namelen );

   return rs;
}

int pop_directory()
{
   int rs;

   if ( is_text )
      return text_pop_directory();

   if ( ! directory )
      return 0;

   if ( ! --directory )
      rs = retrieve_directory(( unsigned char *)"/", 1 );
   else
      rs = retrieve_directory( directories[ directory - 1 ].name, directories[ directory - 1 ].len );

   if ( rs )
      return rs;

   r = 0;
   y = 1;

   if ( screenful( 1 ))
      return 1;

   if ( directory )
      return goto_match_forw( directories[ directory - 1 ].name, directories[ directory - 1 ].len );
   else
      return goto_match_forw( directories[ directory ].name, directories[ directory ].len );
}

unsigned char *get_line( unsigned char **input, unsigned int *len, unsigned int *line_len )
{
   unsigned char *ptr, *start;

   ptr = start = *input;
   *line_len = 0;

   while( *len && *ptr != '\n' )
   {
      --*len;
      ++ptr;
      ++*line_len;
   }

   if ( ! *len && ! *line_len )
      return NULL;

   if ( *len )
   {
      --*len;
      *input = ptr + 1;
   }

   return start;
}

int show_help()
{
   recno_t last;
   unsigned char *ptr, *data;
   unsigned int line_len, lineno;
      static unsigned char *help = ( unsigned char *)"COMMANDS\n"
      "\n"

      "p           - Show help.\n"
      "[Esc]       - Exit help OR Go back.\n"
      "\n"

      "q           - Exit the browser.\n"
      "\n"

      "j           - Move the cursor down.\n"
      "k           - Move the cursor up.\n"
      "\n"

      "[Space]     - Scroll forward.\n"
      "[Backspace] - Scroll backward.\n"
      "\n"

      "h           - Scroll left.\n"
      "l           - Scroll right.\n"
      "\n"

      "[Tab]       - Move the cursor to the next sub-directory.\n"
      "[Enter]     - START or STOP Retrieval of the entity under the cursor.\n"
      "\n"

      "s           - Save a displayed text file.\n"
      "\n"

      "/           - Search (literal, not regexp) forward.\n"
      "?           - Search backward.\n"
      "\n"

      "n           - Repeat the last search.\n"
      "N           - Repeat the last search in the opposite direction.\n"

      "\n";

   if ( is_text )
      return 0;

   is_help = 1;

   if (( last = db_lastline()))
   {
      if ( y == 1 )
         namelen = 0;
      else
      {
         if ( db_retrieve( y, &ptr, &line_len ))
            return 1;

         while( line_len && *ptr != '/' )
         {
            --line_len;
            ++ptr;
         }

         bcopy( ptr, filename, line_len );
         namelen = line_len;
      }
   }

   db_clear();
   is_text = 1;
   lineno = 0;

   for( total = 0, ptr = help; *ptr; ++ptr )
      ++total;

   data = help;

   while(( ptr = get_line( &data, &total, &line_len )) != NULL )
      if ( db_insert( ++lineno, ptr, line_len ))
         return 1;

   y = 1;
   r = 0;

   return screenful( 1 );
}

void load_text()
{
   unsigned char *ptr, *data, *stored;
   unsigned int line_len, lineno;

   if ( lseek( receiving, 0, SEEK_SET ) < 0 )
   {
      fprintf( stderr, "load_text(): lseek(): %s.\n", strerror( errno ));
      return;
   }

   if (( stored = data = malloc( total )) == NULL )
   {
      fprintf( stderr, "load_text(): malloc(): %s.\n", strerror( errno ));
      return;
   }

   if ( read( receiving, stored, total ) <= 0 )
   {
      free( stored );
      fprintf( stderr, "load_text(): read(): %s.\n", strerror( errno ));
      return;
   }

   unlink( basename(( char *)filename ));

   if ( validate_utf8( stored, total ))
   {
      free( stored );
      return;
   }

   db_clear();
   lineno = 0;

   while(( ptr = get_line( &data, &total, &line_len )) != NULL )
   {
      if ( line_len && ptr[ line_len - 1 ] == '\r' )
         --line_len;

      if ( db_insert( ++lineno, ptr, line_len ))
         break;
   }

   free( stored );

   y = 1;
   r = 0;

   screenful( y );
   return;
}

int save_text()
{
   unsigned char *line;
   unsigned int len;
   recno_t idx, last;
   char *ptr;
   mode_t mode;

   if ( ! is_text )
      return 0;

   if (( ptr = setmode( "0600" )) == NULL )
   {
      fprintf( stderr, "save_text(): setmode(): %s.\n", strerror( errno ));
      return 1;
   }

   mode = getmode( ptr, 0 );
   free( ptr );
   ptr = basename(( char *)filename );

   if (( receiving = open( ptr, O_CREAT | O_WRONLY | O_TRUNC, mode )) < 0 )
   {
      fprintf( stderr, "save_text(): open( %s ): %s.\n", ptr, strerror( errno ));
      return 1;
   }

   if ( !( last = db_lastline()))
      goto END;

   for( idx = 1; idx <= last; ++idx )
   {
      if ( db_retrieve( idx, &line, &len ))
         break;

      if ( write( receiving, line, len ) < 0 || write( receiving, "\n", 1 ) < 0 )
      {
         fprintf( stderr, "save_text(): write(): %s.\n", strerror( errno ));
         break;
      }
   }

   putp( tgoto( cm, 0, r ));
   putp( ce );
   putp( tparm( af, COLOR_YELLOW ));
   printf( "[ %s saved ]", filename );
   putp( tparm( af, COLOR_WHITE ));
   fflush( stdout );

END:
   close( receiving );
   receiving = -1;
   return 0;
}

int receive_file()
{
   unsigned int len, result = 1, idx;
   unsigned char buffer[ fc_max_frame ];

   if (( len = fc_read_frame( conn, buffer )) <= 0 )
   {
      if ( len == -1 )
         fputs( fc_error, stderr );

      if ( ! len )
         result = 2;

      goto ERROR;
   }

   if ( !( idx = buffer[ 0 ] ))
      goto FINISH;

   if ( first )
   {
      if ( !( total = strtol(( char *)&buffer[ 1 ], NULL, 10 )))
      {
         putp( tgoto( cm, 0, r ));
         putp( ce );
         putp( tparm( af, COLOR_YELLOW ));
         fputs( "[ server could not access entity ]", stdout );
         putp( tparm( af, COLOR_WHITE ));

         unlink( basename(( char *)filename ));
         is_text = 0;
         goto FINISH;
      }

      --first;
      return 0;
   }

   if ( write( receiving, &buffer[ 1 ], len - 1 ) < 0 )
   {
      fprintf( stderr, "receive_file(): write(): %s.\n", strerror( errno ));
      goto ERROR;
   }

   count += len - 1;

   putp( tgoto( cm, 0, r ));
   putp( ce );
   putp( tparm( af, COLOR_YELLOW ));
   fprintf( stdout, "[ %s: %u of %u ]", filename, count, total );
   putp( tparm( af, COLOR_WHITE ));
   fflush( stdout );
   return 0;

FINISH:
   fflush( stdout );

   if ( is_text )
      load_text();

   close( receiving );
   receiving = -1;
   return 0;

ERROR:
   close( receiving );
   receiving = -1;
   unlink( basename(( char *)filename ));
   is_text = 0;
   return result;
}

void set_text( unsigned char *line, unsigned int len )
{
   unsigned int n, *iptr, lens[] = { 4, 5, 4, 5, 2, 2, 3, 4, 0 };
   unsigned char *ptr1, *ptr2, **ptr,
      *suffixes[] = { ( unsigned char *)".txt", ( unsigned char *)".html",
                      ( unsigned char *)".css", ( unsigned char *)".json",
                      ( unsigned char *)".c",   ( unsigned char *)".h",
                      ( unsigned char *)".js",  ( unsigned char *)".cpp",
                      NULL };

   is_text = 0;

   for( ptr = suffixes, iptr = lens; *ptr != NULL; ++ptr, ++iptr )
   {
      n = *iptr;

      if ( len < n )
         continue;

      ptr1 = &line[ len - n ];
      ptr2 = *ptr;

      while( n && *ptr2 && *ptr1 == *ptr2 )
      {
         ++ptr1;
         ++ptr2;
         --n;
      }

      if ( ! n && ! *ptr2 )
      {
         is_text = 1;
         break;
      }
   }
}

int process_file( unsigned char *line, unsigned int len )
{
   unsigned char *ptr;
   int result = 1, r;
   mode_t mode;

   for( ; len; --len, ++line )
      if ( *line == '/' )
         break;

   if ( ! len || len > MAXPATHLEN )
   {
      beep();
      fflush( stdout );
      return 0;
   }

   for( namelen = 0, ptr = filename; len; ++ptr, ++line, --len )
   {
      *ptr = *line;
      ++namelen;
   }

   filename[ namelen ] = '\0';
   set_text( filename, namelen );

   if (( ptr = setmode( "0600" )) == NULL )
   {
      fprintf( stderr, "process_file(): setmode(): %s.\n", strerror( errno ));
      return 1;
   }

   mode = getmode( ptr, 0 );
   free( ptr );
   ptr = ( unsigned char *)basename(( char *)filename );

   if (( receiving = open(( char *)ptr, O_CREAT | O_RDWR | O_TRUNC, mode )) < 0 )
   {
      fprintf( stderr, "process_file(): open( %s ): %s.\n", ptr, strerror( errno ));
      goto ERROR;
   }

   if (( r = fc_send_frame( conn, filename, namelen + 1 )))
   {
      if ( r == -1 )
         fputs( fc_error, stderr );

      if ( r == -2 )
         result = 2;

      goto ERROR;
   }

   first = 1;
   total = count = 0;
   return 0;

ERROR:
   if ( receiving > 0 )
   {
      close( receiving );
      receiving = -1;
   }

   return result;
}

int process_transfer()
{
   recno_t last;
   unsigned char *line;
   unsigned int len;

   if ( is_text || !( last = db_lastline()))
      return 0;

   if ( db_retrieve( y, &line, &len ))
      return 1;

   if ( ! len )
      return 0;

   if ( line[ len - 1 ] != '/' )
      return process_file( line, len );

   return push_directory( line, len );
}

int process_stop( char c )
{
   int result = 0;

   if ( c == '\r' && ( result = fc_send_frame( conn, ( unsigned char *)".1", 3 )))
   {
      if ( result == -1 )
         fputs( fc_error, stdout );

      return abs( result );
   }

   beep();
   fflush( stdout );
   return 0;
}

int process_command()
{
   char c;
   int n, result = 0;

   if (( n = read( STDIN_FILENO, &c, 1 )) < 0 )
   {
      fprintf( stderr, "process_command(): read( STDIN_FILENO ): %s.\n", strerror( errno ));
      return 1;
   }

   if ( ! n )
      return 1;

   if ( receiving > 0 )
      return process_stop( c );

   switch( c )
   {
      case 'j':
         forw_line();
         break;

      case 'k':
         back_line();
         break;

      case 'l':
         result = forw_char();
         break;

      case 'h':
         result = back_char();
         break;

      case 's':
         save_text();
         break;

      case ' ':
         forw_screen();
         break;

      case 8:
         back_screen();
         break;

      case 9:
         result = next_dir();
         break;

      case '/':
         result = search( 1 );
         break;

      case '?':
         result = search( 0 );
         break;

      case 'p':
         result = show_help();
         break;

      case 'n':
         result = next_match();
         break;

      case 'N':
         result = previous_match();
         break;

      case '\r':
         result = process_transfer();
         break;

      case 27:
         result = pop_directory();
         break;

      case 'q':
         result = 1;
   }

   return result;
}

void resize_message()
{
   putp( cl );
   putp( tgoto( cm, 0, 0 ));
   putp( tparm( af, COLOR_YELLOW ));
   fputs( "[ Press a key when finished resizing ]", stdout );
   putp( tparm( af, COLOR_WHITE ));
   fflush( stdout );
}

void interact()
{
   int i, n, rs, kq, closed;
   struct kevent inq[ 2 ], outq[ 2 ];

   if (( kq = kqueue()) < 0 )
   {
      fprintf( stderr, "interact(): kqueue(): %s", strerror( errno ));
      return;
   }

   closed = 0;

AGAIN:
   inq[ 0 ].ident  = conn->fd;
   inq[ 0 ].filter = EVFILT_READ;
   inq[ 0 ].fflags = 0;
   inq[ 0 ].flags  = EV_ADD | EV_ENABLE;

   inq[ 1 ].ident  = STDIN_FILENO;
   inq[ 1 ].filter = EVFILT_READ;
   inq[ 1 ].fflags = 0;
   inq[ 1 ].flags  = EV_ADD | EV_ENABLE;

   if ( kevent( kq, ( struct kevent *)&inq, 2, NULL, 0, NULL ) < 0 )
   {
      fprintf( stderr, "interact(): kevent(): %s", strerror( errno ));
      return;
   }

   for( ; ; )
   {
      n = kevent( kq, NULL, 0, ( struct kevent *)&outq, 2, NULL );

      if ( n <= 0 )
      {
         if ( sigwinch )
         {
            resize_message();
            continue;
         }

         break;
      }

      if ( sigwinch )
      {
         sigwinch = 0;
         y = 1;
         r = 0;

         if ( screenful( 1 ))
            break;
      }

      for( i = 0; i < n; ++i )
      {
         if ( outq[ i ].flags & EV_ERROR )
            continue;

         if ( closed )
         {
            mf_connect();
            --closed;
            goto AGAIN;
         }
         else if ( outq[ i ].ident == STDIN_FILENO )
         {
            if (( rs = process_command()) == 1 )
               goto ESCAPE;
         }
         else if ( receiving > 0 )
         {
            if (( rs = receive_file()) == 1 )
               goto ESCAPE;
         }
         else
         {
            fc_close_connection( conn );
            ++closed;
         }
      }
   }

ESCAPE:
   close( kq );
}

void start()
{
   if ( fc_init())
   {
      fputs( fc_error, stderr );
      exit( 1 );
   }

   mf_connect();

   if ( retrieve_directory(( unsigned char *)"/", 1 ))
      exit( 1 );

   screenful( 1 );
   show_help();
   interact();

   fc_close_connection( conn );
   fc_deinit();
}

int main( int argc, char **argv )
{
   int i, n = 0;

   while(( i = getopt( argc, argv, "ip:" )) != -1 )
   {
      ++n;

      switch( i )
      {
         case '?':
            exit( 1 );

         case 'i':
            ++ignore;
            break;

         case 'p':
            port = optarg;
            ++n;
      }
   }

   argc -= n;
   argv += n;

   if ( argc < 2 )
   {
      fputs( "usage: mclient [-i] [-p <port>] <hostname|address>\n"
             "usage: -i -- Ignore mismatch of server hostname with TLS certificate hostname.\n"
             "usage: -p -- Connect to a port other than the default 8000.\n",
             stderr );
      exit( 1 );
   }

   hostname = argv[ 1 ];
   init();
   start();

   return 0;
}
