/** 
 * @file updater.cpp
 * @brief Windows auto-updater
 *
 * $LicenseInfo:firstyear=2002&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2010, Linden Research, Inc.
 * 
 * This library 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;
 * version 2.1 of the License only.
 * 
 * This library 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 library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 * 
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 */

//
// Usage: updater -url <url>
//

// We use dangerous fopen, strtok, mbstowcs, sprintf
// which generates warnings on VC2005.
// *TODO: Switch to fopen_s, strtok_s, etc.
#define _CRT_SECURE_NO_DEPRECATE

#include <windows.h>
#include <wininet.h>
#include <stdio.h>
#include <string>
#include <iostream>
#include <stdexcept>
#include <sstream>
#include <fstream>

#define BUFSIZE 8192

int  gTotalBytesRead = 0;
DWORD gTotalBytes = -1;
HWND gWindow = NULL;
WCHAR gProgress[256];
char* gUpdateURL = NULL;

#if _DEBUG
std::ofstream logfile;
#define DEBUG(expr) logfile << expr << std::endl
#else
#define DEBUG(expr) /**/
#endif

char* wchars_to_utf8chars(const WCHAR* in_chars)
{
	int tlen = 0;
	const WCHAR* twc = in_chars;
	while (*twc++ != 0)
	{
		tlen++;
	}
	char* outchars = new char[tlen];
	char* res = outchars;
	for (int i=0; i<tlen; i++)
	{
		int cur_char = (int)(*in_chars++);
		if (cur_char < 0x80)
		{
			*outchars++ = (char)cur_char;
		}
		else
		{
			*outchars++ = '?';
		}
	}
	*outchars = 0;
	return res;
}

class Fetcher
{
public:
    Fetcher(const std::wstring& uri)
    {
        // These actions are broken out as separate methods not because it
        // makes the code clearer, but to avoid triggering AntiVir and
        // McAfee-GW-Edition virus scanners (DEV-31680).
        mInet = openInet();
        mDownload = openUrl(uri);
    }

    ~Fetcher()
    {
        DEBUG("Calling InternetCloseHandle");
        InternetCloseHandle(mDownload);
        InternetCloseHandle(mInet);
    }

    unsigned long read(char* buffer, size_t bufflen) const;

    DWORD getTotalBytes() const
    {
        DWORD totalBytes;
        DWORD sizeof_total_bytes = sizeof(totalBytes);
        HttpQueryInfo(mDownload, HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER,
                      &totalBytes, &sizeof_total_bytes, NULL);
        return totalBytes;
    }

    struct InetError: public std::runtime_error
    {
        InetError(const std::string& what): std::runtime_error(what) {}
    };

private:
    // We test results from a number of different MS functions with different
    // return types -- but the common characteristic is that 0 (i.e. (! result))
    // means an error of some kind.
    template <typename RESULT>
    static RESULT check(const std::string& desc, RESULT result)
    {
        if (result)
        {
            // success, show caller
            return result;
        }
        DWORD err = GetLastError();
        std::ostringstream out;
        out << desc << " Failed: " << err;
        DEBUG(out.str());
        throw InetError(out.str());
    }

    HINTERNET openUrl(const std::wstring& uri) const;
    HINTERNET openInet() const;

    HINTERNET mInet, mDownload;
};

HINTERNET Fetcher::openInet() const
{
    DEBUG("Calling InternetOpen");
    // Init wininet subsystem
    return check("InternetOpen",
                 InternetOpen(L"LindenUpdater", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0));
}

HINTERNET Fetcher::openUrl(const std::wstring& uri) const
{
    DEBUG("Calling InternetOpenUrl: " << wchars_to_utf8chars(uri.c_str()));
    return check("InternetOpenUrl",
                 InternetOpenUrl(mInet, uri.c_str(), NULL, 0, INTERNET_FLAG_NEED_FILE, NULL));
}

unsigned long Fetcher::read(char* buffer, size_t bufflen) const
{
    unsigned long bytes_read = 0;
    DEBUG("Calling InternetReadFile");
    check("InternetReadFile",
          InternetReadFile(mDownload, buffer, bufflen, &bytes_read));
    return bytes_read;
}

int WINAPI get_url_into_file(const std::wstring& uri, const std::string& path, int *cancelled)
{
	int success = FALSE;
	*cancelled = FALSE;

    DEBUG("Opening '" << path << "'");

	FILE* fp = fopen(path.c_str(), "wb");		/* Flawfinder: ignore */

	if (!fp)
	{
        DEBUG("Failed to open '" << path << "'");
		return success;
	}

    // Note, ctor can throw, since it uses check() function.
    Fetcher fetcher(uri);
    gTotalBytes = fetcher.getTotalBytes();

/*==========================================================================*|
    // nobody uses total_bytes?!? What's this doing here?
	DWORD total_bytes = 0;
	success = check("InternetQueryDataAvailable",
                    InternetQueryDataAvailable(hdownload, &total_bytes, 0, 0));
|*==========================================================================*/

	success = FALSE;
	while(!success && !(*cancelled))
	{
        char data[BUFSIZE];		/* Flawfinder: ignore */
        unsigned long bytes_read = fetcher.read(data, sizeof(data));

		if (!bytes_read)
		{
            DEBUG("InternetReadFile Read " << bytes_read << " bytes.");
		}

        DEBUG("Reading Data, bytes_read = " << bytes_read);
		
		if (bytes_read == 0)
		{
			// If InternetFileRead returns TRUE AND bytes_read == 0
			// we've successfully downloaded the entire file
			wsprintf(gProgress, L"Download complete.");
			success = TRUE;
		}
		else
		{
			// write what we've got, then continue
			fwrite(data, sizeof(char), bytes_read, fp);

			gTotalBytesRead += int(bytes_read);

			if (gTotalBytes != -1)
				wsprintf(gProgress, L"Downloaded: %d%%", 100 * gTotalBytesRead / gTotalBytes);
			else
				wsprintf(gProgress, L"Downloaded: %dK", gTotalBytesRead / 1024);

		}

        DEBUG("Calling InvalidateRect");
		
		// Mark the window as needing redraw (of the whole thing)
		InvalidateRect(gWindow, NULL, TRUE);

		// Do the redraw
        DEBUG("Calling UpdateWindow");
		UpdateWindow(gWindow);

        DEBUG("Calling PeekMessage");
		MSG msg;
		while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);

			if (msg.message == WM_QUIT)
			{
				// bail out, user cancelled
				*cancelled = TRUE;
			}
		}
	}

	fclose(fp);
	return success;
}

LRESULT CALLBACK WinProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
{
	HDC hdc;			// Drawing context
	PAINTSTRUCT ps;

	switch(message)
	{
	case WM_PAINT:
		{
			hdc = BeginPaint(hwnd, &ps);

			RECT rect;
			GetClientRect(hwnd, &rect);
			DrawText(hdc, gProgress, -1, &rect, 
				DT_SINGLELINE | DT_CENTER | DT_VCENTER);

			EndPaint(hwnd, &ps);
			return 0;
		}
	case WM_CLOSE:
	case WM_DESTROY:
		// Get out of full screen
		// full_screen_mode(false);
		PostQuitMessage(0);
		return 0;
	}
	return DefWindowProc(hwnd, message, wparam, lparam);
}

#define win_class_name L"FullScreen"

int parse_args(int argc, char **argv)
{
	int j;

	for (j = 1; j < argc; j++) 
	{
		if ((!strcmp(argv[j], "-url")) && (++j < argc)) 
		{
			gUpdateURL = argv[j];
		}
	}

	// If nothing was set, let the caller know.
	if (!gUpdateURL)
	{
		return 1;
	}
	return 0;
}
	
int WINAPI
WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
	// Parse the command line.
	LPSTR cmd_line_including_exe_name = GetCommandLineA();

	const int MAX_ARGS = 100;
	int argc = 0;
	char* argv[MAX_ARGS];		/* Flawfinder: ignore */

#if _DEBUG
	logfile.open("updater.log", std::ios_base::out);
    DEBUG("Parsing command arguments");
#endif
	
	char *token = NULL;
	if( cmd_line_including_exe_name[0] == '\"' )
	{
		// Exe name is enclosed in quotes
		token = strtok( cmd_line_including_exe_name, "\"" );
		argv[argc++] = token;
		token = strtok( NULL, " \t," );
	}
	else
	{
		// Exe name is not enclosed in quotes
		token = strtok( cmd_line_including_exe_name, " \t," );
	}

	while( (token != NULL) && (argc < MAX_ARGS) )
	{
		argv[argc++] = token;
		/* Get next token: */
		if (*(token + strlen(token) + 1) == '\"')		/* Flawfinder: ignore */
		{
			token = strtok( NULL, "\"");
		}
		else
		{
			token = strtok( NULL, " \t," );
		}
	}

	gUpdateURL = NULL;

	/////////////////////////////////////////
	//
	// Process command line arguments
	//

    DEBUG("Processing command arguments");
	
	//
	// Parse the command line arguments
	//
	int parse_args_result = parse_args(argc, argv);
	
	WNDCLASSEX wndclassex = { 0 };
	//DEVMODE dev_mode = { 0 };

	const int WINDOW_WIDTH = 250;
	const int WINDOW_HEIGHT = 100;

	wsprintf(gProgress, L"Connecting...");

	/* Init the WNDCLASSEX */
	wndclassex.cbSize = sizeof(WNDCLASSEX);
	wndclassex.style = CS_HREDRAW | CS_VREDRAW;
	wndclassex.hInstance = hInstance;
	wndclassex.lpfnWndProc = WinProc;
	wndclassex.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
	wndclassex.lpszClassName = win_class_name;
	
	RegisterClassEx(&wndclassex);
	
	// Get the size of the screen
	//EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &dev_mode);
	
	gWindow = CreateWindowEx(NULL, win_class_name, 
		L"Second Life Updater",
		WS_OVERLAPPEDWINDOW, 
		CW_USEDEFAULT, 
		CW_USEDEFAULT, 
		WINDOW_WIDTH, 
		WINDOW_HEIGHT,
		NULL, NULL, hInstance, NULL);

	ShowWindow(gWindow, nShowCmd);
	UpdateWindow(gWindow);

	if (parse_args_result)
	{
		MessageBox(gWindow, 
				L"Usage: updater -url <url> [-name <window_title>] [-program <program_name>] [-silent]",
				L"Usage", MB_OK);
		return parse_args_result;
	}

	// Did we get a userserver to work with?
	if (!gUpdateURL)
	{
		MessageBox(gWindow, L"Please specify the download url from the command line",
			L"Error", MB_OK);
		return 1;
	}

	// Can't feed GetTempPath into GetTempFile directly
	char temp_path[MAX_PATH];		/* Flawfinder: ignore */
	if (0 == GetTempPathA(sizeof(temp_path), temp_path))
	{
		MessageBox(gWindow, L"Problem with GetTempPath()",
			L"Error", MB_OK);
		return 1;
	}
    std::string update_exec_path(temp_path);
	update_exec_path.append("Second_Life_Updater.exe");

	WCHAR update_uri[4096];
    mbstowcs(update_uri, gUpdateURL, sizeof(update_uri));

	int success = 0;
	int cancelled = 0;

	// Actually do the download
    try
    {
        DEBUG("Calling get_url_into_file");
        success = get_url_into_file(update_uri, update_exec_path, &cancelled);
    }
    catch (const Fetcher::InetError& e)
    {
        (void)e;
        success = FALSE;
        DEBUG("Caught: " << e.what());
    }

	// WinInet can't tell us if we got a 404 or not.  Therefor, we check
	// for the size of the downloaded file, and assume that our installer
	// will always be greater than 1MB.
	if (gTotalBytesRead < (1024 * 1024) && ! cancelled)
	{
		MessageBox(gWindow,
			L"The Second Life auto-update has failed.\n"
			L"The problem may be caused by other software installed \n"
			L"on your computer, such as a firewall.\n"
			L"Please visit http://secondlife.com/download/ \n"
			L"to download the latest version of Second Life.\n",
			NULL, MB_OK);
		return 1;
	}

	if (cancelled)
	{
		// silently exit
		return 0;
	}

	if (!success)
	{
		MessageBox(gWindow, 
			L"Second Life download failed.\n"
			L"Please try again later.", 
			NULL, MB_OK);
		return 1;
	}

	// TODO: Make updates silent (with /S to NSIS)
	//char params[256];		/* Flawfinder: ignore */
	//sprintf(params, "/S");	/* Flawfinder: ignore */
	//MessageBox(gWindow, 
	//	L"Updating Second Life.\n\nSecond Life will automatically start once the update is complete.  This may take a minute...",
	//	L"Download Complete",
	//	MB_OK);
		
/*==========================================================================*|
    // DEV-31680: ShellExecuteA() causes McAfee-GW-Edition and AntiVir
    // scanners to flag this executable as a probable virus vector.
    // Less than or equal to 32 means failure
	if (32 >= (int) ShellExecuteA(gWindow, "open", update_exec_path.c_str(), NULL, 
		"C:\\", SW_SHOWDEFAULT))
|*==========================================================================*/
    // from http://msdn.microsoft.com/en-us/library/ms682512(VS.85).aspx
    STARTUPINFOA si;
    PROCESS_INFORMATION pi;
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));

    if (! CreateProcessA(update_exec_path.c_str(), // executable file
                  NULL,                            // command line
                  NULL,             // process cannot be inherited
                  NULL,             // thread cannot be inherited
                  FALSE,            // do not inherit existing handles
                  0,                // process creation flags
                  NULL,             // inherit parent's environment
                  NULL,             // inherit parent's current dir
                  &si,              // STARTUPINFO
                  &pi))             // PROCESS_INFORMATION
	{
		MessageBox(gWindow, L"Update failed.  Please try again later.", NULL, MB_OK);
		return 1;
	}

	// Give installer some time to open a window
	Sleep(1000);

	return 0;
}