
// SPDX-License-Identifier: CC-BY-NC-SA-4.0
//
// Copyright (C) 2026 Bit by Bit Signal Processing LLC (https://bxbsp.com)
//
// This work is placed under the "Creative Commons Attribution
// NonCommercial ShareAlike 4.0 International" license, known
// by the shortened acronym "CC-BY-NC-SA-4.0".
//
// This work 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.
//
// A CC-BY-NC-SA-4.0 license allows you to use this work for
// noncommercial purposes so long as attribution is made to the
// original author.  Modified versions of this work may be distributed,
// but only under the same license.  For further details, see the
// Creative Commons License "CC-BY-NC-SA-4.0".
//
// You should have received a copy of the CC-BY-NC-SA-4.0 license
// along with this work. If not, see
// <https://creativecommons.org/licenses/by-nc-sa/4.0/>.
//

#include "displays.hh"
#include "local_display.hh"
#include "web_display.hh"
#include "runnable.hh"
#include <sys/vt.h>
#include <security/pam_appl.h>
#include <security/pam_misc.h>
#include <stdio.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <errno.h>

int displays::vt_to_open           = 9;

#ifdef SWITCH_VT

static void pam_error(pam_handle_t *pamh, int code)
{
  const char *txt = pam_strerror(pamh, code);  
  if(txt)
    fprintf(stderr, "PAM Error: %s (%d)\n", txt, code);
  else
    fprintf(stderr, "PAM Error Number: (%d)\n", code);
  
  pam_end(pamh, code);
  exit(100);
}

static void fork_to_pam_session_in_current_vt_as_specified_user(const char* user_name, int vt_to_open)
{
  //
  // Fork the session.  Do this so that a main thread is left
  // to clean up when the child exits.
  //
  signal(SIGTSTP, SIG_IGN);
  signal(SIGINT,  SIG_IGN);
  
  struct sigaction sa;
  struct sigaction old_sa_hup;
  struct sigaction old_sa_term;
  memset(&sa, 0, sizeof(sa));
  sa.sa_handler = SIG_IGN;
  sigaction(SIGHUP,  &sa, &old_sa_hup);
  sigaction(SIGTERM, &sa, &old_sa_term);

  char temp[100];
  sprintf(temp, "/dev/tty%d", vt_to_open);

  FILE* original_stdin  = freopen(temp, "r", stdin);
  //FILE* original_stdout = freopen(temp, "w", stdout);
  //FILE* original_stderr = freopen(temp, "w", stderr);

  if(original_stdin==0)// || original_stdout==0 || original_stderr==0)
    {
      printf("Couldn't redirect standard I/O to %s\n", temp);
    }
  
  //  int fd = open(temp, 0);
  //
  //  if(fd>0)
  //  {
  //    dup2(fd, STDIN_FILENO);
  //    dup2(fd, STDOUT_FILENO);
  //    dup2(fd, STDERR_FILENO);
  //  }

  
  pid_t child_pid = fork();
  
  if(child_pid<0)
    {
      printf("Fork failed.  Couldn't start PAM session.\n");
      //pam_setcred(pamh, PAM_DELETE_CRED);
      //pam_end(pamh, pam_close_session(pamh, 0));
      exit(100);
    }
  else if(child_pid)
    {
      //
      // Parent
      //
      // Wait for the child to finish, and then clean up the session.
      //
      
      int original_tty_number = 1;
      int tty_fd;
      
      FILE* fp = fopen("/sys/class/tty/tty0/active", "r");
      if(fp)
	{
	  int num = fscanf(fp, "tty%d", &original_tty_number);
	  if(num==0)
	    {
	      printf("Can't read original TTY number.\n");
	    }
	  else
	    {
	      printf("Program started on TTY %d.\n", original_tty_number);
	    }
	  fclose(fp);
	}

      pam_handle_t *pamh = NULL;
      int rc;

      struct pam_conv conv = { misc_conv, NULL };
      
      rc = pam_start("login", user_name, &conv, &pamh);
      if(rc != PAM_SUCCESS)
        {
          printf("PAM failure, aborting: %s", pam_strerror(pamh, rc));
          exit(EXIT_FAILURE);
        }
      
      rc = pam_acct_mgmt(pamh, 0);
      
      if(rc == PAM_NEW_AUTHTOK_REQD)
        rc = pam_chauthtok(pamh, PAM_CHANGE_EXPIRED_AUTHTOK);
      
      if(rc != PAM_SUCCESS)
        pam_error(pamh, rc);
      
      rc = pam_setcred(pamh, PAM_ESTABLISH_CRED);
      if(rc != PAM_SUCCESS)
        {
          printf("pam_setcred 1 failed.\n");
          exit(20);
        }
  
      rc = pam_open_session(pamh, 0);
      if(rc != PAM_SUCCESS)
        {
          printf("pam_open_session failed.\n");
          exit(20);
        }
      
      rc = pam_setcred(pamh, PAM_REINITIALIZE_CRED);
      if(rc != PAM_SUCCESS)
        {
          printf("pam_setcred 2 failed.\n");
          exit(20);
        }


      //int serror = system("loginctl");
      //if(serror)
      //  printf("Error running loginctl.\n");

      
      tty_fd = open("/dev/tty0", O_RDWR | O_CLOEXEC);
      if(tty_fd<0)
	{
	  printf("Can't open \"/dev/tty0\" in vt_switch().  error is %d, (\"%s\")\n", errno, strerror(errno));
	  exit(20);
	}

      printf("Attempting to switch to Virtual Terminal %d.\n", vt_to_open);
      
      int error = ioctl(tty_fd, VT_ACTIVATE, ((char*)0)+vt_to_open);
      
      if(error<0)
        {
          printf("Switch to Virtual Terminal %d failed.\n", vt_to_open);
        }
      else
        {
          error = ioctl(tty_fd, VT_WAITACTIVE, ((char*)0)+vt_to_open);
	    
          if(error<0)
            {
              printf("Switch to Virtual Terminal %d timed out.\n", vt_to_open);
            }
        }

      sleep(3);

      ssize_t count = write(STDOUT_FILENO, "Activated VT", 12);

      if(count!=12)
        printf("Write to activate VT should have had 12 length but had %d.\n", (int)count);
      
      signal(SIGQUIT, SIG_IGN);
      
      // Wait as long as the child still runs
      while (wait(NULL) == -1 && errno == EINTR)
        ;
      
      //close(STDIN_FILENO);
      //close(STDOUT_FILENO);
      //close(STDERR_FILENO);

      pam_setcred(pamh, PAM_DELETE_CRED);
      pam_end(pamh, pam_close_session(pamh, 0));
      
      printf("Switching back to Virtual Terminal %d on exit.\n", original_tty_number);
      error = ioctl(tty_fd, VT_ACTIVATE, ((char*)0)+original_tty_number);
  
      if(error)
        printf("Error switching back to original VT.\n");
      
      close(tty_fd);
      
      exit(0);
    }
  else
    {
      //
      // Child
      //
      // Restore signals, start a new session head
      //
      sigaction(SIGHUP, &old_sa_hup, NULL);
      sigaction(SIGTERM, &old_sa_term, NULL);
      signal(SIGTSTP, SIG_DFL);
      signal(SIGINT,  SIG_DFL);

      //
      // Don't start a new session head -- if we do, can't ctrl-C to
      // stop the process.
      //
      //setsid();

      //pam_end(pamh, PAM_SUCCESS|PAM_DATA_SILENT);
    }
}

#endif

displays::~displays()
{
  for(int i=0; i<num_local_displays; i++)
    delete local_displays[i];
}


displays::displays(int num_panes_per_display)
  : display(1000, 800, num_panes_per_display)
{
  printf("created new displays, this=%p\n", this);

#ifdef SWITCH_VT
  if(enable_display)
    {
      const char* user_name = getenv("SUDO_USER");
              
      if(!user_name)
        {
          printf("ERROR:  This program was meant to be run under SUDO.\n"
                 "Yet the environment variable SUDO_USER isn't defined.\n"
                 "A PAM session can't be initialized, but continuing\n"
                 "in case one isn't necessary.\n");
          user_name = "root";  // Bad, but better than nothing?
        }
      
      fork_to_pam_session_in_current_vt_as_specified_user(user_name, vt_to_open);
    }
#endif

  
  web_displays = new web_display(num_panes_per_display);
  
  this->num_panes_per_display = num_panes_per_display;
  num_local_displays = 0;

  do_event_loop = true;

  start();
}

void displays::check_for_new_displays()
{
  local_display::check_for_new_displays(this, num_panes_per_display);

  // Might do this sometime, if we don't switch to the VT at start.
  // Currently we need to switch to the VT at start so we can start
  // a PAM session there, so audio works.
  //if(num_local_displays==0)
  //vt_to_open = (vt_to_open+1)&15;

}

void displays::get_and_process_events()
{
  //if(parent)
  //  ((display*)parent)->get_and_process_events();

  if(num_local_displays)
    local_displays[0]->get_and_process_events();

  web_displays->get_and_process_events();
}

bool displays::need_fast_display()
{
  if(num_local_displays)
    return local_displays[0]->need_fast_display();

  return web_displays->need_fast_display();
}


void displays::run()
{
  for(;;)
    {
      check_for_new_displays();
      sleep(2);
    }
}

void displays::check_for_display_changes()
{
  display* display_to_use = 0;
  
  web_displays->handle_browser_connections();  

  if(web_displays->wsc_active && num_local_displays)
    {
      long long t_diff = ( (web_displays->last_event_time.tv_sec-local_displays[0]->last_event_time.tv_sec)*1000000ll
			   + (web_displays->last_event_time.tv_usec-local_displays[0]->last_event_time.tv_usec) );
      if(t_diff>0)
	{
	  display_to_use = web_displays;
	}
      else
	{
	  display_to_use = local_displays[0];
	}
    }
  else if(web_displays->wsc_active)
    {
      display_to_use = web_displays;
    }
  else if(num_local_displays)
    {
      display_to_use = local_displays[0];
    }
      
  if(display_to_use && display_to_use != parent)
    {
      if(parent)
	parent->remove_no_delete(this);
      display_to_use->add(this);

      clearDrawToStatic();
      mark_layout_dirty();
      resize(0, 0, parent->width, parent->height);

      printf("Changed to new primary display with width %d and height %d\n", parent->width, parent->height);
      
      if(num_local_displays && (display_to_use != local_displays[0]))
	display_inactive_message(local_displays[0]);
      if(display_to_use != web_displays)
	display_inactive_message(web_displays);

      entry_accepted_beep();
      trash_events_for_us(10000);
    }
}

void displays::display_inactive_message(display* d)
{
  color c = BLUE;
  d->layout_dirty = false;
  d->mark_dirty();
  d->beginDrawToStatic();
  d->clear(BLACK);
  d->set_text_size(40);
  d->draw_text("Some other display is active.", c, d->width/2, d->height/2-60, DRAW_TEXT_X_CENTER|DRAW_TEXT_Y_CENTER|DRAW_TEXT_ROTATE_0);
  d->draw_text("Click to reclaim.", c, d->width/2, d->height/2, DRAW_TEXT_X_CENTER|DRAW_TEXT_Y_CENTER|DRAW_TEXT_ROTATE_0);
  d->endDrawToStatic();
  while(!d->draw())
    ;

  //printf("displayed inactive message.\n");
}

void displays::event_loop()
{
  //printf("Called displays::event_loop()\n");
  //
  // This runs in a different thread from the display code.  It handles the
  // input events.
  //
  // The event loop is re-entrant.  i.e. some events may not return, but instead
  // may call the event loop to process further events before they can return to
  // the original event loop.  The variable do_event_loop is set to false by them
  // to exit the inner event loop and return to the outer one.
  //
  while(do_event_loop)
    {
      draw();                    // Draw static, dynamic, and flip the page
      usleep(400);
      get_and_process_events();  // Fetch, distribute, and handle mouse and key events
    }

  //printf("Exited inner event loop because do_event_loop was false.\n");
  do_event_loop = true;
}

void displays::end_inner_event_loop()
{
  do_event_loop = false;
}
