// Copyright (c) 2007, Rodrigo Braz Monteiro
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//   * Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//   * Neither the name of the Aegisub Group nor the names of its contributors
//     may be used to endorse or promote products derived from this software
//     without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// Aegisub Project http://www.aegisub.org/
//
// $Id$

/// @file dialog_version_check.cpp
/// @brief Version Checker dialogue box and logic
/// @ingroup configuration_ui
///


#include "config.h"

#ifdef WITH_UPDATE_CHECKER

#include "dialog_version_check.h"

#ifndef AGI_PRE
#ifdef WIN32
// Congratulation wx, you forgot to include a header somewhere
#include <winsock2.h>
#endif

#include <wx/app.h>
#include <wx/button.h>
#include <wx/checkbox.h>
#include <wx/dialog.h>
#include <wx/event.h>
#include <wx/hyperlink.h>
#include <wx/intl.h>
#include <wx/platinfo.h>
#include <wx/protocol/http.h>
#include <wx/sizer.h>
#include <wx/statline.h>
#include <wx/stattext.h>
#include <wx/string.h>
#include <wx/textctrl.h>
#include <wx/thread.h>
#include <wx/tokenzr.h>
#include <wx/txtstrm.h>

#include <algorithm>
#include <memory>
#include <set>
#include <tr1/functional>
#include <vector>
#endif

#include <wx/sstream.h>

#include "compat.h"
#include "main.h"
#include "string_codec.h"
#include "version.h"

#include <libaegisub/exception.h>

#ifdef __APPLE__
#include <CoreFoundation/CoreFoundation.h>
#endif

/* *** Public API is implemented here *** */

// Allocate global lock mutex declared in header
wxMutex VersionCheckLock;

class AegisubVersionCheckerThread : public wxThread {
	bool interactive;
	void DoCheck();
	void PostErrorEvent(const wxString &error_text);
	ExitCode Entry();
public:
	AegisubVersionCheckerThread(bool interactive);
};

// Public API for version checker
void PerformVersionCheck(bool interactive)
{
	new AegisubVersionCheckerThread(interactive);
}

/* *** The actual implementation begins here *** */

struct AegisubUpdateDescription {
	wxString url;
	wxString friendly_name;
	wxString description;
};

class AegisubVersionCheckResultEvent : public wxEvent {
	wxString main_text;
	std::vector<AegisubUpdateDescription> updates;

public:
	AegisubVersionCheckResultEvent(wxString message = wxString());


	wxEvent *Clone() const
	{
		return new AegisubVersionCheckResultEvent(*this);
	}

	const wxString & GetMainText() const { return main_text; }

	// If there are no updates in the list, either none were found or an error occurred,
	// either way it means "failure" if it's empty
	const std::vector<AegisubUpdateDescription> & GetUpdates() const { return updates; }
	void AddUpdate(const wxString &url, const wxString &friendly_name, const wxString &description)
	{
		updates.push_back(AegisubUpdateDescription());
		AegisubUpdateDescription &desc = updates.back();
		desc.url = url;
		desc.friendly_name = friendly_name;
		desc.description = description;
	}
};

wxDEFINE_EVENT(AEGISUB_EVENT_VERSIONCHECK_RESULT, AegisubVersionCheckResultEvent);

AegisubVersionCheckResultEvent::AegisubVersionCheckResultEvent(wxString message)
: wxEvent(0, AEGISUB_EVENT_VERSIONCHECK_RESULT)
, main_text(message)
{
}


DEFINE_SIMPLE_EXCEPTION_NOINNER(VersionCheckError, agi::Exception, "versioncheck")

static void register_event_handler();

AegisubVersionCheckerThread::AegisubVersionCheckerThread(bool interactive)
: wxThread(wxTHREAD_DETACHED)
, interactive(interactive)
{
	register_event_handler();

#ifndef __APPLE__
	if (!wxSocketBase::IsInitialized())
		wxSocketBase::Initialize();
#endif

	Create();
	Run();
}

wxThread::ExitCode AegisubVersionCheckerThread::Entry()
{
	if (!interactive)
	{
		// Automatic checking enabled?
		if (!OPT_GET("App/Auto/Check For Updates")->GetBool())
			return 0;

		// Is it actually time for a check?
		time_t next_check = OPT_GET("Version/Next Check")->GetInt();
		if (next_check > wxDateTime::GetTimeNow())
			return 0;
	}

	if (VersionCheckLock.TryLock() != wxMUTEX_NO_ERROR) return 0;

	try {
		DoCheck();
	}
	catch (const agi::Exception &e) {
		PostErrorEvent(wxString::Format(
			_("There was an error checking for updates to Aegisub:\n%s\n\nIf other applications can access the Internet fine, this is probably a temporary server problem on our end."),
			e.GetMessage()));
	}
	catch (...) {
		PostErrorEvent(_("An unknown error occurred while checking for updates to Aegisub."));
	}

	VersionCheckLock.Unlock();

	// While Options isn't perfectly thread safe, this should still be okay.
	// Traversing the std::map to find the key-value pair doesn't modify any data as long as
	// the key already exists (which it does at this point), and modifying the value only
	// touches that specific key-value pair and will never cause a rebalancing of the tree,
	// because the tree only depends on the keys.
	// Lastly, writing options to disk only happens when Options.Save() is called.
	time_t new_next_check_time = wxDateTime::GetTimeNow() + 60*60; // in one hour
	OPT_SET("Version/Next Check")->SetInt((int)new_next_check_time);

	return 0;
}

void AegisubVersionCheckerThread::PostErrorEvent(const wxString &error_text)
{
	if (interactive)
		wxTheApp->AddPendingEvent(AegisubVersionCheckResultEvent(error_text));
}


static const char * GetOSShortName()
{
	int osver_maj, osver_min;
	wxOperatingSystemId osid = wxGetOsVersion(&osver_maj, &osver_min);

	if (osid & wxOS_WINDOWS_NT)
	{
		if (osver_maj == 5 && osver_min == 0)
			return "win2k";
		else if (osver_maj == 5 && osver_min == 1)
			return "winxp";
		else if (osver_maj == 5 && osver_min == 2)
			return "win2k3"; // this is also xp64
		else if (osver_maj == 6 && osver_min == 0)
			return "win60"; // vista and server 2008
		else if (osver_maj == 6 && osver_min == 1)
			return "win61"; // 7 and server 2008r2
		else if (osver_maj == 6 && osver_min == 2)
			return "win62"; // 8
		else
			return "windows"; // future proofing? I doubt we run on nt4
	}
	// CF returns 0x10 for some reason, which wx has recently started
	// turning into 10
	else if (osid & wxOS_MAC_OSX_DARWIN && (osver_maj == 0x10 || osver_maj == 10))
	{
		// ugliest hack in the world? nah.
		static char osxstring[] = "osx00";
		char minor = osver_min >> 4;
		char patch = osver_min & 0x0F;
		osxstring[3] = minor + ((minor<=9) ? '0' : ('a'-1));
		osxstring[4] = patch + ((patch<=9) ? '0' : ('a'-1));
		return osxstring;
	}
	else if (osid & wxOS_UNIX_LINUX)
		return "linux";
	else if (osid & wxOS_UNIX_FREEBSD)
		return "freebsd";
	else if (osid & wxOS_UNIX_OPENBSD)
		return "openbsd";
	else if (osid & wxOS_UNIX_NETBSD)
		return "netbsd";
	else if (osid & wxOS_UNIX_SOLARIS)
		return "solaris";
	else if (osid & wxOS_UNIX_AIX)
		return "aix";
	else if (osid & wxOS_UNIX_HPUX)
		return "hpux";
	else if (osid & wxOS_UNIX)
		return "unix";
	else if (osid & wxOS_OS2)
		return "os2";
	else if (osid & wxOS_DOS)
		return "dos";
	else
		return "unknown";
}


#ifdef WIN32
typedef BOOL (WINAPI * PGetUserPreferredUILanguages)(DWORD dwFlags, PULONG pulNumLanguages, wchar_t *pwszLanguagesBuffer, PULONG pcchLanguagesBuffer);

// Try using Win 6+ functions if available
static wxString GetUILanguage()
{
	agi::scoped_holder<HMODULE, BOOL (__stdcall *)(HMODULE)> kernel32(LoadLibraryW(L"kernel32.dll"), FreeLibrary);
	if (!kernel32) return "";

	PGetUserPreferredUILanguages gupuil = (PGetUserPreferredUILanguages)GetProcAddress(kernel32, "GetUserPreferredUILanguages");
	if (!gupuil) return "";

	ULONG numlang = 0, output_len = 0;
	if (gupuil(MUI_LANGUAGE_NAME, &numlang, 0, &output_len) != TRUE || !output_len)
		return "";

	std::vector<wchar_t> output(output_len);
	if (!gupuil(MUI_LANGUAGE_NAME, &numlang, &output[0], &output_len) || numlang < 1)
		return "";

	// We got at least one language, just treat it as the only, and a null-terminated string
	return &output[0];
}
static wxString GetSystemLanguage()
{
	wxString res = GetUILanguage();
	if (!res)
	{
		// On an old version of Windows, let's just return the LANGID as a string
		res = wxString::Format("x-win%04x", GetUserDefaultUILanguage());
	}

	return res;
}
#elif __APPLE__
static wxString GetSystemLanguage()
{
	CFLocaleRef locale = CFLocaleCopyCurrent();
	CFStringRef localeName = (CFStringRef)CFLocaleGetValue(locale, kCFLocaleIdentifier);

	char buf[128] = { 0 };
	CFStringGetCString(localeName, buf, sizeof buf, kCFStringEncodingUTF8);
	CFRelease(locale);

	return wxString::FromUTF8(buf);

}
#else
static wxString GetSystemLanguage()
{
	return wxLocale::GetLanguageInfo(wxLocale::GetSystemLanguage())->CanonicalName;
}
#endif

static wxString GetAegisubLanguage()
{
	return lagi_wxString(OPT_GET("App/Language")->GetString());
}

template<class OutIter>
static void split_str(wxString const& str, wxString const& sep, bool empty, OutIter out)
{
	wxStringTokenizer tk(str, sep, empty ? wxTOKEN_DEFAULT : wxTOKEN_RET_EMPTY_ALL);
	while (tk.HasMoreTokens())
	{
		*out++ = tk.GetNextToken();
	}
}


void AegisubVersionCheckerThread::DoCheck()
{
	std::set<wxString> accept_tags;
#ifdef UPDATE_CHECKER_ACCEPT_TAGS
	split_str(wxString(UPDATE_CHECKER_ACCEPT_TAGS, wxConvUTF8), " ", false,
		inserter(accept_tags, accept_tags.end()));
#endif

#ifdef __APPLE__
	wxString cmd = wxString::Format(
		"curl --silent --fail 'http://%s/%s?rev=%d&rel=%d&os=%s&lang=%s&aegilang=%s'",
		UPDATE_CHECKER_SERVER,
		UPDATE_CHECKER_BASE_URL,
		GetSVNRevision(),
		GetIsOfficialRelease()?1:0,
		GetOSShortName(),
		GetSystemLanguage(),
		GetAegisubLanguage());

	// wxExecute only works on the main thread so use popen instead
	char buf[1024];
	FILE *pipe = popen(cmd.utf8_str(), "r");
	if (!pipe)
		throw VersionCheckError("Could not run curl");

	wxString update_str;
	while(fgets(buf, sizeof(buf), pipe))
		update_str += buf;

	int ret = pclose(pipe);
	switch (ret)
	{
		case 0:
			break;
		case 6:
		case 7:
			throw VersionCheckError(STD_STR(_("Could not connect to updates server.")));
		case 22:
			throw VersionCheckError(STD_STR(_("Could not download from updates server.")));
		default:
			throw VersionCheckError(STD_STR(wxString::Format("curl failed with error code %d", ret)));
	}

	agi::scoped_ptr<wxStringInputStream> stream(new wxStringInputStream(update_str));
#else
	wxString path = wxString::Format(
		"%s?rev=%d&rel=%d&os=%s&lang=%s&aegilang=%s",
		UPDATE_CHECKER_BASE_URL,
		GetSVNRevision(),
		GetIsOfficialRelease()?1:0,
		GetOSShortName(),
		GetSystemLanguage(),
		GetAegisubLanguage());

	wxHTTP http;
	http.SetHeader("User-Agent", wxString("Aegisub ") + GetAegisubLongVersionString());
	http.SetHeader("Connection", "Close");
	http.SetFlags(wxSOCKET_WAITALL | wxSOCKET_BLOCK);

	if (!http.Connect(UPDATE_CHECKER_SERVER))
		throw VersionCheckError(STD_STR(_("Could not connect to updates server.")));

	agi::scoped_ptr<wxInputStream> stream(http.GetInputStream(path));
	if (!stream) // check for null-pointer
		throw VersionCheckError(STD_STR(_("Could not download from updates server.")));

	if (http.GetResponse() < 200 || http.GetResponse() >= 300) {
		throw VersionCheckError(STD_STR(wxString::Format(_("HTTP request failed, got HTTP response %d."), http.GetResponse())));
	}
#endif
	wxTextInputStream text(*stream);

	AegisubVersionCheckResultEvent result_event;

	while (!stream->Eof() && stream->GetSize() > 0)
	{
		wxArrayString parsed;
		split_str(text.ReadLine(), "|", true, std::back_inserter(parsed));
		if (parsed.size() != 6) continue;

		wxString line_type = parsed[0];
		wxString line_revision = parsed[1];
		wxString line_tags_str = parsed[2];
		wxString line_url = inline_string_decode(parsed[3]);
		wxString line_friendlyname = inline_string_decode(parsed[4]);
		wxString line_description = inline_string_decode(parsed[5]);

		// stable runners don't want unstable, not interesting, skip
		if ((line_type == "branch" || line_type == "dev") && GetIsOfficialRelease())
			continue;

		// check if the tags match
		if (line_tags_str.empty() || line_tags_str == "all")
		{
			// looking good
		}
		else
		{
			std::set<wxString> tags;
			split_str(line_tags_str, " ", false, inserter(tags, tags.end()));
			if (!includes(accept_tags.begin(), accept_tags.end(), tags.begin(), tags.end()))
				continue;
		}

		if (line_type == "upgrade" || line_type == "bugfix")
		{
			// de facto interesting
		}
		else
		{
			// maybe interesting, check revision

			long new_revision = 0;
			if (!line_revision.ToLong(&new_revision)) continue;
			if (new_revision <= GetSVNRevision())
			{
				// too old, not interesting, skip
				continue;
			}
		}

		// it's interesting!
		result_event.AddUpdate(line_url, line_friendlyname, line_description);
	}

	if (result_event.GetUpdates().size() > 0 || interactive)
	{
		wxTheApp->AddPendingEvent(result_event);
	}
}

class VersionCheckerResultDialog : public wxDialog {
	void OnCloseButton(wxCommandEvent &evt);
	void OnRemindMeLater(wxCommandEvent &evt);
	void OnClose(wxCloseEvent &evt);

	wxCheckBox *automatic_check_checkbox;

public:
	VersionCheckerResultDialog(const wxString &main_text, const std::vector<AegisubUpdateDescription> &updates);

	bool ShouldPreventAppExit() const { return false; }
};

VersionCheckerResultDialog::VersionCheckerResultDialog(const wxString &main_text, const std::vector<AegisubUpdateDescription> &updates)
: wxDialog(0, -1, _("Version Checker"))
{
	const int controls_width = 500;

	wxSizer *main_sizer = new wxBoxSizer(wxVERTICAL);

	wxStaticText *text = new wxStaticText(this, -1, main_text);
	text->Wrap(controls_width);
	main_sizer->Add(text, 0, wxBOTTOM|wxEXPAND, 6);

	std::vector<AegisubUpdateDescription>::const_iterator upd_iterator = updates.begin();
	for (; upd_iterator != updates.end(); ++upd_iterator)
	{
		main_sizer->Add(new wxStaticLine(this), 0, wxEXPAND|wxALL, 6);

		text = new wxStaticText(this, -1, upd_iterator->friendly_name);
		wxFont boldfont = text->GetFont();
		boldfont.SetWeight(wxFONTWEIGHT_BOLD);
		text->SetFont(boldfont);
		main_sizer->Add(text, 0, wxEXPAND|wxBOTTOM, 6);

		wxTextCtrl *descbox = new wxTextCtrl(this, -1, upd_iterator->description, wxDefaultPosition, wxSize(controls_width,60), wxTE_MULTILINE|wxTE_READONLY);
		main_sizer->Add(descbox, 0, wxEXPAND|wxBOTTOM, 6);

		main_sizer->Add(new wxHyperlinkCtrl(this, -1, upd_iterator->url, upd_iterator->url), 0, wxALIGN_LEFT|wxBOTTOM, 6);
	}

	automatic_check_checkbox = new wxCheckBox(this, -1, _("&Auto Check for Updates"));
	automatic_check_checkbox->SetValue(OPT_GET("App/Auto/Check For Updates")->GetBool());

	wxButton *remind_later_button = 0;
	if (updates.size() > 0)
		remind_later_button = new wxButton(this, wxID_NO, _("Remind me again in a &week"));

	wxButton *close_button = new wxButton(this, wxID_OK, _("&Close"));
	SetAffirmativeId(wxID_OK);
	SetEscapeId(wxID_OK);

	if (updates.size())
		main_sizer->Add(new wxStaticLine(this), 0, wxEXPAND|wxALL, 6);
	main_sizer->Add(automatic_check_checkbox, 0, wxEXPAND|wxBOTTOM, 6);

	wxStdDialogButtonSizer *button_sizer = new wxStdDialogButtonSizer();
	button_sizer->AddButton(close_button);
	if (remind_later_button)
		button_sizer->AddButton(remind_later_button);
	button_sizer->Realize();
	main_sizer->Add(button_sizer, 0, wxEXPAND, 0);

	wxSizer *outer_sizer = new wxBoxSizer(wxVERTICAL);
	outer_sizer->Add(main_sizer, 0, wxALL|wxEXPAND, 12);

	SetSizerAndFit(outer_sizer);
	Centre();
	Show();

	Bind(wxEVT_COMMAND_BUTTON_CLICKED, std::tr1::bind(&VersionCheckerResultDialog::Close, this, false), wxID_OK);
	Bind(wxEVT_COMMAND_BUTTON_CLICKED, &VersionCheckerResultDialog::OnRemindMeLater, this, wxID_NO);
	Bind(wxEVT_CLOSE_WINDOW, &VersionCheckerResultDialog::OnClose, this);
}
void VersionCheckerResultDialog::OnRemindMeLater(wxCommandEvent &)
{
	// In one week
	time_t new_next_check_time = wxDateTime::Today().GetTicks() + 7*24*60*60;
	OPT_SET("Version/Next Check")->SetInt((int)new_next_check_time);

	Close();
}

void VersionCheckerResultDialog::OnClose(wxCloseEvent &)
{
	OPT_SET("App/Auto/Check For Updates")->SetBool(automatic_check_checkbox->GetValue());
	Destroy();
}

static void on_update_result(AegisubVersionCheckResultEvent &evt)
{
	wxString text = evt.GetMainText();
	if (!text)
	{
		if (evt.GetUpdates().size() == 1)
		{
			text = _("An update to Aegisub was found.");
		}
		else if (evt.GetUpdates().size() > 1)
		{
			text = _("Several possible updates to Aegisub were found.");
		}
		else
		{
			text = _("There are no updates to Aegisub.");
		}
	}

	new VersionCheckerResultDialog(text, evt.GetUpdates());
}

static void register_event_handler()
{
	static bool is_registered = false;
	if (is_registered) return;

	wxTheApp->Bind(AEGISUB_EVENT_VERSIONCHECK_RESULT, on_update_result);
	is_registered = true;
}

#endif
