// resize.rs

// *************************************************************************
// * Copyright (C) 2018 Daniel Mueller (deso@posteo.net)                   *
// *                                                                       *
// * This program 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 of the License, or     *
// * (at your option) any later version.                                   *
// *                                                                       *
// * This program 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.  See the         *
// * GNU General Public License for more details.                          *
// *                                                                       *
// * You should have received a copy of the GNU General Public License     *
// * along with this program.  If not, see <http://www.gnu.org/licenses/>. *
// *************************************************************************

//! Infrastructure for intercepting and handling SIGWINCH signals.
//!
//! The purpose of this module is to install a signal handler for
//! SIGWINCH, intercept them, and send an event indicating that such a
//! signal was received through a supplied `std::sync::mpsc::Sender`
//! part of a `std::sync::mpsc::channel`.
//!
//! Signal handling is icky and a signal handler is a particularly
//! disgusting context to run in. It is inherently unsafe because there
//! are severe limitations as to what functions may be invoked. More
//! specifically, we must not invoke anything that grabs a lock in order
//! to guarantee dead lock freedom (a signal handler may interrupt and
//! stop the running thread at an arbitrary time, including when it has
//! acquired a lock; given that the thread will not be running we would
//! deadlock if we were to attempt to acquire the same lock).
//!
//! We would ideally want to directly send an event through a channel
//! from a signal handler, but it is left undefined whether the channel
//! primitive may use locks. To be safe, we instead have a thread
//! waiting on a pipe and just send a byte through this pipe to cause a
//! wake up. The thread will subsequently write to the channel as
//! desired -- from a context that is safe.
//!
//! Note that with signalfd(2) another primitive exist that takes care
//! of part of the work. However, it was not chosen for two reasons:
//! 1) It is Linux specific.
//! 2) It requires us to mask the signal we want to receive with it on
//!    all threads. That means we need to somehow run disabling logic in
//!    all threads we would want to create, ever.

use std::cmp::PartialOrd;
use std::io::Error;
use std::io::ErrorKind;
use std::io::Result;
use std::sync::mpsc::Sender;
use std::thread;

use libc::c_int;
use libc::c_void;
use libc::pipe;
use libc::read;
use libc::SIG_ERR;
use libc::signal;
use libc::SIGWINCH;
use libc::size_t;
use libc::write;

use crate::Event;


/// The file descriptor for a write end of a pipe used for signaling resize events.
static mut WRITE_FD: c_int = 0;


/// Check the return value of a system call.
fn check<T>(result: T, error: T) -> Result<()>
where
  T: Copy + PartialOrd<T>,
{
  if result == error {
    Err(Error::last_os_error())
  } else {
    Ok(())
  }
}

/// A signal handler for SIGWINCH that sends a byte through `WRITE_FD` to wake up a thread.
extern "C" fn handler(signum: c_int) {
  debug_assert_eq!(signum, SIGWINCH);

  let buffer = [0];
  let result = unsafe { write(WRITE_FD, buffer.as_ptr() as *const c_void, 1) };
  check(result, -1).unwrap();
}

/// Register a signal handler for `SIGWINCH` and send a `Event::Resize` object upon interception.
pub fn receive_window_resizes(send_event: Sender<Result<Event>>) -> Result<()> {
  let mut fds = [0, 0];
  unsafe {
    check(pipe(fds.as_mut_ptr()), -1)?;
    check(signal(SIGWINCH, handler as size_t), SIG_ERR)?;
    WRITE_FD = fds[1]
  }
  let read_fd = fds[0];

  let _ = thread::spawn(move || {
    loop {
      let mut buffer = [0];
      let result = unsafe { read(read_fd, buffer.as_mut_ptr() as *mut c_void, 1) };
      if result < 0 {
        if Error::last_os_error().kind() != ErrorKind::Interrupted {
          continue
        }
      }

      let result = check(result, -1).map(|_| Event::Resize);
      send_event.send(result).unwrap();
    }
  });
  Ok(())
}
