/*
 * Copyright 2017 The Emscripten Authors.  All rights reserved.
 * Emscripten is available under two separate licenses, the MIT license and the
 * University of Illinois/NCSA Open Source License.  Both these licenses can be
 * found in the LICENSE file.
 */

#include <unistd.h>
#include <stdbool.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <poll.h>
#include <errno.h>
#include <assert.h>
#include <string.h>
#include <stdio.h>

unsigned char buf[1 << 16];

#define MAX(a, b) (((a) >= (b)) ? (a) : (b))


// This test program relies on the simplest read/write behavior when
// all the data can be read/written in one call.

void test_write(int fd1, unsigned char *ch, int size) {
  memset(buf, 0, sizeof buf);
  for (int i = 0; i < size; ++i) {
    buf[i] = (*ch)++;
  }
  assert(write(fd1, buf, size) == size);
}

void test_read(int fd0, unsigned char *ch, int size) {
  memset(buf, 0, sizeof buf);
  assert(read(fd0, buf, size) == size);
  for (int i = 0; i < sizeof buf; ++i) {
    unsigned char correct_ch = (i < size) ? (*ch)++ : 0;
    assert(buf[i] == correct_ch);
  }
}

// test_select and test_poll perform the exact same actions/assertions but
// with two different system calls.  They should always give the same
// result.

void test_select(int *fd, bool data_available) {
  fd_set rfds;
  FD_ZERO(&rfds);
  FD_SET(fd[0], &rfds);
  FD_SET(fd[1], &rfds);

  fd_set wfds;
  FD_ZERO(&wfds);
  FD_SET(fd[0], &wfds);
  FD_SET(fd[1], &wfds);

  // Don't block at all
  struct timeval tv = { 0, 0};
  int maxfd = MAX(fd[0], fd[1]) + 1;
  int ret = select(maxfd, &rfds, &wfds, NULL, &tv);

  if (data_available) {
    assert(ret == 2);
    assert(FD_ISSET(fd[0], &rfds));
  } else {
    assert(ret == 1);
    assert(!FD_ISSET(fd[0], &rfds));
  }

  assert(FD_ISSET(fd[1], &wfds));
}

void test_poll(int *fd, int data_available) {
  struct pollfd pfds[2];
  memset(pfds, 0, sizeof pfds);
  pfds[0].fd = fd[0];
  pfds[0].events = POLLIN | POLLOUT;
  pfds[1].fd = fd[1];
  pfds[1].events = POLLIN | POLLOUT;

  int ret = poll(pfds, 2, 0);
  if (data_available) {
    assert(ret == 2);
    assert(pfds[0].revents == POLLIN);
  } else {
    assert(ret == 1);
    assert(pfds[0].revents == 0);
  }
  assert(pfds[1].revents == POLLOUT);

  // select should report the exact same status.
  test_select(fd, data_available);
}

void test_most() {
  int fd[2];
  unsigned char wchar = 0;
  unsigned char rchar = 0;

  assert(pipe(fd) == 0);

  // Test that pipe is statable
  struct stat st;
  assert(fstat(fd[0], &st) == 0);

  // Test that pipe is not seekable

  memset(buf, 0, sizeof buf);
  assert(write(fd[1], buf, 128) == 128);
  assert(lseek(fd[0], 0, SEEK_CUR) == -1);
  assert(errno == ESPIPE);
  assert(lseek(fd[1], 0, SEEK_CUR) == -1);
  assert(errno == ESPIPE);
  assert(read(fd[0], buf, sizeof buf) == 128);

  // Now pipe is empty
  // Test interleaved writing and reading of different buffer sizes

  // write about 40 Kb of data
  for (int i = 1; i < 200; ++i) {
    test_write(fd[1], &wchar, i + 2);
    test_poll(fd, true);
    test_read (fd[0], &rchar, i);
    test_poll(fd, true);
    test_write(fd[1], &wchar, i + 1);
    test_poll(fd, true);
    test_read (fd[0], &rchar, i + 3);
    test_poll(fd, false);
  }

  // Test reading when there is less data available than the read buffer size

  assert(write(fd[1], buf, 10) == 10);
  assert(read(fd[0], buf, sizeof buf) == 10);

  // Write total of 1 Mb of data in small chunks
  // The pipe should not overflow

  int bytes_to_write = 1 << 20;
  while (bytes_to_write > 0) {
    test_write(fd[1], &wchar, sizeof buf);
    test_read (fd[0], &rchar, sizeof buf);
    bytes_to_write -= sizeof buf;
  }

  // Write large chunks of data (supposed to be larger than one internal buffer)
  test_write(fd[1], &wchar, 123);
  test_write(fd[1], &wchar, (1 << 15) + 321);
  test_write(fd[1], &wchar, 456);
  test_read(fd[0], &rchar, 456);
  test_read(fd[0], &rchar, (1 << 15) + 123);
  test_read(fd[0], &rchar, 321);

#ifndef WASMFS // TODO: fcntl in WASMFS
  // Test non-blocking read from empty pipe
  assert(fcntl(fd[0], F_SETFL, O_NONBLOCK) == 0);
  assert(read(fd[0], buf, sizeof buf) == -1);
  assert(errno == EAGAIN);
#endif

  // Normal operations still work in non-blocking mode
  test_poll(fd, false);
  test_write(fd[1], &wchar, 10);
  test_poll(fd, true);
  test_read (fd[0], &rchar, 10);
  test_poll(fd, false);

  // Clear buffer
  memset(buf, 0, sizeof(buf));
  // Test closing pipes.
  // Write to pipe
  assert(write(fd[1], "XXXX", 4) == 4);
  // Close write end
  assert(close(fd[1]) == 0);
  // This write should fail
  assert(write(fd[1], "YYYY", 4) == -1);
  // The error number is EBADF
  assert(errno == EBADF);

  // read from the other end of the pipe
  assert(read(fd[0], buf, 5) == 4);
  // We should have read what we wrote to the other end
  assert(memcmp(buf, "XXXX", 4) == 0);

  // Close the read end
  assert(close(fd[0]) == 0);
  // Now reading should return an error
  assert(read(fd[0], buf, 5) == -1);
  // The error number is EBADF
  assert(errno == EBADF);

  puts("done");
}

void test_redirect_stderr_to_pipe() {
  int stderrfd = fileno(stderr);
  int pipefd[2];
  int original_fd = dup(stderrfd); // duplicate stderr to original_fd, and original_fd is used to restore stderr later
  assert(original_fd >= 0);
  assert(pipe(pipefd) == 0);

  int read_end_fd = pipefd[0];
  int write_end_fd = pipefd[1];

  assert(dup2(write_end_fd, stderrfd) == stderrfd); // now things write to fd(stderr) is redirected to write_end_fd
  assert(close(write_end_fd) == 0); // close the write end of the pipe after duplicating

  assert(write(stderrfd, "xyz", 3) == 3); // write to the stderr, expected to be read from pipe

  assert(dup2(original_fd, stderrfd) == stderrfd); //  restore fd (stderr) to its original state
  assert(close(original_fd) == 0);

  char buffer[10];
  memset(buffer, 0, 10);
  assert(read(read_end_fd, buffer, 10) == 3);
  assert(strcmp(buffer, "xyz") == 0);
  assert(close(read_end_fd) == 0); // Close the read end of the pipe
  puts("done");
}

int main() {
  test_most();
  test_redirect_stderr_to_pipe();
  return 0;
}
