#include <dio.h>
#include <cc65.h>
#include <errno.h>
#include <ctype.h>
#include <fcntl.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <dirent.h>
#include <device.h>

#include "../inc/ip65.h"
#include "w5100.h"
#include "w5100_http.h"
#include "linenoise.h"

// Both pragmas are obligatory to have cc65 generate code
// suitable to access the W5100 auto-increment registers.
#pragma optimize      (on)
#pragma static-locals (on)

// Size needs to be exactly one track of a 16-sector disk
char buffer[0x1000];
char name[16];

void ip65_error_exit(void)
{
  printf("- %s\n", ip65_strerror(ip65_error));
  exit(EXIT_FAILURE);
}

void file_error_exit(void)
{
  printf("- ");
  perror(NULL);
  exit(EXIT_FAILURE);
}

void dio_error_exit(void)
{
  _seterrno(_osmaperrno(_oserror));
  file_error_exit();
}

void confirm_exit(void)
{
  printf("\nPress any key ");
  cgetc();
}

void reset_cwd(void)
{
  chdir("");
}

char *self_path(const char *filename)
{
  extern char **_argv[];

  return strcat(strcpy(buffer, *_argv[0]), filename);
}

bool match(const char *filter, const char *string)
{
  while (*filter)
  {
    if (!*string)
    {
      return false;
    }
    if (toupper(*filter++) != toupper(*string++))
    {
      return false;
    }
  }
  return true;
}

void url_completion(const char *line, linenoiseCompletions *lc)
{
  if (match(line, "http://"))
  {
    linenoiseAddCompletion(lc, "http://");
  }
  if (match(line, "http://www."))
  {
    linenoiseAddCompletion(lc, "http://www.");
  }
}

void file_completion(const char *line, linenoiseCompletions *lc) {
  char *lineptr;
  DIR *dir;
  struct dirent *ent;
  char *bufferptr;

  // Add devices
  if (line[0] == '!')
  {
    char device = getfirstdevice();
    while (device != INVALID_DEVICE)
    {
      dhandle_t dio = dio_open(device);
      if (dio)
      {
        // dio_query_sectcount() fails if there's no (formatted) disk
        // in the drive but (in contrast to getdevicedir) it succeeds
        // with a non-ProDOS 16-sector 140k disk which is exactly the
        // check we want here.
        if (dio_query_sectcount(dio))
        {
          sprintf(buffer, "!S%d,D%d", device & 7, (device >> 3) + 1);
          if (match(line, buffer))
          {
            linenoiseAddCompletion(lc, buffer);
          }
        }
        dio_close(dio);
      }
      device = getnextdevice(device);
    }
    return;
  }

  lineptr = strrchr(line, '/');

  // Add device names
  if (lineptr == line)
  {
    char device = getfirstdevice();
    while (device != INVALID_DEVICE)
    {
      if (getdevicedir(device, buffer, sizeof(buffer)))
      {
        if (match(line, buffer))
        {
          linenoiseAddCompletion(lc, buffer);
        }
      }
      device = getnextdevice(device);
    }
    return;
  }

  // Add directory entries
  //
  // Absolute or relative path
  if (lineptr)
  {
    *lineptr = '\0';
    dir = opendir(line);
    *lineptr = '/';
    ++lineptr;
  }

  // Current directory
  else
  {
    dir = opendir(".");
    lineptr = (char*)line;
  }

  strcpy(buffer, line);
  bufferptr = buffer + (lineptr - line);

  if (name[0] && match(lineptr, name))
  {
    strcpy(bufferptr, name);
    linenoiseAddCompletion(lc, buffer);
  }

  if (!dir)
  {
    return;
  }

  while (ent = readdir(dir))
  {
    if (match(lineptr, ent->d_name))
    {
      strcpy(bufferptr, ent->d_name);
      linenoiseAddCompletion(lc, buffer);
    }
  }
  closedir(dir);
}

char *get_argument(char arg, const char *name,
                   linenoiseCompletionCallback *completion)
{
  extern int _argc;
  extern char **_argv[];
  char *val;

  if (_argc > arg)
  {
    val = (*_argv)[arg];
    printf("%s: %s", name, val);
  }
  else
  {
    char prompt[10];

    linenoiseSetCompletionCallback(completion);

    snprintf(prompt, sizeof(prompt), "%s? ", name);
    val = linenoise(prompt);
    if (!val || !*val)
    {
      putchar('\n');
      exit(EXIT_FAILURE);
    }
  }

  linenoiseHistoryAdd(val);
  return val;
}

void load_argument(const char *name)
{
  linenoiseHistoryReset();
  linenoiseHistoryLoad(self_path(name));
}

void save_argument(const char *name)
{
  linenoiseHistorySave(self_path(name));
}

void exit_on_key(void)
{
  if (input_check_for_abort_key())
  {
    w5100_disconnect();
    printf("- User abort\n");
    exit(EXIT_FAILURE);
  }
}

void write_file(const char *name)
{
  uint16_t i;
  int file;
  uint16_t rcv;
  bool cont = true;
  uint16_t len = 0;
  uint32_t size = 0;

  printf("- Ok\n\nOpening file ");
  file = open(name, O_WRONLY | O_CREAT | O_TRUNC);
  if (file == -1)
  {
    w5100_disconnect();
    file_error_exit();
  }
  printf("- Ok\n\n");

  while (cont)
  {
    exit_on_key();

    rcv = w5100_receive_request();
    if (!rcv)
    {
      cont = w5100_connected();
      if (cont)
      {
        continue;
      }
    }

    if (rcv > sizeof(buffer) - len)
    {
      rcv = sizeof(buffer) - len;
    }

    {
      // One less to allow for faster pre-increment below
      char *dataptr = buffer + len - 1;
      for (i = 0; i < rcv; ++i)
      {
        // The variable is necessary to have cc65 generate code
        // suitable to access the W5100 auto-increment register.
        char data = *w5100_data;
        *++dataptr = data;
      }
    }

    w5100_receive_commit(rcv);
    len += rcv;

    if (cont && len < sizeof(buffer))
    {
      continue;
    }

    cprintf("\rWriting ");
    if (write(file, buffer, len) != len)
    {
      w5100_disconnect();
      file_error_exit();
    }
    size += len;
    cprintf("%lu bytes ", size);

    len = 0;
  }

  printf("- Ok\n\nClosing file ");
  if (close(file))
  {
    w5100_disconnect();
    file_error_exit();
  }
}

void write_device(char device)
{
  uint16_t i;
  dhandle_t dio;
  uint16_t rcv;
  bool prodos;
  bool cont = true;
  uint16_t len = 0;
  uint16_t num = 0;

  printf("- Ok\n\nOpening drive ");
  dio = dio_open(device);
  if (!dio)
  {
    w5100_disconnect();
    dio_error_exit();
  }

  printf("- Ok\n\nSector order ");

  // The name extension "[P]roDOS sector [O]rder"
  // overrides the default as it's both unambiguous
  // and suitable for any ProDOS drive / disk type.
  if (!strcasecmp(name + strlen(name) - 3, ".PO"))
  {
    prodos = true;
  }

  // Every 140k disk image without .PO extension
  // can be presumed to have DOS 3.3 sector order
  // (usually with extension .DSK or .DO).
  // For all other disk images the DOS 3.3 sector
  // simply just doesn't make any sense.
  else
  {
    prodos = dio_query_sectcount(dio) != 280;
  }
  printf("- %s\n\n", prodos ? "ProDOS" : "DOS 3.3");

  while (cont)
  {
    exit_on_key();

    rcv = w5100_receive_request();
    if (!rcv)
    {
      cont = w5100_connected();
      if (cont)
      {
        continue;
      }
    }

    {
      // Skewing table containing page offsets to write the successive
      // pages read from the W5100 to - depends on buffer size of 4k !
      static char skew[0x10] = {0x0, 0xE, 0xD, 0xC, 0xB, 0xA, 0x9, 0x8,
                                0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0x1, 0xF};
      char *dataptr;

      if (prodos)
      {
        if (rcv > sizeof(buffer) - len)
        {
          rcv = sizeof(buffer) - len;
        }

        // One less to allow for faster pre-increment below
        dataptr = buffer + len - 1;
      }
      else
      {
        // Read each page from W5100 individually
        if (rcv > 0x100 - len % 0x100)
        {
          rcv = 0x100 - len % 0x100;
        }

        // One less to allow for faster pre-increment below
        dataptr = buffer + (skew[len / 0x100] << 8 | len % 0x100) - 1;
      }

      for (i = 0; i < rcv; ++i)
      {
        // The variable is necessary to have cc65 generate code
        // suitable to access the W5100 auto-increment register.
        char data = *w5100_data;
        *++dataptr = data;
      }
    }

    w5100_receive_commit(rcv);
    len += rcv;

    if (cont && len < sizeof(buffer))
    {
      continue;
    }

    cprintf("\rWriting ");
    for (i = 0; i < len; i += 0x200)
    {
      if (dio_write(dio, num++, buffer + i))
      {
        w5100_disconnect();
        dio_error_exit();
      }
    }
    cprintf("%lu bytes ", num * 0x200UL);

    len = 0;
  }

  printf("- Ok\n\nClosing drive ");
  if (dio_close(dio))
  {
    w5100_disconnect();
    dio_error_exit();
  }
}

int main(int, char *argv[])
{
  uint16_t i;
  char *arg;
  char device;
  uint8_t eth_init = ETH_INIT_DEFAULT;

  if (doesclrscrafterexit())
  {
    atexit(confirm_exit);
  }

  if (!*getcwd(buffer, sizeof(buffer)))
  {
    // Set a defined working dir before potentially changing devices
    chdir(getdevicedir(getcurrentdevice(), buffer, sizeof(buffer)));
    atexit(reset_cwd);
  }

  // Trim program name from argv[0] to prepare usage in self_path()
  arg = strrchr(argv[0], '/');
  if (arg) {
    *(arg + 1) = '\0';
  }
  else
  {
    *argv[0] = '\0';
  }

  {
    int file;

    printf("\nSetting slot ");
    file = open(self_path("ethernet.slot"), O_RDONLY);
    if (file != -1)
    {
      read(file, &eth_init, 1);
      close(file);
      eth_init &= ~'0';
    }
  }

  printf("- %d\n\nInitializing %s ", eth_init, eth_name);
  if (ip65_init(eth_init))
  {
    ip65_error_exit();
  }

  // Abort on Ctrl-C to be consistent with Linenoise
  abort_key = 0x83;

  printf("- Ok\n\nObtaining IP address ");
  if (dhcp_init())
  {
    ip65_error_exit();
  }
  printf("- Ok\n\n");

  // Copy IP config from IP65 to W5100
  w5100_config(eth_init);

  load_argument("wget.urls");
  while (true)
  {
    arg = get_argument(1, "URL", url_completion);

    printf("\n\nProcessing URL ");
    if (!url_parse(arg))
    {
      break;
    }

    printf("- %s\n\n", ip65_strerror(ip65_error));
  }
  save_argument("wget.urls");
  printf("- Ok\n\n");

  // Try to derive a ProDOS file
  // name (proposal) from the URL
  {
    char *c = strrchr(arg, '/');
    char *dot = NULL;

    i = 0;
    if (c && c[-1] != '/')
    {
      while (*++c)
      {
        // Name must begin with a letter
        if (!i && !isalpha(*c))
        {
          continue;
        }
        // Uppercase looks more familiar
        if (isalnum(*c))
        {
          buffer[i] = toupper(*c);
        }
        // Replace URL encoded char with dot
        else if (*c == '%')
        {
          buffer[i] = '.';
          if (c[1] && c[2])
          {
            c += 2;
          }
        }
        // Memorize begin of name extension
        else if (*c == '.')
        {
          buffer[i] = '.';
          dot = &buffer[i];
        }
        else
        {
          continue;
        }
        ++i;
      }
    }
    buffer[i] = '\0';

    strncpy(name, buffer, sizeof(name) - 1);

    // Rather cut from base name than from name extension
    if (i > sizeof(name) - 1 && dot)
    {
      uint16_t len = strlen(dot);

      // But keep at least one letter from base name
      if (len > sizeof(name) - 1 - 1)
      {
        len = sizeof(name) - 1 - 1;
      }
      strncpy(name + sizeof(name) - 1 - len, dot, len);
    }
  }

  load_argument("wget.files");
  while (true)
  {
    arg = get_argument(2, "File", file_completion);

    if (arg[0] != '!')
    {
      device = 0;
      break;
    }

    printf("\n\nChecking drive ");

    // !S[1..7],D[1|2]
    if (toupper(arg[1]) == 'S' &&
        arg[2] >= '1' && arg[2] <= '7' &&
        arg[3] == ',' &&
        toupper(arg[4]) == 'D' &&
        arg[5] >= '1' && arg[5] <= '2' &&
        arg[6] == '\0')
    {
      dhandle_t dio;

      device = arg[2] - '0' | arg[5] - '1' << 3;

      // dio_open() succeeeds for every connected drive
      // no matter if it contains any disk at all
      dio = dio_open(device);
      if (dio)
      {
        // dio_query_sectcount() succeeds for every (formatted)
        // 16-sector 140k disk no matter if it is a ProDOS disk
        if (dio_query_sectcount(dio))
        {
          // getdevicedir() succeeds only for ProDOS disks so
          // tell the user the ProDOS volume name of that disk
          // to make sure he doesn't overwrite some important
          // (hard) disk but do not bother him in the (usual?)
          // case of a non-ProDOS 16-sector 140k (game?) disk
          if (getdevicedir(device, buffer, sizeof(buffer)))
          {
            char oldcursor;
            char c;

            printf("- Ok\n\n");
            cprintf("Clobber %s? ", buffer);

            oldcursor = cursor(true);
            c = cgetc();
            cursor(oldcursor);
            if (toupper(c) == 'Y')
            {
              printf("- Yes");
              break;
            }
            printf("- No\n\n");
          }
          else
          {
            printf("- Ok");
            break;
          }
        }
        else
        {
          printf("- Invalid disk\n\n");
        }
        dio_close(dio);
      }
      else
      {
        printf("- Invalid drive\n\n");
      }
    }
    else
    {
      printf("- Malformed drive spec\n\n");
    }
  }
  save_argument("wget.files");

  printf("\n\n");
  if (!w5100_http_open(url_ip, url_port, url_selector, buffer, sizeof(buffer)))
  {
    return EXIT_FAILURE;
  }

  if (device)
  {
    write_device(device);
  }
  else
  {
    write_file(arg);
  }

  printf("- Ok\n\nDisconnecting ");
  w5100_disconnect();

  printf("- Ok\n");
  return EXIT_SUCCESS;
}
