// Copyright (c) 2014- PPSSPP Project.

// 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, version 2.0 or later versions.

// 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 2.0 for more details.

// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/

// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.

#include <functional>

#include "Common/UI/Screen.h"
#include "Common/UI/Context.h"
#include "Common/UI/ViewGroup.h"
#include "Common/UI/IconCache.h"
#include "Common/UI/ScrollView.h"
#include "Common/Render/DrawBuffer.h"

#include "Common/Log.h"
#include "Common/Data/Text/I18n.h"
#include "Common/Data/Format/JSONReader.h"
#include "Common/StringUtils.h"
#include "Common/Render/ManagedTexture.h"
#include "Common/Net/NetBuffer.h"
#include "Core/Config.h"
#include "Core/System.h"
#include "Core/Util/GameManager.h"
#include "UI/EmuScreen.h"
#include "UI/Store.h"

const char *storeBaseUrlHttp = "http://store.ppsspp.org/";
const char *storeBaseUrlHttps = "https://store.ppsspp.org/";

static std::string StoreBaseUrl() {
	return System_GetPropertyBool(SYSPROP_SUPPORTS_HTTPS) ? storeBaseUrlHttps : storeBaseUrlHttp;
}

// baseUrl is assumed to have a trailing slash, and not contain any subdirectories.
std::string ResolveUrl(const std::string &baseUrl, const std::string &url) {
	if (url.empty()) {
		return baseUrl;
	} else if (url[0] == '/') {
		return baseUrl + url.substr(1);
	} else if (startsWith(url, "http://") || startsWith(url, "https://")) {
		return url;
	} else {
		// Huh.
		return baseUrl + url;
	}
}

class HttpImageFileView : public UI::View {
public:
	HttpImageFileView(http::RequestManager *requestManager, const std::string &path, UI::ImageSizeMode sizeMode = UI::IS_DEFAULT, bool useIconCache = true, UI::LayoutParams *layoutParams = nullptr)
		: UI::View(layoutParams), path_(path), sizeMode_(sizeMode), requestManager_(requestManager), useIconCache_(useIconCache) {

		if (useIconCache && g_iconCache.MarkPending(path_)) {
			const char *acceptMime = "image/png, image/jpeg, image/*; q=0.9, */*; q=0.8";
			requestManager_->StartDownload(path_, Path(), http::RequestFlags::ProgressBar | http::RequestFlags::ProgressBarDelayed, acceptMime, "", [](http::Request &download) {
				// Can't touch 'this' in this function! Don't use captures!
				std::string path = download.url();
				if (download.ResultCode() == 200) {
					std::string data;
					download.buffer().TakeAll(&data);
					if (!data.empty()) {
						g_iconCache.InsertIcon(path, IconFormat::PNG, std::move(data));
					} else {
						g_iconCache.CancelPending(path);
					}
				} else {
					g_iconCache.CancelPending(path);
				}
			});
		}
	}

	~HttpImageFileView() {
		if (download_) {
			download_->Cancel();
		}
	}

	void GetContentDimensions(const UIContext &dc, float &w, float &h) const override;
	void Draw(UIContext &dc) override;
	std::string DescribeText() const override { return ""; }

	void SetFilename(const std::string &filename);
	void SetColor(uint32_t color) { color_ = color; }
	void SetFixedSize(float fixW, float fixH) { fixedSizeW_ = fixW; fixedSizeH_ = fixH; }
	void SetCanBeFocused(bool can) { canFocus_ = can; }

	bool CanBeFocused() const override { return false; }

	const std::string &GetFilename() const { return path_; }

private:
	bool canFocus_ = false;
	bool useIconCache_ = false;
	std::string path_;  // or cache key
	uint32_t color_ = 0xFFFFFFFF;
	UI::ImageSizeMode sizeMode_;
	http::RequestManager *requestManager_;
	std::shared_ptr<http::Request> download_;

	std::string textureData_;
	Draw::AutoRef<Draw::Texture> texture_;
	bool textureFailed_ = false;
	float fixedSizeW_ = 0.0f;
	float fixedSizeH_ = 0.0f;
};

void HttpImageFileView::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
	switch (sizeMode_) {
	case UI::IS_FIXED:
		w = fixedSizeW_;
		h = fixedSizeH_;
		break;
	case UI::IS_DEFAULT:
	default:
		if (useIconCache_) {
			int width, height;
			if (g_iconCache.GetDimensions(path_, &width, &height)) {
				w = width;
				h = height;
			} else {
				w = 16;
				h = 16;
			}
		} else {
			if (texture_) {
				float texw = (float)texture_->Width();
				float texh = (float)texture_->Height();
				w = texw;
				h = texh;
			} else {
				w = 16;
				h = 16;
			}
		}
		break;
	}
}

void HttpImageFileView::SetFilename(const std::string &filename) {
	if (!useIconCache_ && path_ != filename) {
		textureFailed_ = false;
		path_ = filename;
		if (texture_) {
			texture_.reset(nullptr);
		}
	}
}

void HttpImageFileView::Draw(UIContext &dc) {
	using namespace Draw;

	if (!useIconCache_) {
		if (!texture_ && !textureFailed_ && !path_.empty() && !download_) {
			const char *acceptMime = "image/png, image/jpeg, image/*; q=0.9, */*; q=0.8";
			requestManager_->StartDownload(path_, Path(), http::RequestFlags::Default, acceptMime, "", [this](http::Request &download) {
				if (download.IsCancelled()) {
					// We were probably destroyed. Can't touch "this" (heh).
					return;
				}
				if (download.ResultCode() == 200) {
					download.buffer().TakeAll(&textureData_);
				} else {
					textureFailed_ = true;
				}
			});
		}

		if (!textureData_.empty()) {
			texture_ = CreateTextureFromFileData(dc.GetDrawContext(), (const uint8_t *)(textureData_.data()), textureData_.size(), ImageFileType::DETECT, false, "store_icon");
			if (!texture_)
				textureFailed_ = true;
			textureData_.clear();
			download_.reset();
		}
	}

	if (HasFocus()) {
		dc.FillRect(dc.GetTheme().itemFocusedStyle.background, bounds_.Expand(3));
	}

	// TODO: involve sizemode
	Draw::Texture *texture = nullptr;
	if (useIconCache_) {
		texture = g_iconCache.BindIconTexture(&dc, path_);
	} else {
		texture = texture_;
	}

	if (texture) {
		float tw = texture->Width();
		float th = texture->Height();

		float x = bounds_.x;
		float y = bounds_.y;
		float w = bounds_.w;
		float h = bounds_.h;

		if (tw / th < w / h) {
			float nw = h * tw / th;
			x += (w - nw) / 2.0f;
			w = nw;
		} else {
			float nh = w * th / tw;
			y += (h - nh) / 2.0f;
			h = nh;
		}

		dc.Flush();
		dc.GetDrawContext()->BindTexture(0, texture);
		dc.Draw()->Rect(x, y, w, h, color_);
		dc.Flush();
		dc.RebindTexture();
	} else {
		// draw a black rectangle to represent the missing image.
		dc.FillRect(UI::Drawable(0x7F000000), GetBounds());
	}
}

// This is the entry in a list. Does not have install buttons and so on.
class ProductItemView : public UI::StickyChoice {
public:
	ProductItemView(const StoreEntry &entry, UI::LayoutParams *layoutParams = 0)
		: UI::StickyChoice(entry.name, "", layoutParams), entry_(entry) {}

	void GetContentDimensions(const UIContext &dc, float &w, float &h) const override {
		w = 300;
		h = 164;
	}

	StoreEntry GetEntry() const { return entry_; }

private:
	const StoreEntry entry_;
};

// This is a "details" view of a game. Lets you install it.
class ProductView : public UI::LinearLayout {
public:
	ProductView(const StoreEntry &entry)
		: LinearLayout(ORIENT_VERTICAL), entry_(entry) {
		CreateViews();
	}

	void Update() override;

	UI::Event OnClickLaunch;

private:
	void CreateViews();
	void OnInstall(UI::EventParams &e);
	void OnCancel(UI::EventParams &e);
	void OnLaunchClick(UI::EventParams &e);

	bool IsGameInstalled() const {
		return g_GameManager.IsGameInstalled(entry_.file);
	}
	std::string DownloadURL() const;

	StoreEntry entry_;
	UI::Button *uninstallButton_ = nullptr;
	UI::Button *installButton_ = nullptr;
	UI::Button *launchButton_ = nullptr;
	UI::Button *cancelButton_ = nullptr;
	bool wasInstalled_ = false;
};

void ProductView::CreateViews() {
	using namespace UI;
	Clear();

	if (!entry_.iconURL.empty()) {
		Add(new HttpImageFileView(&g_DownloadManager, ResolveUrl(StoreBaseUrl(), entry_.iconURL), IS_FIXED))->SetFixedSize(144, 88);
	}
	Add(new TextView(entry_.name))->SetBig(true);
	Add(new TextView(entry_.author));

	auto st = GetI18NCategory(I18NCat::STORE);
	auto di = GetI18NCategory(I18NCat::DIALOG);
	wasInstalled_ = IsGameInstalled();
	bool isDownloading = g_GameManager.IsDownloading(DownloadURL());
	if (!wasInstalled_) {
		launchButton_ = nullptr;
		LinearLayout *progressDisplay = new LinearLayout(ORIENT_HORIZONTAL);
		installButton_ = progressDisplay->Add(new Button(st->T("Install")));
		installButton_->OnClick.Handle(this, &ProductView::OnInstall);
		uninstallButton_ = nullptr;

		Add(progressDisplay);
	} else {
		installButton_ = nullptr;
		launchButton_ = new Button(st->T("Launch Game"));
		launchButton_->OnClick.Handle(this, &ProductView::OnLaunchClick);
		Add(launchButton_);
		uninstallButton_ = new Button(st->T("Uninstall"));
		Add(uninstallButton_)->OnClick.Add([=](UI::EventParams &e) {
			g_GameManager.UninstallGameOnThread(entry_.file);
		});
		// Add(new TextView(st->T("Installed")));  // Not really needed
	}

	cancelButton_ = Add(new Button(di->T("Cancel")));
	cancelButton_->OnClick.Handle(this, &ProductView::OnCancel);
	cancelButton_->SetVisibility(isDownloading ? V_VISIBLE : V_GONE);

	// Add star rating, comments etc?

	// Draw each line separately so focusing can scroll.
	std::vector<std::string> lines;
	SplitString(entry_.description, '\n', lines);
	for (auto &line : lines) {
		Add(new TextView(line, ALIGN_LEFT | FLAG_WRAP_TEXT, false))->SetFocusable(true);
	}

	float size = entry_.size / (1024.f * 1024.f);
	Add(new TextView(StringFromFormat("%s: %.2f %s", st->T_cstr("Size"), size, st->T_cstr("MB"))));

	if (!entry_.license.empty()) {
		LinearLayout *horiz = Add(new LinearLayout(ORIENT_HORIZONTAL));
		horiz->Add(new TextView(StringFromFormat("%s: %s", st->T_cstr("License"), entry_.license.c_str()), new LinearLayoutParams(0.0, Gravity::G_VCENTER)));
		horiz->Add(new Button(di->T("More info"), new LinearLayoutParams(0.0, Gravity::G_VCENTER)))->OnClick.Add([this](UI::EventParams) {
			std::string url = StringFromFormat("https://www.ppsspp.org/docs/reference/homebrew-store-distribution/#%s", entry_.file.c_str());
			System_LaunchUrl(LaunchUrlType::BROWSER_URL, url.c_str());
		});
	}
	if (!entry_.websiteURL.empty()) {
		// Display in a few different ways depending on the URL
		size_t slashes = std::count(entry_.websiteURL.begin(), entry_.websiteURL.end(), '/');
		std::string buttonText;
		if (slashes == 2) {
			// Just strip https and show the URL.
			std::string_view name = StripPrefix("https://", entry_.websiteURL);
			name = StripPrefix("http://", name);
			if (name.size() < entry_.websiteURL.size()) {
				buttonText = name;
			}
		}
		if (buttonText.empty()) {
			// Fall back
			buttonText = st->T("Website");
		}
		Add(new Button(buttonText))->OnClick.Add([this](UI::EventParams) {
			System_LaunchUrl(LaunchUrlType::BROWSER_URL, entry_.websiteURL.c_str());
		});
	}
}

void ProductView::Update() {
	if (wasInstalled_ != IsGameInstalled()) {
		CreateViews();
	}
	if (installButton_) {
		installButton_->SetEnabled(g_GameManager.GetState() == GameManagerState::IDLE);
	}
	if (uninstallButton_) {
		uninstallButton_->SetEnabled(g_GameManager.GetState() == GameManagerState::IDLE);
	}
	if (g_GameManager.GetState() != GameManagerState::DOWNLOADING) {
		if (cancelButton_)
			cancelButton_->SetVisibility(UI::V_GONE);
	}
	if (launchButton_)
		launchButton_->SetEnabled(g_GameManager.GetState() == GameManagerState::IDLE);
	ViewGroup::Update();
}

std::string ProductView::DownloadURL() const {
	if (entry_.downloadURL.empty()) {
		// Construct the URL.
		return StoreBaseUrl() + "files/" + entry_.file + ".zip";
	} else {
		// Use the provided URL, for external hosting.
		return entry_.downloadURL;
	}
}

void ProductView::OnInstall(UI::EventParams &e) {
	std::string fileUrl = DownloadURL();
	if (installButton_) {
		installButton_->SetEnabled(false);
	}
	if (cancelButton_) {
		cancelButton_->SetVisibility(UI::V_VISIBLE);
	}
	INFO_LOG(Log::System, "Triggering install of '%s'", fileUrl.c_str());
	g_GameManager.DownloadAndInstall(fileUrl);
}

void ProductView::OnCancel(UI::EventParams &e) {
	g_GameManager.CancelDownload();
}

void ProductView::OnLaunchClick(UI::EventParams &e) {
	if (g_GameManager.GetState() != GameManagerState::IDLE) {
		// Button should have been disabled. Just a safety check.
		return;
	}

	Path pspGame = GetSysDirectory(DIRECTORY_GAME);
	Path path = pspGame / entry_.file;
	UI::EventParams e2{};
	e2.v = e.v;
	e2.s = path.ToString();
	// Insta-update - here we know we are already on the right thread.
	OnClickLaunch.Trigger(e2);
}

StoreScreen::StoreScreen() : UISimpleBaseDialogScreen(Path(), SimpleDialogFlags::Default) {
	lang_ = g_Config.sLanguageIni;
	loading_ = true;

	std::string indexPath = StoreBaseUrl() + "index.json";
	const char *acceptMime = "application/json, */*; q=0.8";
	listing_ = g_DownloadManager.StartDownload(indexPath, Path(), http::RequestFlags::ProgressBar | http::RequestFlags::ProgressBarDelayed, acceptMime);
}

StoreScreen::~StoreScreen() {
	g_DownloadManager.CancelAll();
}

// Handle async download tasks
void StoreScreen::update() {
	UIBaseDialogScreen::update();

	g_DownloadManager.Update();

	if (listing_.get() && listing_->Done()) {
		resultCode_ = listing_->ResultCode();
		if (listing_->ResultCode() == 200) {
			std::string listingJson;
			listing_->buffer().TakeAll(&listingJson);
			// printf("%s\n", listingJson.c_str());
			loading_ = false;
			connectionError_ = false;

			ParseListing(listingJson);
			RecreateViews();
		} else {
			// Failed to contact store. Don't do anything.
			ERROR_LOG(Log::IO, "Download failed : error code %d", resultCode_);
			connectionError_ = true;
			loading_ = false;
			RecreateViews();
		}

		// Forget the listing.
		listing_.reset();
	}
}

void StoreScreen::ParseListing(const std::string &json) {
	using namespace json;
	JsonReader reader(json.c_str(), json.size());
	if (!reader.ok() || !reader.root()) {
		ERROR_LOG(Log::IO, "Error parsing JSON from store");
		connectionError_ = true;
		RecreateViews();
		return;
	}
	const JsonGet root = reader.root();
	const JsonNode *entries = root.getArray("entries");
	if (entries) {
		entries_.clear();
		for (const JsonNode *pgame : entries->value) {
			JsonGet game = pgame->value;
			StoreEntry e{};
			e.type = ENTRY_PBPZIP;
			e.name = GetTranslatedString(game, "name");
			e.description = GetTranslatedString(game, "description", "");
			e.author = ReplaceAll(game.getStringOr("author", "?"), "&&", "&");  // Can't remove && in the JSON source data due to old app versions, so we do the opposite replacement here.
			e.size = game.getInt("size");
			e.downloadURL = game.getStringOr("download-url", "");
			e.iconURL = game.getStringOr("icon-url", "");
			e.contentRating = game.getInt("content-rating", 0);
			e.websiteURL = game.getStringOr("website-url", "");
			e.license = game.getStringOr("license", "");
#if PPSSPP_PLATFORM(IOS_APP_STORE)
			if (e.contentRating >= 100) {
				continue;
			}
#endif
			e.hidden = false;  // NOTE: Handling of the "hidden" flag is broken in old versions of PPSSPP. Do not use.
			const char *file = game.getStringOr("file", nullptr);
			if (!file)
				continue;
			e.file = file;
			entries_.push_back(e);
		}
	}
}

std::string_view StoreScreen::GetTitle() const {
	auto mm = GetI18NCategory(I18NCat::MAINMENU);
	return mm->T("Homebrew store");
}

void StoreScreen::CreateDialogViews(UI::ViewGroup *parent) {
	using namespace UI;

	auto di = GetI18NCategory(I18NCat::DIALOG);

	LinearLayout *content;
	if (connectionError_ || loading_) {
		auto st = GetI18NCategory(I18NCat::STORE);
		content = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f));
		content->Add(new TextView(loading_ ? std::string(st->T("Loading...")) : StringFromFormat("%s: %d", st->T_cstr("Connection Error"), resultCode_)));
		if (!loading_) {
			content->Add(new Button(di->T("Retry")))->OnClick.Handle(this, &StoreScreen::OnRetry);
		}

		scrollItemView_ = nullptr;
		productPanel_ = nullptr;
	} else {
		content = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f));
		ScrollView *leftScroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(WRAP_CONTENT, FILL_PARENT, 0.4f));
		leftScroll->SetTag("StoreMainList");
		content->Add(leftScroll);
		scrollItemView_ = new LinearLayoutList(ORIENT_VERTICAL, new LayoutParams(FILL_PARENT, WRAP_CONTENT));
		leftScroll->Add(scrollItemView_);

		for (size_t i = 0; i < entries_.size(); i++) {
			scrollItemView_->Add(new ProductItemView(entries_[i]))->OnClick.Handle(this, &StoreScreen::OnGameSelected);
		}

		// TODO: Similar apps, etc etc
		productPanel_ = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(0.5f));
		leftScroll->SetTag("StoreMainProduct");
		content->Add(productPanel_);

		ProductItemView *selectedItem = GetSelectedItem();
		if (selectedItem) {
			ProductView *productView = new ProductView(selectedItem->GetEntry());
			productView->OnClickLaunch.Handle(this, &StoreScreen::OnGameLaunch);
			productPanel_->Add(productView);

			selectedItem->Press();
		} else {
			lastSelectedName_.clear();
		}
	}
	parent->Add(content);
}

ProductItemView *StoreScreen::GetSelectedItem() {
	for (int i = 0; i < scrollItemView_->GetNumSubviews(); ++i) {
		ProductItemView *item = static_cast<ProductItemView *>(scrollItemView_->GetViewByIndex(i));
		if (!item) {
			continue;
		}
		if (item->GetEntry().name == lastSelectedName_)
			return item;
	}

	return nullptr;
}

void StoreScreen::OnGameSelected(UI::EventParams &e) {
	ProductItemView *item = static_cast<ProductItemView *>(e.v);
	if (!item)
		return;

	productPanel_->Clear();
	ProductView *productView = new ProductView(item->GetEntry());
	productView->OnClickLaunch.Handle(this, &StoreScreen::OnGameLaunch);
	productPanel_->Add(productView);

	ProductItemView *previousItem = GetSelectedItem();
	if (previousItem && previousItem != item)
		previousItem->Release();
	lastSelectedName_ = item->GetEntry().name;
}

void StoreScreen::OnGameLaunch(UI::EventParams &e) {
	std::string path = e.s;
	screenManager()->switchScreen(new EmuScreen(Path(path)));
}

void StoreScreen::OnRetry(UI::EventParams &e) {
	RecreateViews();
}

std::string StoreScreen::GetTranslatedString(const json::JsonGet json, const std::string &key, const char *fallback) const {
	json::JsonGet dict = json.getDict("en_US");
	if (dict && json.hasChild(lang_.c_str(), JSON_OBJECT)) {
		if (json.getDict(lang_.c_str()).hasChild(key.c_str(), JSON_STRING)) {
			dict = json.getDict(lang_.c_str());
		}
	}
	const char *str = nullptr;
	if (dict) {
		str = dict.getStringOr(key.c_str(), nullptr);
	}
	if (str) {
		return std::string(str);
	} else {
		return fallback ? fallback : "(error)";
	}
}
