/***************************************************************************
                             -------------------

    copyright   : (C) 2020 by Prasun Kumar

 ***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or         *
 *   modify it under the terms of the GNU Lesser General Public            *
 *   License as published by the Free Software Foundation; either          *
 *   version 2.1 of the License, or (at your option) any later version.    *
 *                                                                         *
 *   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     *
 *   Lesser General Public License for more details.                       *
 *                                                                         *
 *   You should have received a copy of the GNU Lesser General Public      *
 *   License along with this program; if not, write to the Free Software   *
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston,                 *
 *   MA  02111-1307  USA                                                   *
 *                                                                         *
 ***************************************************************************/

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <fstream>
#include <cmath>
#include <curl/curl.h>
#include <sys/stat.h>
#include <errno.h>

#include "algorithms.h"
#include "ktoblzcheck.h"

#if defined(__MINGW32__) || defined(_WIN32)
# ifndef OS_WIN32
#  define OS_WIN32
# endif
#endif

#ifdef OS_WIN32
# include <windows.h>
# define mkdir(dir, mode) mkdir(dir)
#undef ERROR
#endif

#ifdef OS_WIN32
# define DIRSEP "\\"
#else
# define DIRSEP "/"
#endif

class DataUpdater::UpdaterImpl
{
public:
    //Constructor
    UpdaterImpl() : urlPath("https://netcologne.dl.sourceforge.net/project/ktoblzcheck/ktoblzcheck-data/")
    {
    }

    // Structure to store downloaded data
    struct MemoryStruct {
        char *memory;
        size_t size;
    };

    /**Callback function for CURLOPT_WRITEFUNCTION.
     *
     * This method is used to allocate memory to MemoryStruct.
     */
    static size_t writeMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp);

    /**Callback function for CURLOPT_PROGRESSFUNCTION.
     *
     * This method is used to call the progressMeterFunction() method.
     */
    static int progressCallback(void *ptr, double TotalToDownload, double NowDownloaded, double TotalToUpload, double NowUploaded);

    /**
     * Method to print the download progress meter on the console
     */
    int progressMeterFunction(double TotalToDownload, double NowDownloaded);

    // Initialises CURL and return a handle
    CURL *initCurl();

    bool setupCurl(CURL *curl);

    struct MemoryStruct chunk;

    // The URL path from where the files are downloaded
    const std::string urlPath;

    // Name of the file currently being downloaded
    std::string currentlyDownloading;
};

CURL *DataUpdater::UpdaterImpl::initCurl()
{
    curl_global_init(CURL_GLOBAL_DEFAULT);
    return curl_easy_init();
}

int DataUpdater::UpdaterImpl::progressCallback(void *ptr, double TotalToDownload, double NowDownloaded, double TotalToUpload, double NowUploaded)
{
    return ((UpdaterImpl *)ptr)->progressMeterFunction(TotalToDownload, NowDownloaded);
}

int DataUpdater::UpdaterImpl::progressMeterFunction(double totalToDownload, double nowDownloaded)
{
    // To ensure that the file to be downloaded is not empty
    // because that would cause a division by zero error later on
    if (totalToDownload <= 0.0) {
        return 0;
    }

    // Number of markers to determine the width of the progress meter
    int numMarkers = 40;

    double fractiondownloaded = nowDownloaded / totalToDownload;
    // part of the progressmeter that's already "full"
    int markers = round(fractiondownloaded * numMarkers);

    int i = 0;
    printf("Downloading %s: %3.0f%% [", this->currentlyDownloading.c_str(), fractiondownloaded*100);

    // Print marker for part that's full
    for (; i < markers; ++i) {
        printf("=");
    }

    // Remaining part (spaces)
    for (; i < numMarkers; ++i) {
        printf(" ");
    }
    // Back to the beginning of the line
    printf("]\r");

    fflush(stdout);

    return 0;
}

bool DataUpdater::UpdaterImpl::setupCurl(CURL *curl)
{
    if (curl) {
        // set URL
        curl_easy_setopt(curl, CURLOPT_URL, (urlPath+"/"+currentlyDownloading).c_str());
        // Allow link redirection
        curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
        // Method to allocate memory
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeMemoryCallback);
        // Enable progree meter
        curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
        // Specifies the progress function
        curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, DataUpdater::UpdaterImpl::progressCallback);
        // Pass the pointer to the current object to progress function
        curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, this);
        // Specifies structure to write data into
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);

        return true;
    }
    return false;
}

size_t
DataUpdater::UpdaterImpl::writeMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp)
{
    size_t realsize = size * nmemb;
    struct MemoryStruct *mem = (struct MemoryStruct *)userp;

    char *ptr = (char *)realloc(mem->memory, mem->size + realsize + 1);
    if (ptr == NULL) {
        /* out of memory! */
        printf("not enough memory (realloc returned NULL)\n");
        return 0;
    }

    mem->memory = ptr;
    memcpy(&(mem->memory[mem->size]), contents, realsize);
    mem->size += realsize;
    mem->memory[mem->size] = 0;

    return realsize;
}

DataUpdater::DataUpdater()
    : mUpdaterImpl(std::make_unique<UpdaterImpl>())
{
    mUpdaterImpl->chunk.size = 0;
    mUpdaterImpl->chunk.memory = (char *)malloc(1);
}

DataUpdater::~DataUpdater()
{
    if (mUpdaterImpl->chunk.memory) {
        free(mUpdaterImpl->chunk.memory);
    }
    curl_global_cleanup();
}

bool DataUpdater::downloadDatabase(const std::string &filename)
{
    std::string filepath;
    CURL *curl = mUpdaterImpl->initCurl();

    mUpdaterImpl->currentlyDownloading = filename;
    if (mUpdaterImpl->setupCurl(curl)) {
        //std::cerr << "Connecting to " << urlPath << "/" << currentlyDownloading << std::endl;
        CURLcode res = curl_easy_perform(curl);

        /* Check for errors */
        if (res != CURLE_OK) {
            fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
            return false;
        } else {
            //std::cerr << "\nBytes retrieved: " << (unsigned long)chunk.size << std::endl;
            filepath = algorithms_get_writable_bankdata_dir()+DIRSEP+filename;
            int ret = mkdir(algorithms_get_writable_bankdata_dir().c_str(), 0755);
            if (ret == -1) {
                if (errno == EEXIST) {
                    // Directory already exists
                }
            }
            FILE *fp = fopen(filepath.c_str(), "w");

            fwrite(mUpdaterImpl->chunk.memory, sizeof(char), mUpdaterImpl->chunk.size, fp);

            fclose(fp);
            free(mUpdaterImpl->chunk.memory);
            mUpdaterImpl->chunk.memory = (char *)malloc(1);
            mUpdaterImpl->chunk.size = 0;
        }

        curl_easy_cleanup(curl);

        std::cerr << "Downloaded \"" << filename << "\" to \"" << filepath << "\" successfully!" << std::string(50, ' ') << std::endl;
        return true;
    }

    curl_easy_cleanup(curl);
    return false;
}

bool DataUpdater::downloadAllDatabases()
{
    bool flag = true;

    bool success = downloadDatabase("bankdata.de.db");

    if (!success) {
        std::cerr << "Update of bankdata.de.db failed!" << std::string(40, ' ') << std::endl;
        flag = false;
    }

    success = downloadDatabase("bankdata.nl.db");

    if (!success) {
        std::cerr << "Update of bankdata.nl.db failed!" << std::string(40, ' ') << std::endl;
        flag = false;
    }

    success = downloadDatabase("bankdata.ch.db");

    if (!success) {
        std::cerr << "Update of bankdata.ch.db failed!" << std::string(40, ' ') << std::endl;
        flag = false;
    }

    if (flag) {
        std::cerr << "Updated all databases successfully." << std::string(40, ' ') << std::endl;
    }

    return flag;
}

bool DataUpdater::isUpdateAvailable()
{
    // TODO
    return true;
}
