// Emacs style mode select   -*- C++ -*-
//-----------------------------------------------------------------------------
//
// $Id: 3a00df02f20af65fcaddc0542a2078ddee6868a6 $
//
// Copyright (C) 1998-2006 by Randy Heit (ZDoom).
// Copyright (C) 2000-2006 by Sergey Makovkin (CSDoom .62).
// Copyright (C) 2006-2020 by The Odamex Team.
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// 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 General Public License for more details.
//
// DESCRIPTION:
//	File downloads
//
//-----------------------------------------------------------------------------

#include <sstream>
#include <iomanip>

#include "cmdlib.h"
#include "cl_main.h"
#include "d_main.h"
#include "i_net.h"
#include "i_system.h"
#include "md5.h"
#include "m_argv.h"
#include "m_fileio.h"

#ifdef _XBOX
#include "i_xbox.h"
#endif

EXTERN_CVAR (waddirs)
EXTERN_CVAR (cl_serverdownload)
EXTERN_CVAR (cl_waddownloaddir)

void CL_Reconnect(void);

// GhostlyDeath <October 26, 2008> -- VC6 Compiler Error
// C2552: 'identifier' : non-aggregates cannot be initialized with initializer list
// What does this mean? VC6 considers std::string non-static (that it changes every time?)
// So it complains because std::string has a constructor!

/*struct download_s
{
	public:
		std::string filename;
		std::string md5;
		buf_t *buf;
		unsigned int got_bytes;
} download = { "", "", NULL, 0 };*/

// this works though!
struct download_s
{
	public:
		std::string filename;
		std::string md5;
		buf_t *buf;
		size_t got_bytes;
        dtime_t timeout;
		int retrycount;
		
		download_s()
		{
			buf = NULL;
			this->clear();
			timeout = 0;
		}

		~download_s()
		{
		}

		void clear()
		{
			filename = "";
			md5 = "";
			got_bytes = 0;
            timeout = 0;
            retrycount = 0;
            
			if (buf != NULL)
			{
				delete buf;
				buf = NULL;
			}
		}
} download;


extern std::string DownloadStr;

// Pretty prints supplied byte amount into a string
std::string FormatNBytes(float b)
{
    std::ostringstream osstr;
    float amt;

    if (b >= 1048576)
    {
        amt = b / 1048576;

        osstr << std::setprecision(4) << amt << 'M';
    }
    else
    if (b >= 1024)
    {
        amt = b / 1024;

        osstr << std::setprecision(4) << amt << 'K';
    }
    else
    {
       osstr << std::setprecision(4) << b << 'B';
    }

    return osstr.str();
}

void SetDownloadPercentage(size_t pc)
{
    std::stringstream ss;

    ss << "Downloading " << download.filename << ": " << pc << "%";

    DownloadStr = ss.str();
}

void ClearDownloadProgressBar()
{
    DownloadStr = "";
}


void IntDownloadComplete(void)
{
    std::string actual_md5 = MD5SUM(download.buf->ptr(), download.buf->maxsize());

	Printf(PRINT_HIGH, "\nDownload complete, got %u bytes\n", download.buf->maxsize());
	Printf(PRINT_HIGH, "%s\n %s\n", download.filename.c_str(), actual_md5.c_str());

	if(download.md5 == "")
	{
		Printf(PRINT_HIGH, "Server gave no checksum, assuming valid\n", (int)download.buf->maxsize());
	}
	else if(actual_md5 != download.md5)
	{
		Printf(PRINT_HIGH, " %s on server\n", download.md5.c_str());
		Printf(PRINT_HIGH, "Download failed: bad checksum\n");

		download.clear();
        CL_QuitNetGame();

        ClearDownloadProgressBar();

        return;
    }

    // got the wad! save it!
    std::vector<std::string> dirs;
    std::string filename;
    size_t i;
#ifdef _WIN32
    const char separator = ';';
#else
    const char separator = ':';
#endif

    // Try to save to the wad paths in this order -- Hyper_Eye
    D_AddSearchDir(dirs, cl_waddownloaddir.cstring(), separator);
    D_AddSearchDir(dirs, Args.CheckValue("-waddir"), separator);
    D_AddSearchDir(dirs, getenv("DOOMWADDIR"), separator);
    D_AddSearchDir(dirs, getenv("DOOMWADPATH"), separator);
    D_AddSearchDir(dirs, waddirs.cstring(), separator);
    dirs.push_back(startdir);
    dirs.push_back(progdir);

    dirs.erase(std::unique(dirs.begin(), dirs.end()), dirs.end());

    for(i = 0; i < dirs.size(); i++)
    {
        filename.clear();
        filename = dirs[i];
        if(filename[filename.length() - 1] != PATHSEPCHAR)
            filename += PATHSEP;
        filename += download.filename;

        // check for existing file
   	    if(M_FileExists(filename))
   	    {
   	        // there is an existing file, so use a new file whose name includes the checksum
   	        filename += ".";
   	        filename += actual_md5;
   	    }

        if (M_WriteFile(filename, download.buf->ptr(), download.buf->maxsize()))
            break;
    }

    // Unable to write
    if(i == dirs.size())
    {
		download.clear();
        CL_QuitNetGame();
        return;
    }

    Printf(PRINT_HIGH, "Saved download as \"%s\"\n", filename.c_str());

	download.clear();
    CL_QuitNetGame();
    CL_Reconnect();

    ClearDownloadProgressBar();
}

//
// CL_RequestDownload
// please sir, can i have some more?
//
void CL_RequestDownload(std::string filename, std::string filehash)
{
	if (cl_serverdownload)
	{
		// [Russell] - Allow resumeable downloads
		if ((download.filename != filename) ||
			(download.md5 != filehash))
		{
			download.filename = filename;
			download.md5 = filehash;
			download.got_bytes = 0;
		}

		// denis todo clear previous downloads
		MSG_WriteMarker(&net_buffer, clc_wantwad);
		MSG_WriteString(&net_buffer, filename.c_str());
		MSG_WriteString(&net_buffer, filehash.c_str());
		MSG_WriteLong(&net_buffer, download.got_bytes);

		NET_SendPacket(net_buffer, serveraddr);

		Printf(PRINT_HIGH, "Requesting download...\n");

		// check for completion
		// [Russell] - We go over the boundary, because sometimes the download will
		// pause at 100% if the server disconnected you previously, you can
		// reconnect a couple of times and this will let the checksum system do its
		// work

		if ((download.buf != NULL) &&
			(download.got_bytes >= download.buf->maxsize()))
		{
			IntDownloadComplete();
		}
	}
}

//
// CL_DownloadStart
// server tells us the size of the file we are about to download
//
void CL_DownloadStart()
{
	DWORD file_len = MSG_ReadLong();

	if(gamestate != GS_DOWNLOAD)
	{
		Printf(PRINT_HIGH, "Server initiated download failed\n");
		return;
	}

	// don't go for more than 100 megs
	if(file_len > 100*1024*1024)
	{
		Printf(PRINT_HIGH, "Download is over 100MiB, aborting!\n");
		CL_QuitNetGame();
		return;
	}

    // [Russell] - Allow resumeable downloads
	if (download.got_bytes == 0)
    {
        if (download.buf != NULL)
        {
            delete download.buf;
            download.buf = NULL;
        }

        download.buf = new buf_t ((size_t)file_len);

        memset(download.buf->ptr(), 0, file_len);
    }
    else
        Printf(PRINT_HIGH, "Resuming download of %s...\n", download.filename.c_str());



	Printf(PRINT_HIGH, "Downloading %s bytes...\n",
        FormatNBytes(file_len).c_str());

	// Make initial 0% show
	SetDownloadPercentage(0);
}

// Resets the timeout for a packet retry
void CL_DownloadTick()
{
    download.timeout = 0;
}

// Checks if we need to ask the server for a re-request of the current download
// chunk
void CL_DownloadTicker()
{
	dtime_t diff = 0;

	if(gamestate != GS_DOWNLOAD || download.filename.empty())
    {
		return;
    }

    if (download.timeout)
    {
        // Calculate how many seconds have elapsed since the last server 
        // response
        diff = I_GetTime() - download.timeout;

        if (diff)
            diff /= I_ConvertTimeFromMs(1000);
    }
    else
    {
        download.timeout = I_GetTime();
        return;
    }

    if (diff >= 3)
    {
		DPrintf("No response from server for %d seconds, re-requesting\n", diff);
		
		MSG_WriteMarker(&net_buffer, clc_wantwad);
		MSG_WriteString(&net_buffer, download.filename.c_str());
		MSG_WriteString(&net_buffer, download.md5.c_str());
		MSG_WriteLong(&net_buffer, download.got_bytes);

		NET_SendPacket(net_buffer, serveraddr);

		download.timeout = 0;

		++download.retrycount;
    }

    if (download.retrycount >= 5)
    {
        Printf(PRINT_HIGH, "Server hasn't responded to download re-requests, aborting\n");

        download.retrycount = 0;
        download.timeout = 0;

		CL_QuitNetGame();

		gamestate = GS_STARTUP;
    }
}

//
// CL_Download
// denis - get a little chunk of the file and store it, much like a hampster. Well, hamster; but hampsters can dance and sing. Also much like Scraps, the Ice Age squirrel thing, stores his acorn. Only with a bit more success. Actually, quite a bit more success, specifically as in that the world doesn't crack apart when we store our chunk and it does when Scraps stores his (or her?) acorn. But when Scraps does it, it is funnier. The rest of Ice Age mostly sucks.
//
void CL_Download()
{
	DWORD offset = MSG_ReadLong();
	size_t len = MSG_ReadShort();
	size_t left = MSG_BytesLeft();
	void *p = MSG_ReadChunk(len);

	if(gamestate != GS_DOWNLOAD)
		return;

	if (download.buf == NULL)
	{
		// We must have not received the svc_wadinfo message
		Printf(PRINT_HIGH, "Unable to start download, aborting\n");
		download.clear();
		CL_QuitNetGame();
		return;
	}

	// check ranges
	if(offset + len > download.buf->maxsize() || len > left || p == NULL)
	{
		Printf(PRINT_HIGH, "Bad download packet (%d, %d) encountered (%d), aborting\n", (int)offset, (int)left, (int)download.buf->size());

		download.clear();
		CL_QuitNetGame();
		return;
	}

	// Reset retransmission timer
	CL_DownloadTick();
	
	if (offset < download.got_bytes)
        return;
	
	// check for missing packet, re-request
	if(offset > download.got_bytes)
	{
		DPrintf("Missed a packet after %d bytes (got %d), re-requesting\n", download.got_bytes, offset);
		MSG_WriteMarker(&net_buffer, clc_wantwad);
		MSG_WriteString(&net_buffer, download.filename.c_str());
		MSG_WriteString(&net_buffer, download.md5.c_str());
		MSG_WriteLong(&net_buffer, download.got_bytes);
		NET_SendPacket(net_buffer, serveraddr);
		return;
	}

	// send keepalive
	NET_SendPacket(net_buffer, serveraddr);

	// copy into downloaded buffer
	memcpy(download.buf->ptr() + offset, p, len);
	download.got_bytes += len;

	// calculate percentage for the user
	static size_t old_percent = 0;
	size_t percent = (download.got_bytes*100)/download.buf->maxsize();
	if(percent != old_percent)
	{
        SetDownloadPercentage(percent);

		old_percent = percent;
	}

	// check for completion
	// [Russell] - We go over the boundary, because sometimes the download will
	// pause at 100% if the server disconnected you previously, you can
	// reconnect a couple of times and this will let the checksum system do its
	// work
	if(download.got_bytes >= download.buf->maxsize())
	{
        IntDownloadComplete();
	}
}

VERSION_CONTROL (cl_download_cpp, "$Id: 3a00df02f20af65fcaddc0542a2078ddee6868a6 $")
