#include "interp.h"
#include "arena_alloc.h"
#include "context.h"
#include "edit.h"
#include "mprintf.h"
#include "parser.h"
#include "self.h"
#include <desc.h>
#include <filereader.h>
#include <filewriter.h>
#include <fs_types.h>
#include <kb.h>
#include <liballoc.h>
#include <libfat.h>
#include <path.h>
#include <printf.h>
#include <process.h>
#include <stddef.h>
#include <str_status.h>
#include <string.h>
#include <system.h>
#include <tcursor.h>
#include <terminal.h>
#include <tscreen.h>

static bool run = true;

bool interp_is_running (void) { return run; }

void interp_shutdown (void) { run = false; }

static void human_size (double size, double* out, char** str) {
  static char* units[] = {"B", "KiB", "MiB", "GiB", "TiB", "PiB"};

  size_t i = 0;
  while (size >= 1024.0 && i < (sizeof (units) / sizeof (units[0])) - 1) {
    size /= 1024.0;
    i++;
  }

  *out = size;
  *str = units[i];
}

static void echo (struct context* context, char** strings, size_t strings_count) {
  for (size_t i = 0; i < strings_count; i++)
    cprintf (context, "%s ", strings[i]);
}

static void mkfile (struct context* context, char** file_paths, size_t files_count) {
  char volume[VOLUME_MAX];
  const char* path;
  int ret;

  for (size_t i = 0; i < files_count; i++) {
    const char* file_path = file_paths[i];

    if (!path_parse (file_path, volume, &path)) {
      cprintf (context, "ERROR bad path '%s'\n", file_path);
      continue;
    }

    if ((ret = volume_open (volume)) < 0) {
      cprintf (context, "ERROR could not open volume '%s': %s\n", volume, str_status[-ret]);
      continue;
    }

    if ((ret = create_file (path)) < 0) {
      cprintf (context, "ERROR could not create file '%s': %s\n", file_path, str_status[-ret]);
    }

    volume_close ();
  }
}

static void terminfo (struct context* context) {
  size_t cols = 0, rows = 0;
  terminal_dimensions (&cols, &rows);
  cprintf (context, "%zu x %zu\n", cols, rows);
}

static void mkdir (struct context* context, char** dir_paths, size_t dirs_count) {
  char volume[VOLUME_MAX];
  const char* path;
  int ret;

  for (size_t i = 0; i < dirs_count; i++) {
    const char* dir_path = dir_paths[i];

    if (!path_parse (dir_path, volume, &path)) {
      cprintf (context, "ERROR bad path '%s'\n", dir_path);
      continue;
    }

    if ((ret = volume_open (volume)) < 0) {
      cprintf (context, "ERROR could not open volume '%s': %s\n", volume, str_status[-ret]);
      continue;
    }

    create_dir (path);

    volume_close ();
  }
}

static void rm (struct context* context, char** paths, size_t count) {
  char volume[VOLUME_MAX];
  const char* path;
  int ret;

  for (size_t i = 0; i < count; i++) {
    const char* path1 = paths[i];

    if (!path_parse (path1, volume, &path)) {
      cprintf (context, "ERROR bad path '%s'\n", path1);
      continue;
    }

    if ((ret = volume_open (volume)) < 0) {
      cprintf (context, "ERROR could not open volume '%s': %s\n", volume, str_status[-ret]);
      continue;
    }

    remove (path);

    volume_close ();
  }
}

static void cat (struct context* context, char** file_paths, size_t files_count) {
  char volume[VOLUME_MAX];
  const char* path;
  int ret;

  for (size_t i = 0; i < files_count; i++) {
    const char* file_path = file_paths[i];

    if (!path_parse (file_path, volume, &path)) {
      cprintf (context, "ERROR bad path '%s'\n", file_path);
      continue;
    }

    struct filereader fr;
    if ((ret = filereader_init (&fr, volume, path)) < 0) {
      cprintf (context, "ERROR could not initialize filereader for '%s:%s'\n", volume, path);
      return;
    }

    size_t chunk_size = 1024;
    char* buffer = arena_malloc (&arena, chunk_size);

    size_t chunks = fr.file_size / chunk_size;
    size_t rem = fr.file_size % chunk_size;

    for (size_t chunk = 0; chunk < chunks; chunk++) {
      if ((ret = filereader_read (&fr, (uint8_t*)buffer, chunk_size)) < 0) {
        filereader_fini (&fr);
        cprintf (context, "ERROR filereader failed to read from '%s:%s'\n", volume, path);
        return;
      }

      mail_send (e_pgid, buffer, chunk_size);
    }

    if (rem > 0) {
      if ((ret = filereader_read (&fr, (uint8_t*)buffer, rem)) < 0) {
        filereader_fini (&fr);
        cprintf (context, "ERROR filereader failed to read from '%s:%s'\n", volume, path);
        return;
      }

      mail_send (e_pgid, buffer, rem);
    }

    filereader_fini (&fr);
  }
}

static void ls (struct context* context, const char* path_string) {
  struct desc desc;
  char volume[VOLUME_MAX];
  const char* path;
  int ret;
  struct dir_entry entry;

  if (!path_parse (path_string, volume, &path)) {
    cprintf (context, "ERROR bad path '%s'\n", path_string);
    return;
  }

  if ((ret = volume_open (volume)) < 0) {
    cprintf (context, "ERROR could not open volume '%s': %s\n", volume, str_status[-ret]);
    return;
  }

  describe (path, &desc);

  if (desc.type != FS_DIR) {
    cprintf (context, "ERROR '%s' is not a directory\n", path_string);
    volume_close ();
    return;
  }

  size_t entries = desc.size;
  cprintf (context, "total entries: %zu\n", entries);
  cprintf (context, "\n");

  for (size_t entry_num = 0; entry_num < entries; entry_num++) {
    memset (&desc, 0, sizeof (desc));
    memset (&entry, 0, sizeof (entry));

    read_dir_entry (path, &entry, entry_num);
    describe (entry.path, &desc);

    char* hs_string;
    double hs;
    human_size ((double)desc.size, &hs, &hs_string);

    char size_buf[64];
    snprintf (size_buf, sizeof (size_buf), "%.2f %s", hs, hs_string);

    char type = (desc.type == FS_DIR ? 'D' : 'F');

    cprintf (context, "%c %-40s %-40s\n", type, entry.path, size_buf);
  }

  volume_close ();
}

static void edit (struct context* context, const char* path_string) {
  struct desc desc;
  char volume[VOLUME_MAX];
  const char* path;
  int ret;

  if (!path_parse (path_string, volume, &path)) {
    cprintf (context, "ERROR bad path '%s'\n", path_string);
    return;
  }

  if ((ret = volume_open (volume)) < 0) {
    cprintf (context, "ERROR could not open volume '%s': %s\n", volume, str_status[-ret]);
    return;
  }

  if ((ret = describe (path, &desc)) < 0) {
    cprintf (context, "ERROR could not describe '%s': %s\n", path, str_status[-ret]);
    volume_close ();
    return;
  }

  char* str = malloc (desc.size + 1);

  if (str == NULL) {
    volume_close ();
    return;
  }

  memset (str, 0, desc.size);

  if ((ret = read_file (path, 0, (uint8_t*)str, desc.size)) < 0) {
    free (str);
    volume_close ();
    return;
  }

  volume_close ();

  edit_start (volume, path, str, desc.size);
}

static void quit1 (struct context* context) {
  cprintf (context, "Goodbye!\n");
  interp_shutdown ();
}

static void cls (struct context* context) { cprintf (context, ANSIQ_CUR_HOME ANSIQ_SCR_CLR_ALL); }

static void mkvol (struct context* context, const char* volume, const char* str_fs_type,
                   const char* device) {
  int fs_type;

  if (strcmp (str_fs_type, "tar") == 0) {
    fs_type = FS_TARFS;
  } else if (strcmp (str_fs_type, "fat16") == 0) {
    fs_type = FS_FAT16;
  } else if (strcmp (str_fs_type, "fat32") == 0) {
    fs_type = FS_FAT32;
  } else {
    cprintf (context, "ERROR Unknown filesystem '%s'\n", str_fs_type);
    return;
  }

  int ret = create_volume (volume, fs_type, device);

  if (ret < 0)
    cprintf (context, "ERROR Could not create volume: %s\n", str_status[-ret]);
}

static void format (struct context* context, const char* device, const char* str_fs_type) {
  int fs_type;

  if (strcmp (str_fs_type, "tar") == 0) {
    fs_type = FS_TARFS;
  } else if (strcmp (str_fs_type, "fat16") == 0) {
    fs_type = FS_FAT16;
  } else if (strcmp (str_fs_type, "fat32") == 0) {
    fs_type = FS_FAT32;
  } else {
    cprintf (context, "ERROR Unknown filesystem '%s'\n", str_fs_type);
    return;
  }

  struct fatfs_ctx ctx;

  int ret = fat_format_drive (&ctx, device, fs_type);

  if (ret < 0)
    cprintf (context, "ERROR Could not format drive: %s\n", str_status[-ret]);
  else
    cprintf (context, "OK\n");
}

static void help (struct context* context) {
  cprintf (context, "Available commands:\n");
  cprintf (context, "help\n");
  cprintf (context, "echo         <word1> <word2> <word3> ...\n");
  cprintf (context, "cat          <file path>\n");
  cprintf (context, "ls           <directory path>\n");
  cprintf (context, "mkfile       <file path>\n");
  cprintf (context, "mkdir        <directory path>\n");
  cprintf (context, "rm           <path>\n");
  cprintf (context, "edit         <path>\n");
  cprintf (context, "terminfo\n");
  cprintf (context, "cls\n");
  cprintf (context, "mkvol        <volume> <filesystem> <device>\n");
  cprintf (context, "format       <device> <filesystem>\n");
  cprintf (context, "quit\n");
}

static void cmd_cancel_proc (void* arg) {
  int pid = *(int*)arg;

  char ch = 0;
  for (;;) {
    mail_receive (&ch, 1);

    if (ch == KB_CTRL ('C'))
      break;
  }

  kill (pid);
}

static void execute_cmd (struct ast_cmd* cmd, struct context* context) {
  if (strcmp (cmd->name, "echo") == 0) {
    echo (context, cmd->args, cmd->arg_count);
  } else if (strcmp (cmd->name, "help") == 0) {
    help (context);
  } else if (strcmp (cmd->name, "cat") == 0) {
    cat (context, cmd->args, cmd->arg_count);
  } else if (strcmp (cmd->name, "ls") == 0) {
    if (cmd->args[0] != NULL)
      ls (context, cmd->args[0]);
    else
      cprintf (context, "ERROR No directory path provided\n");
  } else if (strcmp (cmd->name, "quit") == 0) {
    quit1 (context);
  } else if (strcmp (cmd->name, "mkfile") == 0) {
    mkfile (context, cmd->args, cmd->arg_count);
  } else if (strcmp (cmd->name, "mkdir") == 0) {
    mkdir (context, cmd->args, cmd->arg_count);
  } else if (strcmp (cmd->name, "rm") == 0) {
    rm (context, cmd->args, cmd->arg_count);
  } else if (strcmp (cmd->name, "edit") == 0) {
    if (cmd->args[0] != NULL)
      edit (context, cmd->args[0]);
    else
      cprintf (context, "ERROR No file path provided\n");
  } else if (strcmp (cmd->name, "terminfo") == 0) {
    terminfo (context);
  } else if (strcmp (cmd->name, "cls") == 0) {
    cls (context);
  } else if (strcmp (cmd->name, "mkvol") == 0) {
    if ((cmd->args[0] != NULL) && (cmd->args[1] != NULL) && (cmd->args[2] != NULL))
      mkvol (context, cmd->args[0], cmd->args[1], cmd->args[2]);
    else
      cprintf (context, "ERROR No volume key, filesystem type or device key provided\n");
  } else if (strcmp (cmd->name, "format") == 0) {
    if ((cmd->args[0] != NULL) && (cmd->args[1] != NULL))
      format (context, cmd->args[0], cmd->args[1]);
    else
      cprintf (context, "ERROR No device key or filesystem type provided\n");
  } else {
    char volume[VOLUME_MAX];
    const char* path;

    if (!(path_parse (cmd->name, volume, &path))) {
      cprintf (context, "ERROR bad path '%s'\n", cmd->name);
      return;
    }

    int pid = exec (volume, path);

    if (pid < 0) {
      cprintf (context, "ERROR could not run '%s': %s\n", cmd->name, str_status[-pid]);
      return;
    }

    struct process_data* cancel_pdata = process_spawn (&cmd_cancel_proc, &pid);

    wait_for_pid (pid);

    if (kill (cancel_pdata->pid) < 0)
      free (cancel_pdata);
  }
}

static void execute_redir (struct ast_redir* redir, struct context* context) {
  char volume[VOLUME_MAX];
  const char* path;
  int ret;

  if (!(path_parse (redir->file_path, volume, &path))) {
    cprintf (context, "ERROR bad path '%s'\n", redir->file_path);
    return;
  }

  struct filewriter fw;
  if ((ret = filewriter_init (&fw, volume, path, FW_CREATE_FILE | FW_APPEND)) < 0) {
    cprintf (context, "ERROR could not initialize filewriter for '%s:%s'\n", volume, path);
    return;
  }

  size_t chunk_size = 1024;
  size_t chunks = (context->strbuf.count - 1) / chunk_size;
  size_t rem = (context->strbuf.count - 1) % chunk_size;

  for (size_t chunk = 0; chunk < chunks; chunk++) {
    if ((ret = filewriter_write (&fw, (uint8_t*)&context->strbuf.items[chunk * chunk_size],
                                 chunk_size)) < 0) {
      filewriter_fini (&fw);
      cprintf (context, "ERROR filewriter failed to write to '%s:%s'\n", volume, path);
      return;
    }
  }

  if (rem > 0) {
    if ((ret = filewriter_write (&fw, (uint8_t*)&context->strbuf.items[chunks * chunk_size], rem)) <
        0) {
      filewriter_fini (&fw);
      cprintf (context, "ERROR filewriter failed to write to '%s:%s'\n", volume, path);
      return;
    }
  }

  filewriter_fini (&fw);
}

void execute (struct ast_node* root, struct context* context) {
  struct context subcontext;
  memset (&subcontext, 0, sizeof (subcontext));

  switch (root->class) {
  case AST_NODE_CLASS_CMD:
    execute_cmd (&root->u.cmd, context);
    break;
  case AST_NODE_CLASS_SUBSHELL:
    execute (root->u.subshell.inner, &subcontext);
    break;
  case AST_NODE_CLASS_SEQ:
    execute (root->u.seq.left, context);
    execute (root->u.seq.right, context);
    break;
  case AST_NODE_CLASS_REDIR:
    execute (root->u.redir.source, &subcontext);
    execute_redir (&root->u.redir, &subcontext);
    break;
  }
}
