// Copyright (c) 2023 Dominic Masters
// 
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

#include "File.hpp"

using namespace Dawn;

std::string File::normalizeSlashes(std::string str) {
  size_t i = 0;
  while(i < str.size()) {
    auto c = str[i];
    if(c == '\\' || c == '/') str[i] = FILE_PATH_SEP;
    ++i;
  }
  return str;
}

void File::mkdirp(std::string path) {
  std::string buffer;
  char c;
  size_t i = 0;
  bool_t inFile;
  bool_t hasMore;

  inFile = false;
  hasMore = false;
  while(c = path[i]) {
    if((c == '\\' || c == '/') && i > 0) {
      fileMkdir(buffer.c_str(), 0755);
      inFile = false;
      hasMore = false;
      buffer += FILE_PATH_SEP;
      i++;
      continue;
    }

    if(c == '.') inFile = true;
    hasMore = true;
    buffer += c;
    i++;
  }

  if(!inFile && hasMore) {
    fileMkdir(buffer.c_str(), 0755);
  }
}

//

File::File(std::string filename) {
  this->filename = File::normalizeSlashes(filename);
}

bool_t File::open(enum FileMode mode) {
  assertNull(this->file, "File is already open");

  this->mode = mode;
  this->file = fopen(
    this->filename.c_str(),
    mode == FILE_MODE_READ ? "rb" : "wb"
  );

  if(this->file == NULL) return false;

  if(mode == FILE_MODE_READ) {
    fseek(this->file, 0, SEEK_END);
    this->length = ftell(this->file);
    fseek(this->file, 0, SEEK_SET);

    if(this->length <= 0) {
      this->close();
      return false;
    }
  } else {
    this->length = 0;
  }

  return true;
}

bool_t File::isOpen() {
  return this->file != nullptr;
}

bool_t File::exists() {
  if(this->file != nullptr) return true;
  FILE *f = fopen(this->filename.c_str(), "rb");
  if(f == NULL || f == nullptr) return false;
  fclose(f);
  return true;
}

void File::close() {
  assertNotNull(this->file, "File::close: File is not open");
  fclose(this->file);
  this->file = nullptr;
}

bool_t File::mkdirp() {
  File::mkdirp(this->filename);
  return true;
}

bool_t File::readString(std::string *out) {
  assertNotNull(out, "File::readString: Out cannot be null");

  if(!this->isOpen()) {
    if(!this->open(FILE_MODE_READ)) return false;
  }
  assertTrue(this->mode == FILE_MODE_READ, "File::readString: File must be open in read mode");
  out->clear();

  size_t i = 0;
  char buffer[FILE_BUFFER_SIZE + 1];// +1 for null term
  while(i != this->length) {
    size_t amt = mathMin<size_t>(FILE_BUFFER_SIZE, (this->length - i));
    auto amtRead = fread(buffer, sizeof(char), amt, this->file);
    if(amtRead != amt) return false;
    i += amtRead;
    buffer[amtRead] = '\0';
    out->append(buffer);
  }

  return true;
}

size_t File::readAhead(char *buffer, size_t max, char needle) {
  assertNotNull(buffer, "File::readAhead: Buffer cannot be null");
  assertTrue(max > 0, "File::readAhead: Max must be greater than 0");

  if(!this->isOpen()) {
    if(!this->open(FILE_MODE_READ)) return 0;
  }
  assertTrue(this->mode == FILE_MODE_READ, "File::readAhead: File must be open in read mode");

  // Buffer
  size_t pos = ftell(this->file);
  size_t amountLeftToRead = mathMin<size_t>(max, this->length - pos);
  char temporary[FILE_BUFFER_SIZE];
  size_t n = 0;
  
  while(amountLeftToRead > 0) {
    size_t toRead = mathMin<size_t>(amountLeftToRead, FILE_BUFFER_SIZE);
    amountLeftToRead -= toRead;
    // Read bytes
    size_t read = fread(temporary, sizeof(char), toRead, this->file);

    // Read error?
    if(toRead != read) return 0;

    // Did we read the needle?
    size_t i = 0;
    while(i < read) {
      char c = temporary[i++];
      if(c == needle) {
        return n;
      } else {
        buffer[n++] = c;
      }
    }
  }

  // Needle was not found.
  return -1;
}

size_t File::readToBuffer(char **buffer) {
  if(!this->isOpen()) {
    if(!this->open(FILE_MODE_READ)) return 0;
  }
  assertTrue(this->mode == FILE_MODE_READ, "File::readToBuffer: File must be open in read mode");

  if((*buffer) == nullptr) *buffer = (char*)malloc(this->length);
  fseek(this->file, 0, SEEK_SET);
  auto l = fread((*buffer), sizeof(char), this->length, this->file);
  return l;
}

size_t File::readRaw(char *buffer, size_t max) {
  assertNotNull(buffer, "File::readRaw: Buffer cannot be null");
  assertTrue(max > 0, "File::readRaw: Max must be greater than 0");
  if(!this->isOpen()) {
    if(!this->open(FILE_MODE_READ)) return 0;
  }
  assertTrue(this->mode == FILE_MODE_READ, "File::readRaw: File must be open in read mode");
  return fread(buffer, sizeof(char), max, this->file);
}

bool_t File::writeString(std::string in) {
  if(!this->isOpen() && !this->open(FILE_MODE_WRITE)) return false;
  assertTrue(this->mode == FILE_MODE_WRITE, "File::writeString: File must be open in write mode");
  return this->writeRaw((char *)in.c_str(), in.size()) && this->length == in.size();
}

bool_t File::writeRaw(char *data, size_t len) {
  if(!this->isOpen() && !this->open(FILE_MODE_WRITE)) return false;
  assertTrue(this->mode == FILE_MODE_WRITE, "File::writeRaw: File must be open in write mode");
  assertTrue(len > 0, "File::writeRaw: Length must be greater than 0");
  this->length = fwrite(data, sizeof(char_t), len, this->file);
  return true;
}

bool_t File::copyRaw(File *otherFile, size_t length) {
  assertTrue(length > 0, "File::copyRaw: Length must be greater than 0");
  assertTrue(otherFile->isOpen(), "File::copyRaw: Other file must be open");

  char buffer[FILE_BUFFER_SIZE];
  size_t amountLeftToRead = length;
  size_t read = 0;
  while(amountLeftToRead > 0) {
    auto iRead = otherFile->readRaw(buffer, mathMin<size_t>(FILE_BUFFER_SIZE, amountLeftToRead));
    if(iRead == 0) return false;
    this->writeRaw(buffer, iRead);
    amountLeftToRead -= iRead;
    read += iRead;
  }

  assertTrue(read == length, "File::copyRaw: Read length does not match expected length");
  return true;
}

void File::setPosition(size_t n) {
  fseek(this->file, 0, SEEK_SET);
  fseek(this->file, n, SEEK_CUR);
}

std::string File::getFileName(bool_t withExt) {
  // Remove all but last slash
  std::string basename;
  size_t lastSlash = this->filename.find_last_of('/');
  if(lastSlash == std::string::npos) {
    basename = this->filename;
  } else {
    basename = this->filename.substr(lastSlash + 1);
  }

  // Do we need to remove ext?
  if(withExt) return basename;
  size_t lastDot = basename.find_last_of('.');
  if(lastDot == std::string::npos) return basename;
  return basename.substr(0, lastDot);
}

std::string File::getExtension() {
  // Remove all but last slash
  std::string basename;
  size_t lastSlash = this->filename.find_last_of('/');
  if(lastSlash == std::string::npos) {
    basename = this->filename;
  } else {
    basename = this->filename.substr(lastSlash + 1);
  }

  size_t lastDot = basename.find_last_of('.');
  if(lastDot == std::string::npos) return "";
  return basename.substr(lastDot + 1);
}


File::~File() {
  if(this->file != nullptr) this->close();
}