/**
 *
 * @file src/parser/PajeParser/mt_PajeFileManager.cpp
 *
 * @copyright 2008-2024 Bordeaux INP, CNRS (LaBRI UMR 5800), Inria,
 *                      Univ. Bordeaux. All rights reserved.
 *
 * @author Camille Ordronneau
 * @author Johnny Jazeix
 * @author Augustin Degomme
 * @author Mathieu Faverge
 *
 * @date 2024-07-17
 */
#include <stdlib.h>
#include <cstring>
#include <string>
#include <fstream>
#include <iostream>
#ifdef WIN32
#include <windows.h>
#include <limits.h>
#else
#include <sys/mman.h>
#endif
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <queue>
/* -- */
#include "common/Errors.hpp"
#include "parser/PajeParser/mt_PajeFileManager.hpp"
/* -- */
using namespace std;

#define MAX_SIZE_UNCUT 100 * 1024 * 1024

mt_PajeFileManager::mt_PajeFileManager() :
    _filename(""), _filesize(0), _total_filesize(0), _nbchunks(0), _current_chunk(0), _mapped_size(0), _eoc(false), _lineid(0), _nbtks(0), _old_line(NULL), _global_offset(0) {
    //_tokens = new char*[_PAJE_NBMAXTKS];
    _offset = 0;
    _eof = false;
}

mt_PajeFileManager::mt_PajeFileManager(const char *filename) :
    _filename(filename), _filesize(0), _total_filesize(0), _nbchunks(0), _mapped_size(0), _eoc(false), _lineid(0), _nbtks(0), _old_line(NULL), _global_offset(0) {
    //_tokens = (char**)malloc(sizeof(char*)*_PAJE_NBMAXTKS);
    _offset = 0;
    _eof = false;
    open(filename);
}

mt_PajeFileManager::~mt_PajeFileManager() {
#ifdef WIN32
    CloseHandle(_fd);
    UnmapViewOfFile(_line);
#else
    munmap(_line, _mapped_size);
#endif

#ifndef WIN32
    fclose(_fd);
#endif

    close();
    // if(_tokens!=NULL)delete _tokens;
}

void mt_PajeFileManager::open(const char *filename) {

    _filename = filename;
#ifdef WIN32
    _fd = CreateFile(filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
#else
    _fd = fopen(filename, "r");
    if (fail())
        throw "Fail to open file";
#endif

#ifndef WIN32
    struct stat buf; // for the file size

    if (-1 == stat(filename, &buf)) // Getq the size
    {
        Error::set(Error::VITE_ERR_FSTAT, 0, Error::VITE_ERRCODE_ERROR);

        if (_fd)
            fclose(_fd); // Close it if wrong
        throw "Fail to stat file";
    }
    _filesize = (long)buf.st_size;
#else
    LARGE_INTEGER size;
    GetFileSizeEx(_fd, &size);
    _filesize = size.QuadPart;
#endif

#ifdef WIN32
    SYSTEM_INFO si;
    GetSystemInfo(&si);
    _pageSize = (int)si.dwAllocationGranularity;

    if (_filesize > MAX_SIZE_UNCUT) {
        _current_chunk = 0;
        _mapped_size = MAX_SIZE_UNCUT;
    }
    else {
        _mapped_size = _filesize;
    }

    _fm = CreateFileMapping(_fd, NULL, PAGE_READONLY, 0, 0, NULL);
    _line = (char *)MapViewOfFile(_fm, FILE_MAP_COPY, 0, 0, _mapped_size);
#else
    _pageSize = getpagesize();
    if (_filesize > MAX_SIZE_UNCUT) {
        _current_chunk = 0;
        _mapped_size = MAX_SIZE_UNCUT;
    }
    else {
        _mapped_size = _filesize;
    }

    if (_mapped_size > 0) {
        _line = (char *)mmap(0, _mapped_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fileno(_fd), 0); // Mapping of the file

        madvise(_line, _mapped_size, MADV_SEQUENTIAL); // advise kernel that we want to read sequentially in the file
    }
    else {
        // empty file
        _eof = true;
    }
#endif

    if ((char *)-1 == _line) {
#ifndef WIN32
        if (_fd)
            fclose(_fd); // Close it if wrong
#endif
        Error::set(Error::VITE_ERR_OPEN, 0, Error::VITE_ERRCODE_ERROR);
        throw "Fail to open file";
    }

    // For windows, clear the eof byte for ept files after reading the first file
    clear();
}

void mt_PajeFileManager::close() {
    _filesize = 0;
}

int mt_PajeFileManager::get_filesize() const {
    return _filesize;
}

int mt_PajeFileManager::get_size_loaded() {
    return _global_offset + _offset;
}

float mt_PajeFileManager::get_percent_loaded() {
    if (_filesize != 0) {
        return (float)((double)(_global_offset + _offset) / (double)_filesize);
    }
    else {
        return 0.;
    }
}

int mt_PajeFileManager::get_line(PajeLine *lineptr) {
    long i = 0;
    long itks = 0;
    char c;

    _tokens = (char **)calloc(_PAJE_NBMAXTKS, sizeof(char *));
    _lineid++;

    // Set the adress of the first token
    c = _line[_offset];
    _tokens[itks] = &_line[_offset];

    // max offset for a single line
    long max_offset = _offset + _pageSize;

    for (i = _offset; (i < max_offset) && (i < _filesize) && ((c != '\n') && (c != '\0')) && (itks < _PAJE_NBMAXTKS); i++) {
        c = _line[i];

        switch (c) {
            // It's the end of the line, we just add the end caractere
            // for the last token and increase the counter
        case '\0':
        case '\n': {
            _line[i] = '\0';
            itks++;
            break;
        }
        case '%': {
            // In these case, we just have two tokens, one with the % and one without
            itks++;
            _tokens[itks] = &_line[i + 1];
            break;
        }
        case '\'': {
            // Check if we are at the beginnning of a new one or not
            if (_tokens[itks][0] == '\'') {
                _line[i] = '\0';
                _tokens[itks]++;
            }
            else {
                _line[i] = '\0';
                itks++;
                _tokens[itks] = &_line[i + 1];
            }

            // Start a long token
            while ((i < _filesize) && (i < max_offset - 1) && (_line[i] != '\'')) {
                i++;
            }

            // If i is too big, we have an error
            if (!(i < max_offset - 1)) {
                throw "overflow";
                return -1;
            }

            if (i == _filesize) {
                _eof = true;
                throw "Overflow";
                return -1;
            }

            // We finish the token by replacing the \' by \0
            _line[i] = '\0';
            itks++;
            _tokens[itks] = &_line[i + 1];
            break;
        }
        case '"': {
            // Check if we are at the beginnning of a new one or not
            if (_tokens[itks][0] == '"') {
                _line[i] = '\0';
                _tokens[itks]++;
            }
            else {
                _line[i] = '\0';
                itks++;
                _tokens[itks] = &_line[i + 1];
            }

            // Start a long token (-1 to have place or the null caractere)
            while ((i < _filesize) && (i < max_offset - 1) && (_line[i] != '"')) {
                i++;
            }

            // If i is too big, we have an error
            if (!(i < max_offset)) {
                throw "Overflow";
                return -1;
            }

            if (i == _filesize) {
                _eof = true;
                throw "Overflow";
                return -1;
            }

            // We finish the token by replacing the " by \0
            _line[i] = '\0';
            itks++;
            _tokens[itks] = &_line[i + 1];
            break;
        }
        case ' ':
        case '\t': {
            // Skip all spaces
            while ((i < _filesize) && (i < max_offset - 1) && ((_line[i] == ' ') || (_line[i] == '\t'))) {

                _line[i] = '\0';
                i++;
            }
            if (i == _filesize) {
                _eof = true;
                throw "Overflow";
                return -1;
            }
            // We store a new token if it's a new space
            if (_tokens[itks][0] != '\0')
                itks++;
            _tokens[itks] = &_line[i];
            i--;
            break;
        }
        case '#': {
            // Skip the comment until new line or end of text
            while ((i < _filesize) && (i < max_offset) && ((_line[i] != '\n') && (_line[i] != '\0'))) {
                _line[i] = '\0';
                i++;
            }

            if (i == _filesize) {
                setstate(ifstream::eofbit);
                throw "Overflow";
                return -1;
            }

            // Let the parser handle properly new line or end of text
            if (((_line[i] == '\n') || (_line[i] == '\0'))) {
                i--;
            }

            break;
        }
        default:
            break;
        }
    }

    if (itks == 0) {
        i += sizeof(char); // we read one char but no token : empty line, advance the offset of one char, in order not to loop
    }
    _offset = i;

    // We remove the last token if it is empty
    if ((itks > 0) && (_tokens[itks - 1][0] == '\0'))
        itks--;

    _nbtks = itks;
    lineptr->_id = _lineid;
    lineptr->_nbtks = _nbtks;
    lineptr->_tokens = _tokens;

#ifdef WIN32

    if (_filesize > MAX_SIZE_UNCUT) {

        if (_global_offset + _offset >= _filesize)
            _eof = true;
        else {
            if (_offset + _pageSize >= MAX_SIZE_UNCUT) { // time to mmap another part of the file, else we might miss a line

                _global_offset += _offset;

                DWORD high_offset = (_global_offset / ULONG_MAX);

                // to align correctly
                DWORD low_offset = (_global_offset % ((ULONGLONG)(ULONG_MAX) + 1));
                DWORD low_aligned_offset = (low_offset / _pageSize) * _pageSize;
                _offset = low_offset % _pageSize;
                _global_offset = high_offset * ((ULONGLONG)(ULONG_MAX) + 1) + low_aligned_offset;

                _current_chunk++;
                if (_global_offset + MAX_SIZE_UNCUT >= _filesize)
                    _mapped_size = _filesize - _global_offset;
                else
                    _mapped_size = MAX_SIZE_UNCUT;

                // munmap(_line, MAX_SIZE_UNCUT);
                // printf("mmaping %d bytes for chunk %d\n",_mapped_size, _current_chunk);
                _old_line = _line;
                _eoc = true; // we finished a chunk
                _line = (char *)MapViewOfFile(_fm, FILE_MAP_COPY, high_offset, low_aligned_offset, _mapped_size);
                if (!_line) {
                    DWORD d = GetLastError();
                    // printf("mmaping failed %d %d %llu %lu %lu %lu\n",_mapped_size, _current_chunk, _global_offset, d, (DWORD)( _global_offset/ULONG_MAX), (DWORD) (_global_offset%ULONG_MAX));
                    Error::set(Error::VITE_ERR_OPEN, 0, Error::VITE_ERRCODE_ERROR);
                    throw "Fail to open file";
                }
            }
        }
    }
    else if (_offset >= _filesize) {
        _eof = true;
    }
#else

    if (_filesize > MAX_SIZE_UNCUT) {

        if (_global_offset + _offset >= _filesize)
            _eof = true;
        else {
            if (_offset + _pageSize >= MAX_SIZE_UNCUT) { // time to mmap another part of the file, else we might miss a line

                _global_offset += _offset;
                _offset = _global_offset % getpagesize();
                _global_offset = (_global_offset / getpagesize()) * getpagesize();

                _current_chunk++;
                if (_global_offset + MAX_SIZE_UNCUT >= _filesize)
                    _mapped_size = _filesize - _global_offset;
                else
                    _mapped_size = MAX_SIZE_UNCUT;

                // munmap(_line, MAX_SIZE_UNCUT);
                // printf("mmaping %d bytes for chunk %d\n",_mapped_size, _current_chunk);
                _old_line = _line;
                _eoc = true; // we finished a chunk
                _line = (char *)mmap(0, _mapped_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fileno(_fd), _global_offset); // Mapping of the file
                madvise(_line, _mapped_size, MADV_SEQUENTIAL); // advise kernel that we want to read sequentially in the file

                if ((char *)-1 == _line) {
                    // printf("mmaping failed\n",_mapped_size, _current_chunk);
                    if (_fd)
                        fclose(_fd); // Close it if wrong
                    Error::set(Error::VITE_ERR_OPEN, 0, Error::VITE_ERRCODE_ERROR);
                    throw "Fail to open file";
                }
            }
        }
    }
    else if (_offset >= _filesize) {
        _eof = true;
    }

#endif
    return _lineid;
}

bool mt_PajeFileManager::eoc() { return _eoc; }

void mt_PajeFileManager::close_old_chunk() {

    if (_old_line != NULL) {
#ifndef WIN32
        munmap(_old_line, MAX_SIZE_UNCUT);
#else
        UnmapViewOfFile(_old_line);
// static int t=0;
// printf("unmap %d\n", t);
#endif
        _eoc = false;
        _old_line = NULL;
    }
}

bool mt_PajeFileManager::eof() {
    return _eof;
}

void mt_PajeFileManager::print_line() {
    int i;

    cout << "==================" << _lineid << "=====================" << endl;
    for (i = 0; i < _nbtks; i++) {
        cout << i << " : " << _tokens[i] << endl;
    }
    cout << "===========================================" << endl;
}
