/*

  sftppager.h

  Author: Tomi Salo <ttsalo@ssh.fi>

  Copyright (c) 1999 SSH Communications Security, Finland
  All rights reserved.

  Created Wed Jan 19 15:37:52 2000.

  Paging for Sftp2.
  
  */

#include "sshincludes.h"
#include "sftppager.h"
#include "sshfsm.h"
#include "sshbuffer.h"

#define SSH_DEBUG_MODULE "SftpPager"

struct SshSftpPagerCtxRec
{
  SshBuffer *buffer;
  SshFSMCondition buffer_grown;
  SshFSMThread thread;
  SshStream stream;
  unsigned int screen_width;
  unsigned int screen_height;

  unsigned int lines_written;
  unsigned int bytes_written;
  unsigned int current_column;

  Boolean disabled;
  Boolean want_exit;

  SshSftpPagerCb user_callback;
  void *user_context;

  SshFSMCondition input_avail;
  SshFSMThread input_availability_notifier;

  int received, printed;

  unsigned char *screen_buffer;
};

struct SshSftpPagerIANotifierCtxRec
{
  SshFSMCondition input_avail;
  SshStream stream;
  Boolean want_input_signal, callback_set;
};

typedef struct SshSftpPagerIANotifierCtxRec *SshSftpPagerIANotifierCtx;

void
ssh_sftp_pager_thread_destructor(void *tdata)
{
  SshSftpPagerCtx ctx = (SshSftpPagerCtx)tdata;

  ssh_buffer_free(ctx->buffer);
  ssh_xfree(ctx->screen_buffer);
  ssh_fsm_condition_destroy(ctx->input_avail);
}

/* Initializes the pager module. Requires the SshStream for
   input/output. */
SshSftpPagerCtx
ssh_sftp_pager_init(SshStream stream, SshFSM fsm,
                    unsigned int screen_width,
                    unsigned int screen_height)
{
  SshSftpPagerCtx ctx;
  SshSftpPagerIANotifierCtx ia_notifier_ctx;
  SshFSMThread thread = ssh_fsm_spawn(fsm, sizeof(struct SshSftpPagerCtxRec),
                                      "pager_buf_to_out", NULL,
                                      ssh_sftp_pager_thread_destructor);

  ctx = ssh_fsm_get_tdata(thread);

  ctx->thread = thread;
  ctx->stream = stream;

  ctx->buffer = ssh_buffer_allocate();
  ctx->buffer_grown = ssh_fsm_condition_create(fsm);
  ctx->screen_width = screen_width;
  ctx->screen_height = screen_height;
  ctx->screen_buffer = ssh_xmalloc(screen_width * screen_height + 128);
  ctx->want_exit = FALSE;
  ctx->disabled = FALSE;

  ctx->input_avail = ssh_fsm_condition_create(fsm);

  ctx->input_availability_notifier =
    ssh_fsm_spawn(fsm, sizeof(struct SshSftpPagerIANotifierCtxRec),
                  "pager_input_availability_notify", NULL, NULL);

  ctx->received = 0;
  ctx->printed = 0;
  ctx->lines_written = 0;
  ctx->bytes_written = 0;
  ctx->current_column = 0;

  ia_notifier_ctx = ssh_fsm_get_tdata(ctx->input_availability_notifier);
  ia_notifier_ctx->input_avail = ctx->input_avail;
  ia_notifier_ctx->stream = ctx->stream;
  ia_notifier_ctx->want_input_signal = FALSE;
  ia_notifier_ctx->callback_set = FALSE;
  
  return ctx;
}

/* Uninitializes the pager module and destroys the context,
   but not before all the data has been displayed to the user.
   We might want to implement an emergency-exit feature too
   (this might never exit if the user doesn't press the any key) */
void
ssh_sftp_pager_uninit(SshSftpPagerCtx pager_ctx,
                      SshSftpPagerCb cb, void *context)
{
  pager_ctx->user_callback = cb;
  pager_ctx->user_context = context;
  pager_ctx->want_exit = TRUE;
}

/* Writes one string. The string is first appended into a buffer. */
void
ssh_sftp_pager_write_string(SshSftpPagerCtx pager_ctx,
                            const unsigned char *line)
{
  if (!pager_ctx->disabled)
    {
      pager_ctx->received += strlen(line);
      ssh_buffer_append(pager_ctx->buffer, line, strlen(line));
    }
}

void
ssh_sftp_pager_wakeup(SshSftpPagerCtx pager_ctx, SshFSMThread thread)
{
  SSH_FSM_CONDITION_SIGNAL(pager_ctx->buffer_grown);
}

/* Changes the current screen size */
void
ssh_sftp_pager_change_screen_size(SshSftpPagerCtx pager_ctx,
                                  unsigned int screen_width,
                                  unsigned int screen_height)
{
  pager_ctx->screen_width = screen_width;
  pager_ctx->screen_height = screen_height;
  ssh_xfree(pager_ctx->screen_buffer);
  pager_ctx->screen_buffer = ssh_xmalloc(screen_width * screen_height + 128);
}

SSH_FSM_STEP(sftp_pager_buf_to_out)
{
  SSH_FSM_TDATA(SshSftpPagerCtx);
  int i, len;
  unsigned char *buffer_ptr;

  SSH_FSM_SET_NEXT("pager_buf_to_out");

#if defined(O_NONBLOCK) && !defined(O_NONBLOCK_BROKEN)
      fcntl(1, F_SETFL, fcntl(1, F_GETFL, 0) & ~(O_NONBLOCK));
#else /* O_NONBLOCK && !O_NONBLOCK_BROKEN */
      fcntl(1, F_SETFL, fcntl(1, F_GETFL, 0) & ~(O_NDELAY));
#endif /* O_NONBLOCK && !O_NONBLOCK_BROKEN */

  if (tdata->disabled)
    {
      ssh_buffer_consume(tdata->buffer, ssh_buffer_len(tdata->buffer));
    }
  
  if (ssh_buffer_len(tdata->buffer) == 0)
    {
      if (tdata->want_exit == TRUE)
        {
          SSH_FSM_SET_NEXT("pager_finish");
          return SSH_FSM_CONTINUE;
        }
      SSH_FSM_CONDITION_WAIT(tdata->buffer_grown);
    }

  /* lines_written tells us how much text there is already on the
     screen. We'll now collect lines from the buffer into the screen
     buffer, updating the lines_written and bytes_written as we go. */

  len = ssh_buffer_len(tdata->buffer);
  buffer_ptr = ssh_buffer_ptr(tdata->buffer);
  i = 0;
  while (i < len)
    {
      tdata->screen_buffer[i] = buffer_ptr[i];
      if (buffer_ptr[i] == '\n')
        {
          tdata->lines_written++;
        }
      else
        if (buffer_ptr[i] == '\r')
          tdata->current_column = 0;
        else
          tdata->current_column++;
      i++;
      if (tdata->current_column == tdata->screen_width)
        tdata->lines_written++;
      if (tdata->lines_written == tdata->screen_height - 3)
        {
          /* Screen full. */
          write(1, tdata->screen_buffer, i);
          ssh_buffer_consume(tdata->buffer, i);
          SSH_FSM_SET_NEXT("pager_empty_input");
          return SSH_FSM_CONTINUE;
        }
    }

  write(1, tdata->screen_buffer, i);
  ssh_buffer_consume(tdata->buffer, i);
  
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(sftp_pager_empty_input)
{
  SSH_FSM_TDATA(SshSftpPagerCtx);
  unsigned char bunch_of_bytes[256];
  int ret;

#if defined(O_NONBLOCK) && !defined(O_NONBLOCK_BROKEN)
      fcntl(1, F_SETFL, fcntl(1, F_GETFL, 0) | O_NONBLOCK);
#else /* O_NONBLOCK && !O_NONBLOCK_BROKEN */
      fcntl(1, F_SETFL, fcntl(1, F_GETFL, 0) | O_NDELAY);
#endif /* O_NONBLOCK && !O_NONBLOCK_BROKEN */

  do {
    ret = ssh_stream_read(tdata->stream, bunch_of_bytes, 256);
  } while (ret > 0);

  SSH_FSM_SET_NEXT("pager_write_prompt");
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(sftp_pager_write_prompt)
{
#if defined(O_NONBLOCK) && !defined(O_NONBLOCK_BROKEN)
      fcntl(1, F_SETFL, fcntl(1, F_GETFL, 0) & ~(O_NONBLOCK));
#else /* O_NONBLOCK && !O_NONBLOCK_BROKEN */
      fcntl(1, F_SETFL, fcntl(1, F_GETFL, 0) & ~(O_NDELAY));
#endif /* O_NONBLOCK && !O_NONBLOCK_BROKEN */

  write(1, "<Press any key for more or q to quit>\r", 38); 

  SSH_FSM_SET_NEXT("pager_read_a_char");
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(sftp_pager_read_a_char)
{
  SSH_FSM_TDATA(SshSftpPagerCtx);
  int ret;
  unsigned char one_char;

#if defined(O_NONBLOCK) && !defined(O_NONBLOCK_BROKEN)
      fcntl(1, F_SETFL, fcntl(1, F_GETFL, 0) | O_NONBLOCK);
#else /* O_NONBLOCK && !O_NONBLOCK_BROKEN */
      fcntl(1, F_SETFL, fcntl(1, F_GETFL, 0) | O_NDELAY);
#endif /* O_NONBLOCK && !O_NONBLOCK_BROKEN */

  ret = ssh_stream_read(tdata->stream, &one_char, 1);
  if (ret == -1)
    {
      SSH_FSM_CONDITION_WAIT(tdata->input_avail);
    }
  if (one_char == 'q')
    tdata->disabled = TRUE;
  SSH_FSM_SET_NEXT("pager_reset");
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(sftp_pager_reset)
{
  SSH_FSM_TDATA(SshSftpPagerCtx);

  tdata->lines_written = 0;
  tdata->current_column = 0;
  
#if defined(O_NONBLOCK) && !defined(O_NONBLOCK_BROKEN)
      fcntl(1, F_SETFL, fcntl(1, F_GETFL, 0) & ~(O_NONBLOCK));
#else /* O_NONBLOCK && !O_NONBLOCK_BROKEN */
      fcntl(1, F_SETFL, fcntl(1, F_GETFL, 0) & ~(O_NDELAY));
#endif /* O_NONBLOCK && !O_NONBLOCK_BROKEN */

  memset(tdata->screen_buffer, ' ', tdata->screen_width);
  write(1, tdata->screen_buffer, tdata->screen_width);
  write(1, "\r", 1);
  
  SSH_FSM_SET_NEXT("pager_buf_to_out");
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(sftp_pager_finish)
{
  SSH_FSM_TDATA(SshSftpPagerCtx);
  ssh_fsm_kill_thread(tdata->input_availability_notifier);
  tdata->user_callback(SSH_SFTP_PAGER_OK, tdata->user_context);
  return SSH_FSM_FINISH;
}

void
sftp_pager_ia_notifier_stream_cb(SshStreamNotification notification,
                                 void *context)
{
  SshFSMThread thread = (SshFSMThread)context;
  SshSftpPagerIANotifierCtx ctx =
    (SshSftpPagerIANotifierCtx)ssh_fsm_get_tdata(thread);
  if (notification == SSH_STREAM_INPUT_AVAILABLE)
    {
      ctx->want_input_signal = TRUE;
      ssh_fsm_continue(thread);
    }
}

SSH_FSM_STEP(sftp_pager_input_availability_notify)
{
  SSH_FSM_TDATA(SshSftpPagerIANotifierCtx);
  if (tdata->want_input_signal == TRUE)
    {
      SSH_FSM_CONDITION_SIGNAL(tdata->input_avail);
      tdata->want_input_signal = FALSE;
    }
  if (tdata->callback_set == FALSE)
    {
      ssh_stream_set_callback(tdata->stream, sftp_pager_ia_notifier_stream_cb,
                              thread);
      tdata->callback_set = TRUE;
    }
  return SSH_FSM_SUSPENDED;
}

