#include "userprog/syscall.h"
#include "devices/shutdown.h"
#include "devices/input.h"
#include "filesys/file.h"
#include "filesys/filesys.h"
#include "threads/vaddr.h"
#include "threads/interrupt.h"
#include "threads/malloc.h"
#include "threads/thread.h"
#include "threads/synch.h"
#include "userprog/process.h"
#include "userprog/pagedir.h"
#include <stdio.h>
#include <syscall-nr.h>

static unsigned fd_counter = MIN_USER_FD;

struct open_file
  {
    int fd;                     /* File Descriptor / Identifier */
    struct file *file;          /* Pointer to the associated file */
    struct hash_elem elem;      /* elem for a hash table */
  };

static void syscall_handler (struct intr_frame *);

/* A syscall_function is a function that receives up to 3 arguments, the
   arguments to the functions are either ints or pointers taking up to 32 bits
   in size. */
typedef uintptr_t (*syscall_function) (uintptr_t, uintptr_t, uintptr_t);

/* System call function prototypes */
static void syscall_halt (void);
static void syscall_exit (int status);
static pid_t syscall_exec (const char *cmd_line);
static int syscall_wait (pid_t pid);
static bool syscall_create (const char *file, unsigned initial_size);
static bool syscall_remove (const char *file);
static int syscall_open (const char *file);
static int syscall_filesize (int fd);
static int syscall_read (int fd, void *buffer, unsigned size);
static int syscall_write (int fd, const void *buffer, unsigned size);
static void syscall_seek (int fd, unsigned position);
static unsigned syscall_tell (int fd);
static void syscall_close (int fd);

static struct open_file *fd_get_file (int fd);
static void *validate_user_pointer (const void *ptr, size_t size);

/* A struct defining a syscall_function pointer along with its arity. */
typedef struct
  {
    syscall_function function;    /* Function pointer. */
    int arity;                    /* Number of arguments of the function. */
  } syscall_arguments;

/* A look-up table mapping numbers to system call functions with their number of
   arguments. */
static const syscall_arguments syscall_lookup[] =
{
  [SYS_HALT] = {(syscall_function) syscall_halt, 0},
  [SYS_EXIT] = {(syscall_function) syscall_exit, 1},
  [SYS_EXEC] = {(syscall_function) syscall_exec, 1},
  [SYS_WAIT] = {(syscall_function) syscall_wait, 1},
  [SYS_CREATE] = {(syscall_function) syscall_create, 2},
  [SYS_REMOVE] = {(syscall_function) syscall_remove, 1},
  [SYS_OPEN] = {(syscall_function) syscall_open, 1},
  [SYS_FILESIZE] = {(syscall_function) syscall_filesize, 1},
  [SYS_READ] = {(syscall_function) syscall_read, 3},
  [SYS_WRITE] = {(syscall_function) syscall_write, 3},
  [SYS_SEEK] = {(syscall_function) syscall_seek, 2},
  [SYS_TELL] = {(syscall_function) syscall_tell, 1},
  [SYS_CLOSE] = {(syscall_function) syscall_close, 1},
};

/* The number of syscall functions (i.e, number of elements) within the
   syscall_lookup table. */
static const int LOOKUP_SIZE
  = sizeof (syscall_lookup) / sizeof (syscall_arguments);


/* Initialises the syscall handling system, as well as a global lock to
   synchronise all file access between processes. */
void
syscall_init (void) 
{
  intr_register_int (0x30, 3, INTR_ON, syscall_handler, "syscall");
  lock_init (&filesys_lock);
}

/* Function that takes a interrupt frame containing a syscall and its args.
   Validates the arguments and pointers before calling the relevant
   high-level system call function, storing its output (if any) in f->eax */
static void
syscall_handler (struct intr_frame *f)
{
  /* First, read the system call number from the stack. */
  validate_user_pointer (f->esp, 1);
  unsigned syscall_number = *(int *) f->esp;

  /* Ensures the number corresponds to a system call that can be handled. */
  if (syscall_number >= LOOKUP_SIZE)
    thread_exit ();

  syscall_arguments syscall = syscall_lookup[syscall_number];

  /* Next, read and copy the arguments from the stack pointer. */
  validate_user_pointer (f->esp + sizeof (uintptr_t),
                         syscall.arity * sizeof (uintptr_t));
  uintptr_t args[3] = {0};
  for (int i=0; i < syscall.arity; i++)
      args[i] = *(uintptr_t *) (f->esp + sizeof (uintptr_t) * (i + 1));

  /* Call the function that handles this system call with the arguments. When
     there is a return value it is stored in f->eax. */
  f->eax = syscall.function (args[0], args[1], args[2]);
}

/* Called upon a "halt" syscall, resulting in a complete shutdown of the
   process, via shutdown_power_off (); */
static void
syscall_halt (void)
{
  shutdown_power_off ();
}

static void
syscall_exit (int status)
{
  /* Sets exit_status of the thread to status. thread_exit () will call
     process_exit () if user programs are allowed. */
  thread_current ()->exit_status = status;
  thread_exit ();
}

/* Executes a given command with the relevant args, by calling process_execute.
   Acquires the filesystem lock as process_execute accesses the file system.
   Returns PID for the process that is running the CMD_LINE
 */
static pid_t
syscall_exec (const char *cmd_line)
{
  validate_user_pointer (cmd_line, 1);

  pid_t pid = process_execute(cmd_line);

  return pid;
}

/* Handles the syscall of wait. Effectively a wrapper for process_wait as the
   necessary validation and such all happens in process_wait anyway. */
static int
syscall_wait (pid_t pid)
{
  return process_wait (pid);
}

/* Handles the syscall for file creation. First validates the user file
   pointer. Acquires the file system lock to prevent synchronisation issues,
   and then uses FILESYS_CREATE to create the file, returning the same status */
static bool
syscall_create (const char *file UNUSED, unsigned initial_size UNUSED)
{
  validate_user_pointer (file, 1);

  lock_acquire (&filesys_lock);
  bool status = filesys_create (file, initial_size);
  lock_release (&filesys_lock);

  return status;
}

/* Handles the syscall for file removal. First validates the user file pointer.
   Acquires the file system lock to prevent synchronisation issues, and then
   uses FILESYS_REMOVE to remove the file, returning the same success status */
static bool
syscall_remove (const char *file)
{
  validate_user_pointer (file, 1);

  lock_acquire (&filesys_lock);
  bool status = filesys_remove (file);
  lock_release (&filesys_lock);

  return status;
}

/* Handles the syscall for opening a file connection. First, validates the file
   pointer. Then it acquires a lock for the file system, in order to open the
   connection without synchronisation issues. It then maps a new fd to this file
   in the hash table before returning the fd. */
static int
syscall_open (const char *file)
{
  validate_user_pointer (file, 1);

  lock_acquire (&filesys_lock);
  struct file *ptr = filesys_open (file);
  lock_release (&filesys_lock);
  if (ptr == NULL)
    return -1;

  /* Allocate space for a struct representing a mapping from an FD to a struct
     file. */
  struct open_file *file_info
    = (struct open_file*) malloc (sizeof (struct open_file));
  if (file_info == NULL)
    return -1;

  /* Populate the above struct, with a unique FD and the current open file */
  file_info->fd = fd_counter++;
  file_info->file = ptr;

  /* Add the new FD->file mapping to the hashtable for the current thread */
  hash_insert (&thread_current ()->open_files, &file_info->elem);

  /* Return the new FD */
  return file_info->fd;
}

/* Handles the syscall for getting a file's size. Converts a provided FD into
   the asssociated file struct. Acquire the lock for the filesystem and use
   FILE_LENGTH to calculate the length for return. */
static int
syscall_filesize (int fd)
{
  struct open_file *file_info = fd_get_file (fd);
  if (file_info == NULL)
      return -1;

  lock_acquire (&filesys_lock);
  int bytes = file_length (file_info->file);
  lock_release (&filesys_lock);

  return bytes;
}

/* Handles the syscall for reading SIZE bytes from a file referenced by FD.
   If the FD references the console, use input_getc (), otherwise convert the
   FD to its associated file struct, acquire the filesystem lock, read up to
   SIZE bytes and then return the number of bytes read.*/
static int
syscall_read (int fd, void *buffer, unsigned size)
{
  /* Only console (fd = 0) or other files, not including STDOUT, (fd > 1) are
     allowed. */
  if (fd < 0 || fd == STDOUT_FILENO)
    return -1;

  validate_user_pointer (buffer, size);

  if (fd == STDIN_FILENO)
    {
      /* Reading from the console. */
      char *write_buffer = buffer;
      for (int i = 0; i < size; i++)
        write_buffer[i] = input_getc ();

      return size;
    }
  else
    {
      /* Reading from a file. */
      struct open_file *file_info = fd_get_file (fd);
      if (file_info == NULL)
        return -1;

      lock_acquire (&filesys_lock);
      int bytes_written = file_read (file_info->file, buffer, size);
      lock_release (&filesys_lock);
      return bytes_written;
    }
}

/* Handles the syscall for writing SIZE bytes to a file referenced by FD.
   If the FD references the console, use put_buf (), otherwise convert the
   FD to its associated file struct, acquire the filesystem lock, write up to
   SIZE bytes and then return the number of bytes written.*/
static int
syscall_write (int fd, const void *buffer, unsigned size)
{
  /* Only console (fd = 1) or other files, not including STDIN, (fd > 1) are
     allowed. */
  if (fd <= 0)
    return 0;

  validate_user_pointer (buffer, size);

  if (fd == STDOUT_FILENO)
    {
      /* Writing to the console. */
      putbuf (buffer, size);
      return size;
    }
  else
    {
      /* Writing to a file. */
      struct open_file *file_info = fd_get_file (fd);
      if (file_info == NULL)
        return 0;

      lock_acquire (&filesys_lock);
      int bytes = file_write (file_info->file, buffer, size);
      lock_release (&filesys_lock);

      return bytes;
    }
}

/* Handles the syscall for seeking to POSITION bytes in a file referenced by
   FD. Converts the FD to its associated file struct, acquires the filesystem
   lock and then uses file_seek to adjust the cursor to a specific position in
   the file.*/
static void
syscall_seek (int fd, unsigned position)
{
  struct open_file *file_info = fd_get_file (fd);
  if (file_info != NULL)
  {
    lock_acquire (&filesys_lock);
    file_seek (file_info->file, position);
    lock_release (&filesys_lock);
  }
}

/* Handles the syscall for returning the next byte in a file referenced by
   FD. Converts the FD to its associated file struct, acquires the filesystem
   lock and then uses file_tell to read the next byte.*/
static unsigned
syscall_tell (int fd)
{
  struct open_file *file_info = fd_get_file (fd);
  if (file_info == NULL)
    return 0;

  lock_acquire (&filesys_lock);
  unsigned pos = file_tell (file_info->file);
  lock_release (&filesys_lock);

  return pos;
}

/* Handles the syscall for closing a connection to a file. Converts the FD to
   its associated file struct. If it exists, it removes it from the hash table,
   acquires the filesystem lock, and uses file_close to close the connection.*/
static void
syscall_close (int fd)
{
  struct open_file *file_info = fd_get_file (fd);
  if (file_info != NULL)
    {
      hash_delete (&thread_current ()->open_files, &file_info->elem);

      lock_acquire (&filesys_lock);
      file_close (file_info->file);
      lock_release (&filesys_lock);
      free (file_info);
    }
}

/* Hashing function needed for the open_file table. Returns a hash for an entry,
   based on its FD. */
unsigned
fd_hash (const struct hash_elem *element, void *aux UNUSED)
{
  return hash_int (hash_entry (element, struct open_file, elem)->fd);
}

/* Comparator function for the open_file table. Compares two entries based on
   the FDs. */
bool
fd_less (const struct hash_elem *a_, const struct hash_elem *b_,
        void *aux UNUSED)
{
  struct open_file *a = hash_entry (a_, struct open_file, elem);
  struct open_file *b = hash_entry (b_, struct open_file, elem);

  return a->fd < b->fd;
}

/* Gets a file from its descriptor (FD number). If there is no file with the fd
   FD it returns NULL. */
static struct open_file *
fd_get_file (int fd)
{
  /* We have to set up a fake open_file in order to be able to search the hash
     table. See hash.h. */
  struct open_file fake_file_info;
  fake_file_info.fd = fd;

  struct hash_elem *e
    = hash_find (&thread_current ()->open_files, &fake_file_info.elem);

  if (e == NULL)
      return NULL;

  return hash_entry (e, struct open_file, elem);
}

/* Validates if a block of memory starting at PTR and of size SIZE bytes is
   fully contained within user virtual memory. Kills the thread (by calling
   thread_exit) if the memory is invalid. Otherwise, returns the PTR given.
   If the size is 0, the function does no checks and returns PTR.*/
static void *
validate_user_pointer (const void *ptr, size_t size)
{
  if (size > 0 && (ptr == NULL ||
                   !is_user_vaddr (ptr) ||
                   !is_user_vaddr (ptr + size - 1) ||
                   pagedir_get_page (thread_current()->pagedir, ptr) == NULL))
    thread_exit ();

  return (void *) ptr;
}
