/* This file is part of pr-downloader (GPL v2 or later), see the LICENSE file */

#include "Sdp.h"
#include "RapidDownloader.h"
#include "Util.h"
#include "Logger.h"
#include "FileSystem/FileSystem.h"
#include "FileSystem/FileData.h"
#include "FileSystem/HashMD5.h"
#include "FileSystem/File.h"
#include "Downloader/CurlWrapper.h"
#include "Downloader/Download.h"
#include <string>
#include <string.h>
#include <stdio.h>
#include <curl/curl.h>
#include <stdlib.h>

CSdp::CSdp(const std::string& shortname, const std::string& md5,
	   const std::string& name, const std::string& depends,
	   const std::string& url)
    : m_download(NULL)
    , downloadInitialized(false)
    , globalFiles(NULL)
    , file_handle(NULL)
    , file_pos(0)
    , skipped(false)
    , cursize(0)
    , name(name)
    , md5(md5)
    , shortname(shortname)
    , url(url)
    , depends(depends)
    , downloaded(false)
{
	memset(this->cursize_buf, 0, LENGTH_SIZE);
}

CSdp::~CSdp()
{
	if (file_handle != NULL) {
		delete file_handle;
	}
}

bool createPoolDirs(const std::string& root)
{
	char buf[1024];
	const int pos = snprintf(buf, sizeof(buf), "%s", root.c_str());
	for (int i = 0; i < 256; i++) {
		snprintf(buf + pos, 4, "%02x%c", i, PATH_DELIMITER);
		std::string tmp(buf, pos + 3);
		if ((!fileSystem->directoryExists(tmp)) &&
		    (!fileSystem->createSubdirs(tmp))) {
			LOG_ERROR("Couldn't create %s", tmp.c_str());
			return false;
		}
	}
	return true;
}

bool CSdp::download(IDownload* download)
{
	if (downloaded) // allow download only once of the same sdp
		return true;
	m_download = download;
	filename =
	    fileSystem->getSpringDir() + PATH_DELIMITER + "packages" + PATH_DELIMITER;
	LOG_DEBUG("%s", filename.c_str());
	if (!fileSystem->directoryExists(filename)) {
		fileSystem->createSubdirs(filename);
	}
	int count = 0;
	filename += this->md5 + ".sdp";
	const std::string tmpFile = filename + ".tmp";
	std::list<FileData*> files;

	bool rename = false;
	if (!fileSystem->fileExists(filename)) { //.sdp isn't avaiable, download it
		IDownload dl(tmpFile);
		dl.addMirror(url + "/packages/" + this->md5 + ".sdp");
		httpDownload->download(&dl);
		fileSystem->parseSdp(tmpFile, files); // parse downloaded file
		rename = true;
	} else {
		fileSystem->parseSdp(filename, files); // parse downloaded file
	}

	HashMD5 md5 = HashMD5();
	FileData tmp = FileData();
	int i = 0;
	for (FileData* filedata : files) { // check which file are available on local
					   // disk -> create list of files to download
		i++;
		md5.Set(filedata->md5, sizeof(filedata->md5));
		std::string file;
		fileSystem->getPoolFilename(md5.toString(), file);
		if (!fileSystem->fileExists(
			file)) { // add non-existing files to download list
			count++;
			filedata->download = true;
		} else {
			filedata->download = false;
		}
		if (i % 30 == 0) {
			LOG_DEBUG("\r%d/%d checked", i, (int)files.size());
		}
	}
	LOG_DEBUG("\r%d/%d need to download %d files", i, (unsigned int)files.size(),
		  count);

	std::string root = fileSystem->getSpringDir();
	root += PATH_DELIMITER;
	root += "pool";
	root += PATH_DELIMITER;
	if (!createPoolDirs(root)) {
		LOG_ERROR("Creating pool directories failed");
		count = 0;
	}
	if (count > 0) {
		downloaded =
		    downloadStream(this->url + "/streamer.cgi?" + this->md5, files);
		if (!downloaded) {
			LOG_ERROR("Couldn't download files for %s", this->md5.c_str());
			fileSystem->removeFile(tmpFile);
			fileSystem->removeFile(filename);
			return false;
		}
		LOG_DEBUG("Sucessfully downloaded %d files: %s %s", count,
			  shortname.c_str(), name.c_str());
	} else {
		LOG_DEBUG("Already downloaded: %s", shortname.c_str());
		downloaded = true;
	}

	for (FileData* filedata : files) { // free memory
		delete filedata;
	}
	if ((rename) && (!fileSystem->Rename(tmpFile, filename))) {
		LOG_ERROR("Couldn't rename %s to %s", tmpFile.c_str(), filename.c_str());
		return false;
	}
	if (downloaded) {
		download->state = IDownload::STATE_FINISHED;
	}
	return downloaded;
}

/**
        write the data received from curl to the rapid pool.

        the filename is read from the sdp-list (created at request start)
        filesize is read from the http-data received (could overlap!)
*/
static size_t write_streamed_data(const void* tmp, size_t size, size_t nmemb,
				  CSdp* sdp)
{
	char buf[CURL_MAX_WRITE_SIZE];
	memcpy(&buf, tmp, CURL_MAX_WRITE_SIZE);
	if (!sdp->downloadInitialized) {
		sdp->list_it = sdp->globalFiles->begin();
		sdp->downloadInitialized = true;
		sdp->file_handle = NULL;
		sdp->file_name = "";
		sdp->skipped = 0;
	}
	char* buf_start = (char*)&buf;
	const char* buf_end = buf_start + size * nmemb;
	char* buf_pos = buf_start;

	while (buf_pos < buf_end) {		// all bytes written?
		if (sdp->file_handle == NULL) { // no open file, create one
			while ((!(*sdp->list_it)->download == true) &&
			       (sdp->list_it != sdp->globalFiles->end())) { // get file
				++sdp->list_it;
			}
			HashMD5 md5;
			md5.Set((*sdp->list_it)->md5, sizeof((*sdp->list_it)->md5));
			fileSystem->getPoolFilename(md5.toString(), sdp->file_name);
			sdp->file_handle = new CFile();
			if (sdp->file_handle == NULL) {
				LOG_ERROR("couldn't open %s", (*sdp->list_it)->name.c_str());
				return -1;
			}
			sdp->file_handle->Open(sdp->file_name);
			sdp->file_pos = 0;
		}
		assert(sdp->file_handle != NULL);
		if (sdp->skipped < LENGTH_SIZE) { // check if we skipped all 4 bytes for
						  // file length, if not so, skip them
			const int toskip =
			    intmin(buf_end - buf_pos,
				   LENGTH_SIZE - sdp->skipped); // calculate bytes we can skip,
								// could overlap received bufs
			for (int i = 0; i < toskip; i++) {      // copy bufs avaiable
				sdp->cursize_buf[sdp->skipped + i] = buf_pos[i];
			}
			sdp->skipped += toskip;
			buf_pos += toskip;
			if (sdp->skipped == LENGTH_SIZE) { // all length bytes read, parse
				(*sdp->list_it)->compsize = parse_int32(sdp->cursize_buf);
				assert((*sdp->list_it)->size + 2000 >= (*sdp->list_it)->compsize);
			}
		}
		if (sdp->skipped == LENGTH_SIZE) { // length bytes read
			const int towrite =
			    intmin((*sdp->list_it)->compsize -
				       sdp->file_pos, // minimum of bytes to write left in file
						      // and bytes to write left in buf
				   buf_end - buf_pos);
			assert(towrite >= 0);

			if (towrite == 0)
				break;

			const int res = sdp->file_handle->Write(buf_pos, towrite);
			if (res != towrite) {
				LOG_ERROR("fwrite error");
				return -1;
			}
			buf_pos += res;
			sdp->file_pos += res;

			if (sdp->file_pos >=
			    (*sdp->list_it)->compsize) { // file finished -> next file
				sdp->file_handle->Close();
				delete sdp->file_handle;
				sdp->file_handle = NULL;
				if (!fileSystem->fileIsValid(*sdp->list_it, sdp->file_name.c_str())) {
					LOG_ERROR("File is broken?!: %s", sdp->file_name.c_str());
					fileSystem->removeFile(sdp->file_name.c_str());
					return -1;
				}
				++sdp->list_it;
				sdp->file_pos = 0;
				sdp->skipped = 0;
			}
		}
	}
	return buf_pos - buf_start;
}

/** *
        draw a nice download status-bar
*/
static int progress_func(CSdp& csdp, double TotalToDownload,
			 double NowDownloaded, double TotalToUpload,
			 double NowUploaded)
{

	(void)csdp;
	(void)TotalToUpload;
	(void)NowUploaded; // remove unused warning
	csdp.m_download->rapid_size[&csdp] = TotalToDownload;
	csdp.m_download->map_rapid_progress[&csdp] = NowDownloaded;
	uint64_t total = 0;
	for (auto it : csdp.m_download->rapid_size) {
		total += it.second;
	}
	csdp.m_download->size = total;
	if (IDownloader::listener != nullptr) {
		IDownloader::listener(NowDownloaded, TotalToDownload);
	}
	total = 0;
	for (auto it : csdp.m_download->map_rapid_progress) {
		total += it.second;
	}
	csdp.m_download->progress = total;
	if (TotalToDownload == NowDownloaded) // force output when download is
					      // finished
		LOG_PROGRESS(NowDownloaded, TotalToDownload, true);
	else
		LOG_PROGRESS(NowDownloaded, TotalToDownload);
	return 0;
}

bool CSdp::downloadStream(const std::string& url, std::list<FileData*> files)
{
	CurlWrapper* curlw = new CurlWrapper();
	if (!curlw) {
		return false;
	}
	CURLcode res;
	LOG_INFO("Using rapid");
	LOG_INFO(url.c_str());

	curl_easy_setopt(curlw->GetHandle(), CURLOPT_URL, url.c_str());

	int buflen = files.size() / 8;
	if (files.size() % 8 != 0)
		buflen++;
	char* buf =
	    (char*)malloc(buflen); // FIXME: compress blockwise and not all at once
	memset(buf, 0, buflen);
	int destlen = files.size() * 2;
	LOG_DEBUG("%d %d %d", (int)files.size(), buflen, destlen);
	int i = 0;
	for (FileData* it : files) {
		if (it->download == true) {
			buf[i / 8] |= (1 << (i % 8));
		}
		i++;
	}
	char* dest = (char*)malloc(destlen);

	gzip_str(buf, buflen, dest, &destlen);

	curl_easy_setopt(curlw->GetHandle(), CURLOPT_WRITEFUNCTION,
			 write_streamed_data);
	curl_easy_setopt(curlw->GetHandle(), CURLOPT_WRITEDATA, this);

	globalFiles = &files;
	curl_easy_setopt(curlw->GetHandle(), CURLOPT_POSTFIELDS, dest);
	curl_easy_setopt(curlw->GetHandle(), CURLOPT_POSTFIELDSIZE, destlen);
	curl_easy_setopt(curlw->GetHandle(), CURLOPT_NOPROGRESS, 0L);
	curl_easy_setopt(curlw->GetHandle(), CURLOPT_PROGRESSFUNCTION, progress_func);
	curl_easy_setopt(curlw->GetHandle(), CURLOPT_PROGRESSDATA, this);

	res = curl_easy_perform(curlw->GetHandle());
	free(dest);
	free(buf);
	/* always cleanup */
	delete curlw;
	curlw = NULL;
	if (res != CURLE_OK) {
		LOG_ERROR("Curl cleanup error: %s", curl_easy_strerror(res));
		return false;
	}
	return true;
}
