#include "ka_indvd.h"

#define RISCOS_FILE_HANDLING 1

#include <stdlib.h>

#include <string.h>
#include "config.h"
#include "ka_error.h"
#include "ka_log.h"
#include "ka_mem.h"
#include "ro_file.h"
#include "dvd/dvd_io.h"

#ifdef RISCOS_FILE_HANDLING
#include "swis.h"
#else
#include <stdio.h>
#endif

#define SECTOR_SIZE 2048

typedef struct
{
  ka_input_t hdr;
#ifdef RISCOS_FILE_HANDLING
  uint32_t   handle;             // RISC OS file handle
#else
  FILE*      handle;             // C lib file handle
#endif
  uint64_t   inputLen;           // size in bytes of current stream
  uint64_t   start;
  uint64_t   end;
  uint64_t   pos;
  // DVD device
  dvd_io_t*  dvdIo;
  int        css; // 1 if must use title key
  dvd_key_t  titleKey;
  char       inputname[MAXPATH]; // full pathname of current stream
  uint8_t    buffer[SECTOR_SIZE + 4];
} data_t;

/**
 * Returns the length in bytes of the input stream.
 *
 * @param  f         Input stream handle.
 *
 * @returns Length in bytes of the input stream.
 */
static uint64_t ka_indvd_getLen(ka_input_t* pInput)
{
  data_t* f = (data_t*) pInput;

  return (uint64_t) f->inputLen;
}

/**
 * Closes the input stream.
 *
 * @param  f         Input stream handle.
 *
 * @returns negative value in case of error.
 */
static int ka_indvd_close(ka_input_t* pInput)
{
  data_t* f = (data_t*) pInput;

  if (f->inputname[0] == ':')
  {
    // DVD
    return 0;
  }
  else
  {
    if (f->handle)
    {
#ifdef RISCOS_FILE_HANDLING
      // Close file
      if (_swix(OS_Find, _INR(0,1), 0, f->handle))
        return -1;
#else
      fclose(f->handle);
#endif
      f->handle = 0;
    }
  }

  return 0;
}

/**
 * Changes from position within the input stream.
 *
 * @param  f         Input stream handle.
 * @param  pos       New position.
 *
 * @returns negative value in case of error.
 */
static int ka_indvd_seek(ka_input_t* pInput, uint64_t pos)
{
  data_t* f = (data_t*) pInput;

  // Set file pos
  pos &= ~(SECTOR_SIZE-1);
  if (pos < f->start) pos = f->start;
  if (pos > f->end) pos = f->end;
  f->pos = pos;

  if (f->inputname[0] == ':')
  {
    // DVD
    return 0;
  }
  else
  {
#ifdef RISCOS_FILE_HANDLING
    _swix(OS_Args, _INR(0,2), 1, f->handle, (uint32_t) (pos - f->start));

    return 0;
#else
    return fseek(f->handle, (long int) (pos - f->start), SEEK_SET);
#endif
  }
}

/**
 * Opens the input stream.
 *
 * @param  f         Input stream handle.
 *
 * @returns non-0 value in case of error.
 */
int ka_indvd_open(ka_input_t* pInput, const char* path, const char* inputname, uint64_t start, uint64_t end, dvd_key_t titleKey)
{
  data_t* f = (data_t*) pInput;

  ka_indvd_close(pInput);

  snprintf(f->inputname, sizeof(f->inputname), "%s.%s", path, inputname);
  f->css = 0;

  if (!strncmp(path, "::CDFS", 6))
  {
    if (titleKey && (titleKey[0] || titleKey[1] || titleKey[2] || titleKey[3] || titleKey[4]))
    {
      memcpy(f->titleKey, titleKey, DVD_KEY_SIZE);
      f->css = 1;
    }

    f->inputLen = end - start;
  }
  else
  {
#ifdef RISCOS_FILE_HANDLING
    const _kernel_oserror* err = NULL;
    uint32_t inputLen;

    // Open file in read mode
    err = _swix(OS_Find, _INR(0,1)|_OUT(0), 0x4f, f->inputname, &f->handle);
    // Read file size
    if (!err) err = _swix(OS_Args, _INR(0,1)|_OUT(2), 2, f->handle, &inputLen);

    if (err)
    {
      ka_log(ka_log_error, "%s", err->errmess);
      ka_error_fill(f->hdr.pErrorBlock, "error2:%s", f->inputname);
      return -1;
    }
    f->inputLen = (uint64_t) inputLen;
#else
    // Open file in read mode
    f->handle = fopen(f->inputname, "rb");

    if (!f->handle)
    {
      ka_error_fill(f->hdr.pErrorBlock, "error2:%s", f->inputname);
      return -1;
    }

    // Read file size
    fseek(f->handle, 0, SEEK_END);
    f->inputLen = (uint64_t) ftell(f->handle);
#endif
  }

  f->start = start;
  f->end = end;
  if (f->end > start + f->inputLen) f->end = start + f->inputLen;
  ka_indvd_seek(pInput, f->start);

  return 0;
}

/**
 * Read data from current position in stream.
 *
 * @param  f         Input stream handle.
 * @param  ptr       Destination pointer where to store the data.
 * @param  nbr       Number of bytes to read.
 *
 * @return Number of bytes read.
 */
#define MAX_NR_BLOCKS 64

static int ka_indvd_read(ka_input_t* pInput, void* ptr, int nbr)
{
  data_t* f = (data_t*) pInput;

  if ((f->end - f->pos) < nbr)
    nbr = (int) (f->end - f->pos);

  if (nbr <= 0)
    return 0;

  if (f->inputname[0] == ':')
  {
    // DVD
    const _kernel_oserror* err = NULL;
    int toread = nbr;
    int read;
    uint32_t lba = (uint32_t) (f->pos / SECTOR_SIZE);
    read = (uint32_t) (f->pos % SECTOR_SIZE);

    if (read)
    {
      // partial read from end of first sector, use a buffer
      err = dvd_io_readBlocks(f->dvdIo, lba, f->buffer, 1);
      if (err) goto cd_error;

      if (f->css)
        dvd_io_unscramble(f->dvdIo, f->titleKey, f->buffer);
      // copy relevant part of buffer to destination
      uint8_t* p = f->buffer + read;
      read = SECTOR_SIZE - read;
      if (read > nbr)
        read = nbr;
      memcmp(ptr, p, read);
      ptr = ((uint8_t*) ptr) + read;
      toread -= nbr;

      // next read starts from next sector
      lba++;
    }

    // sectors to read
    read = toread / SECTOR_SIZE;
    while (read > 0)
    {
      int nr = read;//(read > MAX_NR_BLOCKS) ? MAX_NR_BLOCKS : read;
      err = dvd_io_readBlocks(f->dvdIo, lba, ptr, nr);
      if (err) goto cd_error;

      for (int i = nr; i > 0; i--)
      {
        if (f->css)
          dvd_io_unscramble(f->dvdIo, f->titleKey, ptr);
        ptr = ((uint8_t*) ptr) + SECTOR_SIZE;
      }
      read -= nr;
      lba += nr;
      toread -= nr* SECTOR_SIZE;
    }

    if (toread)
    {
      // partial read from start of last, use a buffer
      err = dvd_io_readBlocks(f->dvdIo, lba, f->buffer, 1);
      if (err) goto cd_error;

      if (f->css)
        dvd_io_unscramble(f->dvdIo, f->titleKey, f->buffer);
      // copy relevant part of buffer to destination
      memcmp(ptr, f->buffer, toread);
      toread = 0;
    }

cd_error:
    if (err)
    {
      *f->hdr.pErrorBlock = *err;
      return -1;
    }
    nbr -= toread;
  }
  else
  {
#ifdef RISCOS_FILE_HANDLING
    const _kernel_oserror* err = NULL;
    int toread;

    // Read bytes in file
    err = _swix(OS_GBPB, _INR(0,3)|_OUT(3), 4, f->handle, ptr, nbr, &toread);
    if (err)
    {
      *f->hdr.pErrorBlock = *err;
      return -1;
    }
    nbr -= toread;
#else
    // Read bytes in file
    nbr = fread(ptr, 1, nbr, f->handle);
#endif
  }

  f->pos += (uint64_t) nbr;
  return nbr;
}

/**
 * Returns the input stream position.
 *
 * @param  f         Input stream handle.
 *
 * @returns Position within the input stream.
 */
static uint64_t ka_indvd_tell(ka_input_t* pInput)
{
  data_t* f = (data_t*) pInput;

  return f->pos;
}

/**
 * Read data from current position in stream and add it to buffer.
 *
 * @param  f         Input stream handle.
 * @param  buffer    Buffer handle.
 * @param  pdone     EOF indecator to fill on exit.
 *
 * @returns Number of bytes read.
 */
static int ka_indvd_buffer(ka_input_t* pInput, ka_buffer_t* pbuffer, int* pdone)
{
  uint8_t* pstart;
  uint8_t* pend;
  int len = ka_buffer_getFreeBlock(pbuffer, &pstart, &pend);
  int read = 0;

  *pdone = 0;

  if (len)
  {
    // Fill free block of data
    read = ka_indvd_read(pInput, pstart, len);
    // Error ?
    if (read < 0)
      return -1;
    ka_buffer_setWritten(pbuffer, pstart + read);

    // Data was read, it will have to be pushed on stack
    // No more additional data will come
    if (read == 0)
      *pdone = 1;
  }

  return read;
}

/**
 * Releases the input stream and all its data.
 *
 * @param  f         Input stream handle to delete.
 */
static void ka_delete_indvd(ka_input_t** ppInput)
{
  data_t* f = (data_t*) *ppInput;
  *ppInput = NULL;

  if (!f)
    return;

  ka_indvd_close(&f->hdr);

  // DVD
  dvd_io_delete(&f->dvdIo);

  ka_mem_free(f);
}

static const kav_input_t kav_indvd =
{ .name = "File"
, .FNdelete = ka_delete_indvd
, .FNclose  = ka_indvd_close
, .FNgetLen = ka_indvd_getLen
, .FNtell   = ka_indvd_tell
, .FNhasBytes = NULL
, .FNskip   = NULL
, .FNseek   = ka_indvd_seek
, .FNread   = ka_indvd_read
, .FNbuffer = ka_indvd_buffer
};

/**
 * Creates a new input stream handler.
 *
 * @param  inputname  Name of the input which is to serve as input.
 *
 * @returns Handle to new input stream.
 */
ka_input_t* ka_new_indvd(ka_error_t* pErrorBlock)
{
  data_t* f = ka_mem_calloc(sizeof(*f));

  if (!f)
  {
    ka_error_fill(pErrorBlock, ka_error_nomem);
    return NULL;
  }
  f->hdr.vptr = &kav_indvd;
  f->hdr.pErrorBlock = pErrorBlock;

  return &f->hdr;
}

int ka_indvd_initDevice(ka_input_t* pInput, const char* path)
{
  data_t* f = (data_t*) pInput;

 if (strncmp(path, "::CDFS", 6))
 {
    ka_error_fill(f->hdr.pErrorBlock, "Not a device");
    return -1;
 }

  int drive = atoi(path + 6);

  f->dvdIo = dvd_io_new(f->hdr.pErrorBlock, drive);
  if (f->dvdIo == NULL)
  {
    ka_error_t errBlock = *f->hdr.pErrorBlock;

    ka_log(ka_log_error, "DVD: %x - %s", errBlock.errnum, errBlock.errmess);
    ka_error_fill(f->hdr.pErrorBlock, "error201:%s", errBlock.errmess);
    return -1;
  }

  return 0;
}

dvd_io_t* ka_indvd_getDvdIo(ka_input_t* pInput)
{
  data_t* f = (data_t*) pInput;

  return f->dvdIo;
}
