// Copyright (c) 2009, Niels Martin Hansen
// 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
//
// Website: http://www.aegisub.org/
// Contact: mailto:nielsm@aegisub.org
//



#include "config.h"

#include "dialog_version_check.h"
#include "options.h"
#include "include/aegisub/exception.h"
#include "string_codec.h"
#include "version.h"

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

#ifdef __WXMAC__
// We use a CFHTTP based implementation on Mac
#include <Carbon/Carbon.h>
#include <wx/sstream.h>
#endif

#include <wx/string.h>
#include <wx/event.h>
#include <wx/thread.h>
#include <wx/protocol/http.h>
#include <wx/txtstrm.h>
#include <wx/platinfo.h>
#include <wx/tokenzr.h>
#include <wx/hyperlink.h>
#include <wx/intl.h>
#include <memory>
#include <set>


/* *** 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);
public:
	AegisubVersionCheckerThread(bool interactive);
	virtual ExitCode Entry();
};

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



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



// A new event class for reporting the result of checking

DEFINE_EVENT_TYPE(AEGISUB_EVENT_VERSIONCHECK_RESULT)

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

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

public:
	AegisubVersionCheckResultEvent()
		: wxEvent(0, AEGISUB_EVENT_VERSIONCHECK_RESULT)
	{ }

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

	const wxString & GetMainText() const { return main_text; }
	void SetMainText(const wxString &new_text) { main_text = new_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;
	}

};

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


class AegisubVersionCheckEventHandler : public wxEvtHandler {
public:
	void OnUpdateResult(AegisubVersionCheckResultEvent &evt);

	static void EnsureHandlerIsRegistered();
};



AegisubVersionCheckerThread::AegisubVersionCheckerThread(bool interactive)
: wxThread(wxTHREAD_DETACHED)
, interactive(interactive)
{
	AegisubVersionCheckEventHandler::EnsureHandlerIsRegistered();

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

	Create();
	Run();
}


wxThread::ExitCode AegisubVersionCheckerThread::Entry()
{
	if (!interactive)
	{
		// Automatic checking enabled?
		if (!Options.AsBool(_T("auto check for updates")))
			return 0;

		// Is it actually time for a check?
		time_t next_check = Options.AsInt(_T("Updates Next Check Time"));
		if ((time_t)next_check > wxDateTime::GetTimeNow())
			return 0;
	}

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

	try {
		DoCheck();
	}
	catch (const Aegisub::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().c_str()));
	}
	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
	Options.SetInt(_T("Updates Next Check Time"), (int)new_next_check_time);

	return 0;
}


void AegisubVersionCheckerThread::PostErrorEvent(const wxString &error_text)
{
	if (!interactive) return;

	AegisubVersionCheckResultEvent evt;
	evt.SetMainText(error_text);
	wxTheApp->AddPendingEvent(evt);
}


static const wxChar * 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 _T("win2k");
		else if (osver_maj == 5 && osver_min == 1)
			return _T("winxp");
		else if (osver_maj == 5 && osver_min == 2)
			return _T("win2k3"); // this is also xp64
		else if (osver_maj == 6 && osver_min == 0)
			return _T("win60"); // vista and server 2008
		else if (osver_maj == 6 && osver_min == 1)
			return _T("win61"); // 7 and server 2008r2
		else
			return _T("windows"); // future proofing? I doubt we run on nt4
	}
	else if (osid & wxOS_MAC_OSX_DARWIN && osver_maj == 10) // 10 decimal, previously 0x10... sigh
	{
		// ugliest hack in the world? nah.
		static wxChar osxstring[] = _T("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 _T("linux");
	else if (osid & wxOS_UNIX_FREEBSD)
		return _T("freebsd");
	else if (osid & wxOS_UNIX_OPENBSD)
		return _T("openbsd");
	else if (osid & wxOS_UNIX_NETBSD)
		return _T("netbsd");
	else if (osid & wxOS_UNIX_SOLARIS)
		return _T("solaris");
	else if (osid & wxOS_UNIX_AIX)
		return _T("aix");
	else if (osid & wxOS_UNIX_HPUX)
		return _T("hpux");
	else if (osid & wxOS_UNIX)
		return _T("unix");
	else if (osid & wxOS_OS2)
		return _T("os2");
	else if (osid & wxOS_DOS)
		return _T("dos");
	else
		return _T("unknown");
}


#ifdef WIN32
typedef BOOL (WINAPI * PGetUserPreferredUILanguages)(DWORD dwFlags, PULONG pulNumLanguages, wchar_t *pwszLanguagesBuffer, PULONG pcchLanguagesBuffer);
static wxString GetSystemLanguage()
{
	wxString res;

	// Try using Win 6+ functions if available
	HMODULE kernel32 = LoadLibraryW(L"kernel32.dll");
	PGetUserPreferredUILanguages gupuil = (PGetUserPreferredUILanguages)GetProcAddress(kernel32, "GetUserPreferredUILanguages");
	if (gupuil)
	{
		ULONG numlang = 0, output_len = 0;
		if (gupuil(MUI_LANGUAGE_NAME, &numlang, 0, &output_len) != TRUE)
			goto getsyslang_fallback;
		wchar_t *output = new wchar_t[output_len];
		if (gupuil(MUI_LANGUAGE_NAME, &numlang, output, &output_len) && numlang >= 1)
		{
			// We got at least one language, just treat it as the only, and a null-terminated string
			res = wxString(output);
			delete[] output;
		}
		else
		{
			delete[] output;
			goto getsyslang_fallback;
		}
	}
	else
	{
getsyslang_fallback:
		// On an old version of Windows, let's just return the LANGID as a string
		LANGID langid = GetUserDefaultUILanguage();
		res = wxString::Format(_T("x-win%04x"), langid);

	}
	FreeModule(kernel32);

	return res;
}
#else
static wxString GetSystemLanguage()
{
	try {
		return wxLocale::GetLanguageInfo(wxLocale::GetSystemLanguage())->CanonicalName;
	} catch (...) {
		return _T("x-unk");
	}
}
#endif


static void ProcessUpdateFileLine(const std::set<wxString> &accept_tags, AegisubVersionCheckResultEvent &result_event, const wxString &line)
{
	wxStringTokenizer tkn(line, _T("|"), wxTOKEN_RET_EMPTY_ALL);
	wxArrayString parsed;
	while (tkn.HasMoreTokens()) {
		parsed.Add(tkn.GetNextToken());
	}
	if (parsed.Count() != 6) return;

	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]);

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

	// check if the tags match
	if (line_tags_str.IsEmpty() || line_tags_str == _T("all"))
	{
		// looking good
	}
	else
	{
		bool accepts_all_tags = true;
		wxStringTokenizer tk(line_tags_str, _T(" "));
		while (tk.HasMoreTokens())
		{
			if (accept_tags.find(tk.GetNextToken()) == accept_tags.end())
			{
				accepts_all_tags = false;
				break;
			}
		}
		if (!accepts_all_tags)
			return;
	}

	if (line_type == _T("upgrade") || line_type == _T("bugfix"))
	{
		// de facto interesting
	}
	else
	{
		// maybe interesting, check revision
		
		long new_revision = 0;
		if (!line_revision.ToLong(&new_revision)) return;
		if (new_revision <= GetSVNRevision()) 
		{
			// too old, not interesting, skip
			return;
		}
	}

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


void AegisubVersionCheckerThread::DoCheck()
{
	std::set<wxString> accept_tags;
#ifdef UPDATE_CHECKER_ACCEPT_TAGS
	{
		wxStringTokenizer tk(wxString(UPDATE_CHECKER_ACCEPT_TAGS, wxConvUTF8), _T(" "));
		while (tk.HasMoreTokens())
		{
			accept_tags.insert(tk.GetNextToken());
		}
	}
#endif

	const wxString servername = _T("updates.aegisub.org");
	const wxString base_updates_path = _T("/2.1.9");

	wxString querystring = wxString::Format(
		_T("?rev=%d&rel=%d&os=%s&lang=%s"),
		GetSVNRevision(),
		GetIsOfficialRelease()?1:0,
		GetOSShortName(),
		GetSystemLanguage().c_str());

	wxString path = base_updates_path + querystring;

#ifndef __WXMAC__
	wxHTTP http;
	http.SetHeader(_T("Connection"), _T("Close"));
	http.SetFlags(wxSOCKET_WAITALL|wxSOCKET_BLOCK);

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

	std::auto_ptr<wxInputStream> stream(http.GetInputStream(path));
	if (stream.get() == 0) // check for null-pointer
		throw VersionCheckError(_("Could not download from updates server."));

	if (http.GetResponse() < 200 || http.GetResponse() >= 300)
		throw VersionCheckError(wxString::Format(_("HTTP request failed, got HTTP response %d."), http.GetResponse()));

	wxTextInputStream text(*stream);

	AegisubVersionCheckResultEvent result_event;

	while (!stream->Eof() && stream->GetSize() > 0)
	{
		wxString line = text.ReadLine();
		ProcessUpdateFileLine(accept_tags, result_event, line);
	}
#else // is __WXMAC__
	CFReadStreamRef stream;
	{
		wxString url = wxString("http://") + servername + path;
		CFStringRef cfurlstr = CFStringCreateWithCString(kCFAllocatorDefault, url.utf8_str(), kCFStringEncodingUTF8);
		CFURLRef urlobj = CFURLCreateWithString(kCFAllocatorDefault, cfurlstr, 0);
		CFDataRef bodyData = CFStringCreateExternalRepresentation(kCFAllocatorDefault, CFSTR(""), kCFStringEncodingUTF8, 0);
		CFHTTPMessageRef httpmsg = CFHTTPMessageCreateRequest(kCFAllocatorDefault, CFSTR("GET"), urlobj, kCFHTTPVersion1_1);
		CFHTTPMessageSetHeaderFieldValue(httpmsg, CFSTR("User-Agent"), CFSTR("Aegisub"));
		CFHTTPMessageSetHeaderFieldValue(httpmsg, CFSTR("Connection"), CFSTR("Close"));
		CFHTTPMessageSetBody(httpmsg, bodyData);
		stream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, httpmsg);
		CFRelease(httpmsg);
		CFRelease(bodyData);
		CFRelease(urlobj);
	}
	CFReadStreamSetProperty(stream, kCFStreamPropertyHTTPShouldAutoredirect, kCFBooleanTrue);

	CFReadStreamOpen(stream);
	wxString result_body;
	do {
		const CFIndex buffer_len = 512;
		UInt8 buf[buffer_len];
		CFIndex read_len = CFReadStreamRead(stream, buf, buffer_len);
		if (read_len <= 0) break;
		result_body += wxString(buf, wxConvUTF8, read_len);
	} while (true);
	
	CFHTTPMessageRef httpresp = (CFHTTPMessageRef)CFReadStreamCopyProperty(stream, kCFStreamPropertyHTTPResponseHeader);
	
	AegisubVersionCheckResultEvent result_event;
	
	int http_code = 0;
	if (httpresp != 0)
	{
		CFHTTPMessageGetResponseStatusCode(httpresp);
	}
	
	if (http_code >= 200 && http_code < 300)
	{
		wxStringInputStream strstream(result_body);
		wxTextInputStream text(strstream);
		while (!strstream.Eof() && strstream.GetSize() > 0)
		{
			wxString line = text.ReadLine();
			ProcessUpdateFileLine(accept_tags, result_event, line);
		}
	}
	else if (http_code == 0)
	{
		throw VersionCheckError(_("Could not connect to updates server."));
	}
	else
	{
		throw VersionCheckError(wxString::Format(_("HTTP request failed, got HTTP response %d."), http_code));
	}

	CFRelease(stream);
#endif

	if (result_event.GetUpdates().size() == 1)
	{
		result_event.SetMainText(_("An update to Aegisub was found."));
	}
	else if (result_event.GetUpdates().size() > 1)
	{
		result_event.SetMainText(_("Several possible updates to Aegisub were found."));
	}
	else
	{
		result_event.SetMainText(_("There are no updates to Aegisub."));
	}

	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; }

	DECLARE_EVENT_TABLE()
};

BEGIN_EVENT_TABLE(VersionCheckerResultDialog, wxDialog)
	EVT_BUTTON(wxID_OK, VersionCheckerResultDialog::OnCloseButton)
	EVT_BUTTON(wxID_NO, VersionCheckerResultDialog::OnRemindMeLater)
	EVT_CLOSE(VersionCheckerResultDialog::OnClose)
END_EVENT_TABLE()


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

	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(Options.AsBool(_T("Auto check for updates")));

	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);

	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();
	close_button->SetFocus();
}


void VersionCheckerResultDialog::OnCloseButton(wxCommandEvent &evt)
{
	Close();
}


void VersionCheckerResultDialog::OnRemindMeLater(wxCommandEvent &evt)
{
	// In one week
	time_t new_next_check_time = wxDateTime::Today().GetTicks() + 7*24*60*60;
	Options.SetInt(_T("Updates Next Check Time"), (int)new_next_check_time);

	Close();
}


void VersionCheckerResultDialog::OnClose(wxCloseEvent &evt)
{
	Options.SetBool(_T("Auto check for updates"), automatic_check_checkbox->GetValue());
	Destroy();
}



void AegisubVersionCheckEventHandler::OnUpdateResult(AegisubVersionCheckResultEvent &evt)
{
	VersionCheckerResultDialog *dlg = new VersionCheckerResultDialog(evt.GetMainText(), evt.GetUpdates());
	dlg->Show();
}

void AegisubVersionCheckEventHandler::EnsureHandlerIsRegistered()
{
	static bool is_registered = false;
	static AegisubVersionCheckEventHandler evt_handler_object;

	if (is_registered) return;

	wxTheApp->Connect(
		-1, -1,
		AEGISUB_EVENT_VERSIONCHECK_RESULT,
		(wxObjectEventFunction)&AegisubVersionCheckEventHandler::OnUpdateResult,
		0,
		&evt_handler_object);

	is_registered = true;
}

