#include <frostbite2D/resource/asset.h>
#include <fstream>
#include <sstream>
#include <system_error>
#include <algorithm>
#include <cctype>

namespace frostbite2D {

Asset &Asset::get() {
  static Asset instance;
  return instance;
}

bool Asset::isDevicePath(const std::string &path) const {
  auto colon = path.find(':');
  if (colon == std::string::npos || colon == 0) {
    return false;
  }

  for (size_t i = 0; i < colon; ++i) {
    unsigned char ch = static_cast<unsigned char>(path[i]);
    if (!std::isalnum(ch) && ch != '_' && ch != '-') {
      return false;
    }
  }

  return colon + 1 < path.size() &&
         (path[colon + 1] == '/' || path[colon + 1] == '\\');
}

fs::path Asset::toPath(const std::string &path) const {
#ifdef _WIN32
  return fs::u8path(path);
#else
  return fs::path(path);
#endif
}

std::string Asset::fromPath(const fs::path &path) const {
#ifdef _WIN32
  return path.u8string();
#else
  return path.string();
#endif
}

std::string Asset::resolveFullPath(const std::string &path) const {
  if (path.empty()) {
    return path;
  }

  fs::path p = toPath(path);
  if (p.is_absolute() || isDevicePath(path)) {
    return path;
  }

  if (!workingDirectory_.empty()) {
    return fromPath(toPath(workingDirectory_) / p);
  }

  return path;
}

bool Asset::readTextFile(const std::string &path, std::string &outContent) {
  std::string fullPath = resolveFullPath(path);

  if (!exists(fullPath)) {
    return false;
  }

  std::ifstream file(toPath(fullPath), std::ios::in | std::ios::binary);
  if (!file.is_open()) {
    return false;
  }

  std::ostringstream ss;
  ss << file.rdbuf();
  outContent = ss.str();
  file.close();
  return true;
}

bool Asset::writeTextFile(const std::string &path, const std::string &content,
                          bool append) {
  std::string fullPath = resolveFullPath(path);

  auto mode = std::ios::out;
  if (append) {
    mode |= std::ios::app;
  }

  std::ofstream file(toPath(fullPath), mode);
  if (!file.is_open()) {
    return false;
  }

  file << content;
  file.close();
  return true;
}

bool Asset::readBinaryFile(const std::string &path,
                           std::vector<uint8> &outData) {
  std::string fullPath = resolveFullPath(path);

  if (!exists(fullPath)) {
    return false;
  }

  std::ifstream file(toPath(fullPath), std::ios::in | std::ios::binary);
  if (!file.is_open()) {
    return false;
  }

  file.seekg(0, std::ios::end);
  auto size = file.tellg();
  file.seekg(0, std::ios::beg);

  outData.resize(static_cast<size_t>(size));
  file.read(reinterpret_cast<char *>(outData.data()), size);
  file.close();
  return true;
}

bool Asset::writeBinaryFile(const std::string &path,
                            const std::vector<uint8> &data, bool append) {
  std::string fullPath = resolveFullPath(path);

  auto mode = std::ios::out | std::ios::binary;
  if (append) {
    mode |= std::ios::app;
  }

  std::ofstream file(toPath(fullPath), mode);
  if (!file.is_open()) {
    return false;
  }

  file.write(reinterpret_cast<const char *>(data.data()), data.size());
  file.close();
  return true;
}

bool Asset::exists(const std::string &path) const {
  std::error_code ec;
  return fs::exists(toPath(resolveFullPath(path)), ec);
}

bool Asset::isDirectory(const std::string &path) const {
  std::error_code ec;
  return fs::is_directory(toPath(resolveFullPath(path)), ec);
}

bool Asset::isRegularFile(const std::string &path) const {
  std::error_code ec;
  return fs::is_regular_file(toPath(resolveFullPath(path)), ec);
}

bool Asset::createDirectory(const std::string &path) {
  std::error_code ec;
  return fs::create_directory(toPath(resolveFullPath(path)), ec);
}

bool Asset::createDirectories(const std::string &path) {
  std::error_code ec;
  return fs::create_directories(toPath(resolveFullPath(path)), ec);
}

bool Asset::removeFile(const std::string &path) {
  std::error_code ec;
  return fs::remove(toPath(resolveFullPath(path)), ec);
}

bool Asset::removeDirectory(const std::string &path) {
  std::error_code ec;
  return fs::remove_all(toPath(resolveFullPath(path)), ec) > 0;
}

bool Asset::copyFile(const std::string &from, const std::string &to,
                     bool overwrite) {
  std::error_code ec;
  auto options =
      overwrite ? fs::copy_options::overwrite_existing : fs::copy_options::none;
  return fs::copy_file(toPath(resolveFullPath(from)),
                       toPath(resolveFullPath(to)), options, ec);
}

bool Asset::moveFile(const std::string &from, const std::string &to) {
  std::error_code ec;
  fs::rename(toPath(resolveFullPath(from)), toPath(resolveFullPath(to)), ec);
  return !ec;
}

bool Asset::rename(const std::string &oldPath, const std::string &newPath) {
  std::error_code ec;
  fs::rename(toPath(resolveFullPath(oldPath)), toPath(resolveFullPath(newPath)),
             ec);
  return !ec;
}

std::vector<std::string> Asset::listFiles(const std::string &directoryPath,
                                          bool recursive) {
  std::vector<std::string> files;
  fs::path fullPath = toPath(resolveFullPath(directoryPath));

  std::error_code ec;
  if (!fs::exists(fullPath, ec) || !fs::is_directory(fullPath, ec)) {
    return files;
  }

  try {
    if (recursive) {
      for (const auto &entry : fs::recursive_directory_iterator(fullPath, ec)) {
        if (entry.is_regular_file()) {
          files.push_back(fromPath(entry.path()));
        }
      }
    } else {
      for (const auto &entry : fs::directory_iterator(fullPath, ec)) {
        if (entry.is_regular_file()) {
          files.push_back(fromPath(entry.path()));
        }
      }
    }
  } catch (...) {
  }

  return files;
}

std::vector<std::string>
Asset::listDirectories(const std::string &directoryPath, bool recursive) {
  std::vector<std::string> directories;
  fs::path fullPath = toPath(resolveFullPath(directoryPath));

  std::error_code ec;
  if (!fs::exists(fullPath, ec) || !fs::is_directory(fullPath, ec)) {
    return directories;
  }

  try {
    if (recursive) {
      for (const auto &entry : fs::recursive_directory_iterator(fullPath, ec)) {
        if (entry.is_directory()) {
          directories.push_back(fromPath(entry.path()));
        }
      }
    } else {
      for (const auto &entry : fs::directory_iterator(fullPath, ec)) {
        if (entry.is_directory()) {
          directories.push_back(fromPath(entry.path()));
        }
      }
    }
  } catch (...) {
  }

  return directories;
}

std::vector<std::string> Asset::listAll(const std::string &directoryPath,
                                        bool recursive) {
  std::vector<std::string> items;
  fs::path fullPath = toPath(resolveFullPath(directoryPath));

  std::error_code ec;
  if (!fs::exists(fullPath, ec) || !fs::is_directory(fullPath, ec)) {
    return items;
  }

  try {
    if (recursive) {
      for (const auto &entry : fs::recursive_directory_iterator(fullPath, ec)) {
        items.push_back(fromPath(entry.path()));
      }
    } else {
      for (const auto &entry : fs::directory_iterator(fullPath, ec)) {
        items.push_back(fromPath(entry.path()));
      }
    }
  } catch (...) {
  }

  return items;
}

std::vector<std::string>
Asset::listFilesWithExtension(const std::string &directoryPath,
                               const std::string &extension, bool recursive) {
  std::vector<std::string> files;
  fs::path fullPath = toPath(resolveFullPath(directoryPath));
  
  std::string extLower = extension;
  std::transform(extLower.begin(), extLower.end(), extLower.begin(),
                [](unsigned char c) { return std::tolower(c); });

  std::error_code ec;
  if (!fs::exists(fullPath, ec) || !fs::is_directory(fullPath, ec)) {
    return files;
  }

  try {
    auto checkExtension = [&](const fs::path& filePath) {
      std::string fileExt = fromPath(filePath.extension());
      std::transform(fileExt.begin(), fileExt.end(), fileExt.begin(),
                    [](unsigned char c) { return std::tolower(c); });
      return fileExt == extLower;
    };

    if (recursive) {
      for (const auto &entry : fs::recursive_directory_iterator(fullPath, ec)) {
        if (entry.is_regular_file() && checkExtension(entry.path())) {
          files.push_back(fromPath(entry.path()));
        }
      }
    } else {
      for (const auto &entry : fs::directory_iterator(fullPath, ec)) {
        if (entry.is_regular_file() && checkExtension(entry.path())) {
          files.push_back(fromPath(entry.path()));
        }
      }
    }
  } catch (...) {
  }

  return files;
}

FileInfo Asset::getFileInfo(const std::string &path) {
  FileInfo info;
  std::string fullPath = resolveFullPath(path);
  info.fullPath = fullPath;

  if (!exists(fullPath)) {
    info.exists = false;
    return info;
  }

  info.exists = true;
  info.isDirectory = isDirectory(fullPath);
  info.isRegularFile = isRegularFile(fullPath);

  fs::path p = toPath(fullPath);
  info.name = fromPath(p.filename());
  info.extension = fromPath(p.extension());

  if (info.isRegularFile) {
    std::error_code ec;
    info.size = fs::file_size(p, ec);
    if (ec) {
      info.size = 0;
    }
  }

  return info;
}

std::string Asset::getFileName(const std::string &path) const {
  return fromPath(toPath(path).filename());
}

std::string Asset::getFileNameWithoutExtension(const std::string &path) const {
  return fromPath(toPath(path).stem());
}

std::string Asset::getExtension(const std::string &path) const {
  return fromPath(toPath(path).extension());
}

std::string Asset::getParentPath(const std::string &path) const {
  return fromPath(toPath(path).parent_path());
}

std::string Asset::getAbsolutePath(const std::string &path) const {
  std::error_code ec;
  return fromPath(fs::absolute(toPath(resolveFullPath(path)), ec));
}

std::string Asset::getCanonicalPath(const std::string &path) const {
  std::error_code ec;
  return fromPath(fs::canonical(toPath(resolveFullPath(path)), ec));
}

std::string Asset::getCurrentPath() const {
  std::error_code ec;
  return fromPath(fs::current_path(ec));
}

bool Asset::setCurrentPath(const std::string &path) {
  std::error_code ec;
  fs::current_path(toPath(path), ec);
  return !ec;
}

std::string Asset::combinePath(const std::string &left,
                               const std::string &right) const {
  return fromPath(toPath(left) / toPath(right));
}

std::string Asset::normalizePath(const std::string &path) const {
  std::error_code ec;
  auto p = fs::absolute(toPath(resolveFullPath(path)), ec);
  if (ec) {
    return path;
  }
  return fromPath(p.make_preferred());
}

uint64 Asset::getFileSize(const std::string &path) const {
  std::error_code ec;
  auto size = fs::file_size(toPath(resolveFullPath(path)), ec);
  if (ec) {
    return 0;
  }
  return size;
}

bool Asset::isEmpty(const std::string &path) const {
  std::error_code ec;
  return fs::is_empty(toPath(resolveFullPath(path)), ec);
}

std::optional<std::string> Asset::readFileToString(const std::string &path) {
  std::string content;
  if (readTextFile(path, content)) {
    return content;
  }
  return std::nullopt;
}

std::optional<std::vector<uint8>>
Asset::readFileToBytes(const std::string &path) {
  std::vector<uint8> data;
  if (readBinaryFile(path, data)) {
    return data;
  }
  return std::nullopt;
}

void Asset::setWorkingDirectory(const std::string &path) {
  workingDirectory_ = path;
}

const std::string &Asset::getWorkingDirectory() const {
  return workingDirectory_;
}

std::string Asset::resolvePath(const std::string &relativePath) const {
  return resolveFullPath(relativePath);
}

void Asset::setAssetRoot(const std::string &root) { assetRoot_ = root; }

const std::string &Asset::getAssetRoot() const { return assetRoot_; }

std::string Asset::resolveAssetPath(const std::string &relativePath) const {
  if (assetRoot_.empty()) {
    return resolveFullPath(relativePath);
  }
  return resolveFullPath(combinePath(assetRoot_, relativePath));
}

} // namespace frostbite2D
