#include "stdafx.h"
#include "game_list_table.h"
#include "game_list_delegate.h"
#include "game_list_frame.h"
#include "gui_settings.h"
#include "localized.h"
#include "custom_table_widget_item.h"
#include "persistent_settings.h"
#include "qt_utils.h"

#include "Emu/vfs_config.h"
#include "Utilities/StrUtil.h"

#include "Loader/ISO.h"

#include <QHeaderView>
#include <QScrollBar>
#include <QStringBuilder>

game_list_table::game_list_table(game_list_frame* frame, std::shared_ptr<persistent_settings> persistent_settings)
	: game_list(), m_game_list_frame(frame), m_persistent_settings(std::move(persistent_settings))
{
	m_is_list_layout = true;

	setShowGrid(false);
	setItemDelegate(new game_list_delegate(this));
	setEditTriggers(QAbstractItemView::NoEditTriggers);
	setSelectionBehavior(QAbstractItemView::SelectRows);
	setSelectionMode(QAbstractItemView::ExtendedSelection);
	setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
	setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
	verticalScrollBar()->setSingleStep(20);
	horizontalScrollBar()->setSingleStep(20);
	verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
	verticalHeader()->setVisible(false);
	horizontalHeader()->setHighlightSections(false);
	horizontalHeader()->setSortIndicatorShown(true);
	horizontalHeader()->setStretchLastSection(true);
	horizontalHeader()->setDefaultSectionSize(150);
	horizontalHeader()->setDefaultAlignment(Qt::AlignLeft);
	horizontalHeader()->setSectionsMovable(true);
	setContextMenuPolicy(Qt::CustomContextMenu);
	setAlternatingRowColors(true);
	setColumnCount(static_cast<int>(gui::game_list_columns::count));
	setMouseTracking(true);

	connect(this, &game_list_table::size_on_disk_ready, this, [this](const game_info& game, movie_item_base* item)
	{
		if (!game || !game->item || game->item != item) return;
		if (QTableWidgetItem* size_item = this->item(static_cast<movie_item*>(game->item)->row(), static_cast<int>(gui::game_list_columns::dir_size)))
		{
			const u64& game_size = game->info.size_on_disk;
			size_item->setText(game_size != umax ? gui::utils::format_byte_size(game_size) : tr("Unknown"));
			size_item->setData(Qt::UserRole, QVariant::fromValue<qulonglong>(game_size));
		}
	});

	connect(this, &game_list::IconReady, this, [](const game_info& game, const movie_item_base* item)
	{
		if (game && item && game->item == item) item->image_change_callback();
	});
}

void game_list_table::restore_layout(const QByteArray& state)
{
	// Resize to fit and get the ideal icon column width
	resize_columns_to_contents();
	const int icon_column_width = columnWidth(static_cast<int>(gui::game_list_columns::icon));

	// Restore header layout from last session
	if (!horizontalHeader()->restoreState(state) && rowCount())
	{
		// Nothing to do
	}

	// Re-apply after restoreState() since it resets setSectionsMovable to false
	horizontalHeader()->setSectionsMovable(true);
	// Make sure no columns are squished
	fix_narrow_columns();

	// Make sure that the icon column is large enough for the actual items.
	// This is important if the list appeared as empty when closing the software before.
	horizontalHeader()->resizeSection(static_cast<int>(gui::game_list_columns::icon), icon_column_width);

	// Save new header state
	horizontalHeader()->restoreState(horizontalHeader()->saveState());
}

void game_list_table::resize_columns_to_contents(int spacing)
{
	horizontalHeader()->resizeSections(QHeaderView::ResizeMode::ResizeToContents);

	// Make non-icon columns slightly bigger for better visuals
	for (int i = 1; i < columnCount(); i++)
	{
		if (isColumnHidden(i))
		{
			continue;
		}

		const int size = horizontalHeader()->sectionSize(i) + spacing;
		horizontalHeader()->resizeSection(i, size);
	}
}

void game_list_table::adjust_icon_column()
{
	// Fixate vertical header and row height
	verticalHeader()->setDefaultSectionSize(m_icon_size.height());
	verticalHeader()->setMinimumSectionSize(m_icon_size.height());
	verticalHeader()->setMaximumSectionSize(m_icon_size.height());

	// Resize the icon column
	resizeColumnToContents(static_cast<int>(gui::game_list_columns::icon));

	// Shorten the last section to remove horizontal scrollbar if possible
	resizeColumnToContents(static_cast<int>(gui::game_list_columns::count) - 1);
}

void game_list_table::sort(usz game_count, int sort_column, Qt::SortOrder col_sort_order)
{
	// Back-up old header sizes to handle unwanted column resize in case of zero search results
	const int old_row_count = rowCount();
	const usz old_game_count = game_count;

	std::vector<int> column_widths(columnCount());
	for (int i = 0; i < columnCount(); i++)
	{
		column_widths[i] = columnWidth(i);
	}

	// Sorting resizes hidden columns, so unhide them as a workaround
	std::vector<int> columns_to_hide;

	for (int i = 0; i < columnCount(); i++)
	{
		if (isColumnHidden(i))
		{
			setColumnHidden(i, false);
			columns_to_hide.push_back(i);
		}
	}

	// Sort the list by column and sort order
	sortByColumn(sort_column, col_sort_order);

	// Hide columns again
	for (int col : columns_to_hide)
	{
		setColumnHidden(col, true);
	}

	// Don't resize the columns if no game is shown to preserve the header settings
	if (!rowCount())
	{
		for (int i = 0; i < columnCount(); i++)
		{
			setColumnWidth(i, column_widths[i]);
		}

		horizontalHeader()->setSectionResizeMode(static_cast<int>(gui::game_list_columns::icon), QHeaderView::Fixed);
		return;
	}

	// Fixate vertical header and row height
	verticalHeader()->setDefaultSectionSize(m_icon_size.height());
	verticalHeader()->setMinimumSectionSize(m_icon_size.height());
	verticalHeader()->setMaximumSectionSize(m_icon_size.height());

	// Resize columns if the game list was empty before
	if (!old_row_count && !old_game_count)
	{
		resize_columns_to_contents();
	}
	else
	{
		resizeColumnToContents(static_cast<int>(gui::game_list_columns::icon));
	}

	// Fixate icon column
	horizontalHeader()->setSectionResizeMode(static_cast<int>(gui::game_list_columns::icon), QHeaderView::Fixed);

	// Shorten the last section to remove horizontal scrollbar if possible
	resizeColumnToContents(static_cast<int>(gui::game_list_columns::count) - 1);
}

void game_list_table::set_custom_config_icon(const game_info& game)
{
	if (!game)
	{
		return;
	}

	const QString serial = QString::fromStdString(game->info.serial);

	for (int row = 0; row < rowCount(); ++row)
	{
		if (QTableWidgetItem* title_item = item(row, static_cast<int>(gui::game_list_columns::name)))
		{
			if (const QTableWidgetItem* serial_item = item(row, static_cast<int>(gui::game_list_columns::serial)); serial_item && serial_item->text() == serial)
			{
				title_item->setIcon(game_list_base::GetCustomConfigIcon(game));
			}
		}
	}
}

void game_list_table::populate(
	const std::vector<game_info>& game_data,
	const std::map<QString, QString>& notes_map,
	const std::map<QString, QString>& title_map,
	const std::set<std::string>& selected_item_ids,
	bool play_hover_movies,
	bool play_hover_music)
{
	clear_list();

	setRowCount(::narrow<int>(game_data.size()));

	// Default locale. Uses current Qt application language.
	const QLocale locale{};
	const Localized localized;

	const std::string dev_flash = g_cfg_vfs.get_dev_flash();

	int row = 0;
	int index = -1;
	std::set<int> selected_rows;

	const auto get_title = [&title_map](const QString& serial, const std::string& name) -> QString
	{
		if (const auto it = title_map.find(serial); it != title_map.cend())
		{
			return it->second;
		}

		return QString::fromStdString(name);
	};

	for (const auto& game : game_data)
	{
		index++;

		const QString serial = QString::fromStdString(game->info.serial);
		const QString title = get_title(serial, game->info.name);

		// Icon
		custom_table_widget_item* icon_item = new custom_table_widget_item;
		game->item = icon_item;

		icon_item->set_image_change_callback([this, icon_item, game](const QVideoFrame& frame)
		{
			if (!icon_item || !game)
			{
				return;
			}

			if (const QPixmap pixmap = icon_item->get_movie_image(frame); icon_item->get_active() && !pixmap.isNull())
			{
				icon_item->setData(Qt::DecorationRole, pixmap.scaled(m_icon_size, Qt::KeepAspectRatio));
				return;
			}

			std::lock_guard lock(icon_item->pixmap_mutex);

			if (!game->pxmap.isNull())
			{
				icon_item->setData(Qt::DecorationRole, game->pxmap);

				if (!game->has_hover_gif && !game->has_hover_pam)
				{
					game->pxmap = {};
				}
			}
		});

		icon_item->set_size_calc_func([this, game, cancel = icon_item->size_on_disk_loading_aborted(), dev_flash]()
		{
			if (game && game->info.size_on_disk == umax && (!cancel || !cancel->load()))
			{
				if (game->info.path.starts_with(dev_flash))
				{
					// Do not report size of apps inside /dev_flash (it does not make sense to do so)
					game->info.size_on_disk = 0;
				}
				else if (is_file_iso(game->info.path))
				{
					fs::stat_t iso_stat;
					fs::get_stat(game->info.path, iso_stat);

					game->info.size_on_disk = iso_stat.size;
				}
				else
				{
					game->info.size_on_disk = fs::get_dir_size(game->info.path, 1, cancel.get());
				}

				if (!cancel || !cancel->load())
				{
					Q_EMIT size_on_disk_ready(game, game->item);
					return;
				}
			}
		});

		bool check_iso = false;

		if (play_hover_movies && (game->has_hover_gif || game->has_hover_pam))
		{
			icon_item->set_video_path(game->info.movie_path);
			check_iso |= !fs::exists(game->info.movie_path);
		}

		if (play_hover_music && game->has_audio_file)
		{
			icon_item->set_audio_path(game->info.audio_path);
			check_iso |= !fs::exists(game->info.audio_path);
		}

		if (check_iso && is_file_iso(game->info.path))
		{
			icon_item->set_iso_path(game->info.path);
		}

		icon_item->setData(Qt::UserRole, index, true);
		icon_item->setData(gui::custom_roles::game_role, QVariant::fromValue(game));

		// Title
		custom_table_widget_item* title_item = new custom_table_widget_item(title);
		title_item->setIcon(game_list_base::GetCustomConfigIcon(game));

		// Serial
		custom_table_widget_item* serial_item = new custom_table_widget_item(game->info.serial);

		if (const auto it = notes_map.find(serial); it != notes_map.cend() && !it->second.isEmpty())
		{
			const QString tool_tip = tr("%0 [%1]\n\nNotes:\n%2").arg(title).arg(serial).arg(it->second);
			title_item->setToolTip(tool_tip);
			serial_item->setToolTip(tool_tip);
		}

		// Move Support (http://www.psdevwiki.com/ps3/PARAM.SFO#ATTRIBUTE)
		const bool supports_move = game->info.attr & 0x800000;

		// Compatibility
		custom_table_widget_item* compat_item = new custom_table_widget_item;
		compat_item->setText(game->compat.text % (game->compat.date.isEmpty() ? QStringLiteral("") : " (" % game->compat.date % ")"));
		compat_item->setData(Qt::UserRole, game->compat.index, true);
		compat_item->setToolTip(game->compat.tooltip);
		if (!game->compat.color.isEmpty())
		{
			compat_item->setData(Qt::DecorationRole, gui::utils::circle_pixmap(game->compat.color, devicePixelRatioF() * 2));
		}

		// Version
		QString app_version = QString::fromStdString(game->GetGameVersion());

		if (game->info.bootable && !game->compat.latest_version.isEmpty())
		{
			f64 top_ver = 0.0, app_ver = 0.0;
			const bool unknown = app_version == localized.category.unknown;
			const bool ok_app = !unknown && try_to_float(&app_ver, app_version.toStdString(), ::std::numeric_limits<s32>::min(), ::std::numeric_limits<s32>::max());
			const bool ok_top = !unknown && try_to_float(&top_ver, game->compat.latest_version.toStdString(), ::std::numeric_limits<s32>::min(), ::std::numeric_limits<s32>::max());

			// If the app is bootable and the compat database contains info about the latest patch version:
			// add a hint for available software updates if the app version is unknown or lower than the latest version.
			if (unknown || (ok_top && ok_app && top_ver > app_ver))
			{
				app_version = tr("%0 (Update available: %1)").arg(app_version, game->compat.latest_version);
			}
		}

		// Playtimes
		const quint64 elapsed_ms = m_persistent_settings->GetPlaytime(serial);

		// Last played (support outdated values)
		QDateTime last_played;
		const QString last_played_str = m_persistent_settings->GetLastPlayed(serial);

		if (!last_played_str.isEmpty())
		{
			last_played = QDateTime::fromString(last_played_str, gui::persistent::last_played_date_format);

			if (!last_played.isValid())
			{
				last_played = QDateTime::fromString(last_played_str, gui::persistent::last_played_date_format_old);
			}
		}

		const u64 game_size = game->info.size_on_disk;

		setItem(row, static_cast<int>(gui::game_list_columns::icon),       icon_item);
		setItem(row, static_cast<int>(gui::game_list_columns::name),       title_item);
		setItem(row, static_cast<int>(gui::game_list_columns::serial),     serial_item);
		setItem(row, static_cast<int>(gui::game_list_columns::firmware),   new custom_table_widget_item(game->info.fw));
		setItem(row, static_cast<int>(gui::game_list_columns::version),    new custom_table_widget_item(app_version));
		setItem(row, static_cast<int>(gui::game_list_columns::category),   new custom_table_widget_item(game->localized_category));
		setItem(row, static_cast<int>(gui::game_list_columns::path),       new custom_table_widget_item(game->info.path));
		setItem(row, static_cast<int>(gui::game_list_columns::move),       new custom_table_widget_item((supports_move ? tr("Supported") : tr("Not Supported")).toStdString(), Qt::UserRole, !supports_move));
		setItem(row, static_cast<int>(gui::game_list_columns::resolution), new custom_table_widget_item(Localized::GetStringFromU32(game->info.resolution, localized.resolution.mode, true)));
		setItem(row, static_cast<int>(gui::game_list_columns::sound),      new custom_table_widget_item(Localized::GetStringFromU32(game->info.sound_format, localized.sound.format, true)));
		setItem(row, static_cast<int>(gui::game_list_columns::parental),   new custom_table_widget_item(Localized::GetStringFromU32(game->info.parental_lvl, localized.parental.level), Qt::UserRole, game->info.parental_lvl));
		setItem(row, static_cast<int>(gui::game_list_columns::last_play),  new custom_table_widget_item(locale.toString(last_played, last_played >= QDateTime::currentDateTime().addDays(-7) ? gui::persistent::last_played_date_with_time_of_day_format : gui::persistent::last_played_date_format_new), Qt::UserRole, last_played));
		setItem(row, static_cast<int>(gui::game_list_columns::playtime),   new custom_table_widget_item(elapsed_ms == 0 ? tr("Never played") : localized.GetVerboseTimeByMs(elapsed_ms), Qt::UserRole, elapsed_ms));
		setItem(row, static_cast<int>(gui::game_list_columns::compat),     compat_item);
		setItem(row, static_cast<int>(gui::game_list_columns::dir_size),   new custom_table_widget_item(game_size != umax ? gui::utils::format_byte_size(game_size) : tr("Unknown"), Qt::UserRole, QVariant::fromValue<qulonglong>(game_size)));

		if (selected_item_ids.contains(game->info.path + game->info.icon_path))
		{
			selected_rows.insert(row);
		}

		row++;
	}

	bool first_index = true;

	for (int selected_row : selected_rows)
	{
		const QModelIndex index = model()->index(selected_row, 0);

		selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows);

		if (first_index)
		{
			selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
			first_index = false;
		}
	}
	Q_EMIT itemSelectionChanged();
}

void game_list_table::repaint_icons(std::vector<game_info>& game_data, const QColor& icon_color, const QSize& icon_size, qreal device_pixel_ratio)
{
	game_list_base::repaint_icons(game_data, icon_color, icon_size, device_pixel_ratio);
	adjust_icon_column();
}
