// Copyright 2022, FOSS-VG Developers and Contributers
//
// Author(s):
//   Bodgemaster, Shwoomple
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// version 3 along with this program.
// If not, see https://www.gnu.org/licenses/agpl-3.0.en.html

#include <fstream>
#include <filesystem>
#include <exception>
#include <vector>
#include <string>
#include "tinyutf8/tinyutf8.h"
#include "file.hpp"

File::File(std::string path, char mode, uint64_t cursorPosition): mode(mode), path(path), cursorPosition(cursorPosition){
    std::filesystem::path filePath = path;
    if(this->mode == 'w' || (this->mode == 'a' && !std::filesystem::exists(filePath))){
        this->size = ErrorOr<uint64_t>(0);
    }else{
        this->size = ErrorOr<uint64_t>(std::filesystem::file_size(filePath));
    }
    this->open();
}

File::~File() {
    if (this->isOpen) {
        this->fileStream.close();
    }
}

void File::open(){
    switch(this->mode){
        case 'w':
            this->fileStream.open(this->path, std::fstream::out | std::fstream::binary);
            break;
        case 'm':
            this->fileStream.open(this->path, std::fstream::out | std::fstream::in | std::fstream::binary);
            break;
        case 'r':
            this->fileStream.open(this->path, std::fstream::in | std::fstream::binary);
            break;
        case 'a':
            this->fileStream.open(this->path, std::fstream::app | std::fstream::binary);
            break;
        default:
            break;
    }

    std::filesystem::path filePath = this->path;
    this->size = ErrorOr<uint64_t>(std::filesystem::file_size(filePath));
    this->isOpen = this->fileStream.is_open();
}

void File::close(){
    this->fileStream.close();
    std::filesystem::path filePath = this->path;
    this->size = ErrorOr<uint64_t>(std::filesystem::file_size(filePath));
    this->isOpen = false;
}

bool File::eof() {
    return !this->size.isError && this->cursorPosition >= this->size.value;
}

ErrorOr<uint8_t> File::readByte(){
    if (!this->isOpen) {
        return ErrorOr<uint8_t>(true, ErrorCodes::FILE_NOT_OPEN);
    }
    if (!this->size.isError && this->cursorPosition >= this->size.value) {
        return ErrorOr<uint8_t>(true, ErrorCodes::OVERRUN);
    }

    uint8_t* nextPointer = new uint8_t;
    uint8_t nextByte;
    bool failure = false;

    try {
        this->fileStream.seekg(this->cursorPosition);
        this->fileStream.read(reinterpret_cast<char*>(nextPointer), 1);
        nextByte = *nextPointer;
        this->cursorPosition++;

    } catch (std::exception& e) {
        failure = true;
    }

    delete nextPointer;
    return failure? ErrorOr<uint8_t>(true, ErrorCodes::UNKNOWN) : ErrorOr<uint8_t>(nextByte);
}

ErrorOr<std::vector<uint8_t>> File::read(uint64_t bytes){
    if (!this->isOpen) {
        return ErrorOr<std::vector<uint8_t>>(true, ErrorCodes::FILE_NOT_OPEN);
    }
    if (!this->size.isError && this->cursorPosition >= this->size.value+bytes) {
        return ErrorOr<std::vector<uint8_t>>(true, ErrorCodes::OVERRUN);
    }

    uint8_t* buffer = new uint8_t[bytes];
    std::vector<uint8_t> data;
    bool failure = false;

    try {
        this->fileStream.seekg(this->cursorPosition);
        this->fileStream.read(reinterpret_cast<char*>(buffer), bytes);
        data = std::vector<uint8_t>(buffer, buffer+bytes);
        this->cursorPosition += bytes;

    } catch (std::exception& e) {
        failure = true;
    }

    delete[] buffer;
    return failure? ErrorOr<std::vector<uint8_t>>(true, ErrorCodes::UNKNOWN) : ErrorOr<std::vector<uint8_t>>(data);
}

ErrorOr<tiny_utf8::string> File::readString(uint64_t bytes){
    ErrorOr<std::vector<uint8_t>> data = this->read(bytes);
    if(data.isError){
        return ErrorOr<tiny_utf8::string>(true, data.errorCode);
    }
    std::string s;
    for(auto byte: data.value){
        s.push_back(byte);
    }
    return ErrorOr<tiny_utf8::string>(tiny_utf8::string(s));
}

ErrorOrVoid File::writeByte(uint8_t byte){

    bool failure = false;

    try{
        this->fileStream << byte;
    }catch(std::exception& e){
        failure = true;
    }

    return failure ? ErrorOrVoid(true, ErrorCodes::UNKNOWN) : ErrorOrVoid();
}

ErrorOrVoid File::write(std::vector<uint8_t> data){
    bool failure = false;

    try{
        this->fileStream << std::string(data.begin(), data.end());
    }catch(std::exception& e){
        failure = true;
    }

    return failure ? ErrorOrVoid(true, ErrorCodes::UNKNOWN) : ErrorOrVoid();
}

ErrorOrVoid File::writeString(tiny_utf8::string data){
    bool failure = false;

    try{
        this->fileStream << data;
    }catch(std::exception& e){
        failure = true;
    }
    return failure ? ErrorOrVoid(true, ErrorCodes::UNKNOWN) : ErrorOrVoid();
}

// Naive implementation of insertByte
ErrorOrVoid File::insertByte(uint8_t byte){
    bool failure = false;

    try{
        uint8_t* buffer = new uint8_t[this->size.value];
        std::vector<uint8_t> readData;

        this->fileStream.read(reinterpret_cast<char*>(buffer), this->size.value);
        readData = std::vector<uint8_t>(buffer, buffer+this->size.value);

        readData.insert(readData.begin()+this->cursorPosition, byte);
        this->fileStream.seekg(0);
        this->write(readData);
        this->cursorPosition++;
        delete[] buffer;
    }catch(std::exception& e){
        failure = true;
    }
    return failure ? ErrorOrVoid(true, ErrorCodes::UNKNOWN) : ErrorOrVoid();
}


ErrorOrVoid File::insert(std::vector<uint8_t> data){
    bool failure = false;

    try{
        uint8_t* buffer = new uint8_t[this->size.value];
        std::vector<uint8_t> readData;

        this->fileStream.read(reinterpret_cast<char*>(buffer), this->size.value);
        readData = std::vector<uint8_t>(buffer, buffer+this->size.value);

        readData.insert(readData.begin()+this->cursorPosition, data.begin(), data.end());
        this->fileStream.seekg(0);
        this->write(readData);
        this->cursorPosition += data.size();
        delete[] buffer;
    }catch(std::exception& e){
        failure = true;
    }
    return failure ? ErrorOrVoid(true, ErrorCodes::UNKNOWN) : ErrorOrVoid();
}

ErrorOrVoid File::insertString(tiny_utf8::string string){
    bool failure = false;

    try{
        uint8_t* buffer = new uint8_t[this->size.value];
        tiny_utf8::string readData;

        this->fileStream.read(reinterpret_cast<char*>(buffer), this->size.value);
        
        readData = tiny_utf8::string((char *) buffer, 0, this->size.value);
        readData.insert(readData.begin()+this->cursorPosition, string);

        this->fileStream.seekg(0);

        this->writeString(readData);
        this->cursorPosition += string.size();
        delete[] buffer;
    }catch(std::exception& e){
        failure = true;
    }
    return failure ? ErrorOrVoid(true, ErrorCodes::UNKNOWN) : ErrorOrVoid();
}

ErrorOr<uint8_t> File::cutByte(){
    bool failure = false;
    uint8_t byte;
    try{
        uint8_t* buffer = new uint8_t[this->size.value];
        std::vector<uint8_t> readData;

        this->fileStream.read(reinterpret_cast<char*>(buffer), this->size.value);
        readData = std::vector<uint8_t>(buffer, buffer+this->size.value);

        byte = readData[this->cursorPosition];
        readData.erase(readData.begin() + this->cursorPosition);

        std::filesystem::resize_file(this->path, readData.size());
        this->fileStream.seekg(0);
        this->write(readData);
        //this->cursorPosition++;
        delete[] buffer;
    }catch(std::exception& e){
        failure = true;
    }
    return failure ? ErrorOr<uint8_t>(true, ErrorCodes::UNKNOWN) : ErrorOr<uint8_t>(byte);
}

ErrorOr<std::vector<uint8_t>> File::cut(uint64_t length){
    bool failure = false;
    std::vector<uint8_t> bytes;

    try{
        uint8_t* buffer = new uint8_t[this->size.value];
        std::vector<uint8_t> readData;

        this->fileStream.read(reinterpret_cast<char*>(buffer), this->size.value);
        readData = std::vector<uint8_t>(buffer, buffer+this->size.value);

        bytes = std::vector<uint8_t>(readData.begin() + this->cursorPosition, readData.begin() + (this->cursorPosition + length));
        readData.erase(readData.begin() + this->cursorPosition, readData.begin() + (this->cursorPosition + length));

        std::filesystem::resize_file(this->path, readData.size());
        this->fileStream.seekg(0);
        this->write(readData);
        //this->cursorPosition += length;
        delete[] buffer;
    }catch(std::exception& e){
        failure = true;
    }

    return failure ? ErrorOr<std::vector<uint8_t>>(true, ErrorCodes::UNKNOWN) :ErrorOr<std::vector<uint8_t>>(bytes);
}

ErrorOr<tiny_utf8::string> File::cutString(uint64_t length){
    bool failure = false;
    tiny_utf8::string cutString;
    try{
        uint8_t* buffer = new uint8_t[this->size.value];
        tiny_utf8::string readData;

        this->fileStream.read(reinterpret_cast<char*>(buffer), this->size.value);
        readData = tiny_utf8::string((char *) buffer, 0, this->size.value);
        
        cutString = readData.substr(this->cursorPosition, length);

        std::filesystem::resize_file(this->path, readData.size()-cutString.size());
        this->fileStream.seekg(0);
        this->writeString(readData.substr(this->cursorPosition+length));
        //this->cursorPosition += length;
        delete[] buffer;
    }catch(std::exception& e){
        failure = true;
    }

    return failure ? ErrorOr<tiny_utf8::string>(true, ErrorCodes::UNKNOWN) : ErrorOr<tiny_utf8::string>(cutString);
}

ErrorOr<File*> File::open(std::string path, char mode, uint64_t startPosition){
    if (!std::filesystem::exists(path) && (mode == 'r' || mode == 'm')) {
        return ErrorOr<File*>(true, ErrorCodes::FILE_NOT_FOUND, nullptr);
    }

    //TODO: check access perms

    return ErrorOr<File*>(new File(path, mode, startPosition));
}
