/***************************************************************************
 *   Copyright (C) 2008-2021 by Andrzej Rybczak                            *
 *   andrzej@rybczak.net                                                   *
 *                                                                         *
 *   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; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   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 for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.              *
 ***************************************************************************/

#ifndef NCMPCPP_HELPERS_H
#define NCMPCPP_HELPERS_H

#include "interfaces.h"
#include "mpdpp.h"
#include "screens/playlist.h"
#include "screens/screen.h"
#include "settings.h"
#include "song_list.h"
#include "status.h"
#include "utility/string.h"
#include "utility/type_conversions.h"
#include "utility/wide_string.h"

enum ReapplyFilter { Yes, No };

template <typename ItemT>
struct ScopedUnfilteredMenu
{
	ScopedUnfilteredMenu(ReapplyFilter reapply_filter, NC::Menu<ItemT> &menu)
		: m_refresh(false), m_reapply_filter(reapply_filter), m_menu(menu)
	{
		m_is_filtered = m_menu.isFiltered();
		if (m_is_filtered)
			m_menu.showAllItems();
	}

	~ScopedUnfilteredMenu()
	{
		if (m_is_filtered)
		{
			switch (m_reapply_filter)
			{
			case ReapplyFilter::Yes:
				m_menu.reapplyFilter();
				break;
			case ReapplyFilter::No:
				m_menu.showFilteredItems();
				break;
			}
		}
		if (m_refresh)
			m_menu.refresh();
	}

	void set(ReapplyFilter reapply_filter, bool refresh)
	{
		m_reapply_filter = reapply_filter;
		m_refresh = refresh;
	}

private:
	bool m_is_filtered;
	bool m_refresh;
	ReapplyFilter m_reapply_filter;
	NC::Menu<ItemT> &m_menu;
};

template <typename Iterator, typename PredicateT>
Iterator wrappedSearch(Iterator begin, Iterator current, Iterator end,
                       const PredicateT &pred, bool wrap, bool skip_current)
{
	if (begin == end)
	{
		assert(current == end);
		return begin;
	}
	if (skip_current)
		++current;
	auto it = std::find_if(current, end, pred);
	if (it == end && wrap)
	{
		it = std::find_if(begin, current, pred);
		if (it == current)
			it = end;
	}
	return it;
}

template <typename ItemT, typename PredicateT>
bool search(NC::Menu<ItemT> &m, const PredicateT &pred,
                  SearchDirection direction, bool wrap, bool skip_current)
{
	bool result = false;
	if (pred.defined())
	{
		switch (direction)
		{
			case SearchDirection::Backward:
			{
				auto it = wrappedSearch(m.rbegin(), m.rcurrent(), m.rend(),
					pred, wrap, skip_current
				);
				if (it != m.rend())
				{
					m.highlight(it.base()-m.begin()-1);
					result = true;
				}
				break;
			}
			case SearchDirection::Forward:
			{
				auto it = wrappedSearch(m.begin(), m.current(), m.end(),
					pred, wrap, skip_current
				);
				if (it != m.end())
				{
					m.highlight(it-m.begin());
					result = true;
				}
			}
		}
	}
	return result;
}

template <typename Iterator>
bool hasSelected(Iterator first, Iterator last)
{
	for (; first != last; ++first)
		if (first->isSelected())
			return true;
	return false;
}

template <typename Iterator>
std::vector<Iterator> getSelected(Iterator first, Iterator last)
{
	std::vector<Iterator> result;
	for (; first != last; ++first)
		if (first->isSelected())
			result.push_back(first);
	return result;
}

/// @return true if range that begins and ends with selected items was
/// found, false when there is no selected items (in which case first
/// == last).
template <typename Iterator>
bool findRange(Iterator &first, Iterator &last)
{
	for (; first != last; ++first)
	{
		if (first->isSelected())
			break;
	}
	if (first == last)
		return false;
	--last;
	for (; first != last; --last)
	{
		if (last->isSelected())
			break;
	}
	++last;
	return true;
}

/// @return true if fully selected range was found or no selected
/// items were found, false otherwise.
template <typename Iterator>
bool findSelectedRange(Iterator &first, Iterator &last)
{
	auto orig_first = first;
	if (!findRange(first, last))
	{
		// If no selected items were found, return original range.
		if (first == last)
		{
			first = orig_first;
			return true;
		}
		else
			return false;
	}
	// We have range, now check if it's filled with selected items.
	for (auto it = first; it != last; ++it)
	{
		if (!it->isSelected())
			return false;
	}
	return true;
}

template <typename T>
void selectCurrentIfNoneSelected(NC::Menu<T> &m)
{
	if (!hasSelected(m.begin(), m.end()))
		m.current()->setSelected(true);
}

template <typename Iterator>
std::vector<Iterator> getSelectedOrCurrent(Iterator first, Iterator last, Iterator current)
{
	std::vector<Iterator> result = getSelected(first, last);
	if (result.empty())
		result.push_back(current);
	return result;
}

template <typename Iterator>
void reverseSelectionHelper(Iterator first, Iterator last)
{
	for (; first != last; ++first)
		first->setSelected(!first->isSelected());
}

template <typename F>
void moveSelectedItemsUp(NC::Menu<MPD::Song> &m, F swap_fun)
{
	if (m.choice() > 0)
		selectCurrentIfNoneSelected(m);
	auto list = getSelected(m.begin(), m.end());
	auto begin = m.begin();
	if (!list.empty() && list.front() != m.begin())
	{
		Mpd.StartCommandsList();
		for (auto it = list.begin(); it != list.end(); ++it)
			swap_fun(&Mpd, *it - begin, *it - begin - 1);
		Mpd.CommitCommandsList();
		if (list.size() > 1)
		{
			for (auto it = list.begin(); it != list.end(); ++it)
			{
				(*it)->setSelected(false);
				(*it-1)->setSelected(true);
			}
			m.highlight(list[(list.size())/2] - begin - 1);
		}
		else
		{
			// if we move only one item, do not select it. however, if single item
			// was selected prior to move, it'll deselect it. oh well.
			list[0]->setSelected(false);
			m.scroll(NC::Scroll::Up);
		}
	}
}

template <typename F>
void moveSelectedItemsDown(NC::Menu<MPD::Song> &m, F swap_fun)
{
	if (m.choice() < m.size()-1)
		selectCurrentIfNoneSelected(m);
	auto list = getSelected(m.rbegin(), m.rend());
	auto begin = m.begin() + 1; // reverse iterators add 1, so we need to cancel it
	if (!list.empty() && list.front() != m.rbegin())
	{
		Mpd.StartCommandsList();
		for (auto it = list.begin(); it != list.end(); ++it)
			swap_fun(&Mpd, it->base() - begin, it->base() - begin + 1);
		Mpd.CommitCommandsList();
		if (list.size() > 1)
		{
			for (auto it = list.begin(); it != list.end(); ++it)
			{
				(*it)->setSelected(false);
				(*it-1)->setSelected(true);
			}
			m.highlight(list[(list.size())/2].base() - begin + 1);
		}
		else
		{
			// if we move only one item, do not select it. however, if single item
			// was selected prior to move, it'll deselect it. oh well.
			list[0]->setSelected(false);
			m.scroll(NC::Scroll::Down);
		}
	}
}

template <typename F>
void moveSelectedItemsTo(NC::Menu<MPD::Song> &menu, F &&move_fun)
{
	auto cur_ptr = &menu.current()->value();
	ScopedUnfilteredMenu<MPD::Song> sunfilter(ReapplyFilter::No, menu);
	// this is kinda shitty, but there is no other way to know
	// what position current item has in unfiltered menu.
	ptrdiff_t pos = 0;
	for (auto it = menu.begin(); it != menu.end(); ++it, ++pos)
		if (&it->value() == cur_ptr)
			break;
	auto begin = menu.begin();
	auto list = getSelected(menu.begin(), menu.end());
	// we move only truly selected items
	if (list.empty())
		return;
	// we can't move to the middle of selected items
	//(this also handles case when list.size() == 1)
	if (pos >= (list.front() - begin) && pos <= (list.back() - begin))
		return;
	int diff = pos - (list.front() - begin);
	Mpd.StartCommandsList();
	if (diff > 0) // move down
	{
		pos -= list.size();
		size_t i = list.size()-1;
		for (auto it = list.rbegin(); it != list.rend(); ++it, --i)
			move_fun(&Mpd, *it - begin, pos+i);
		Mpd.CommitCommandsList();
		i = list.size()-1;
		for (auto it = list.rbegin(); it != list.rend(); ++it, --i)
		{
			(*it)->setSelected(false);
			menu[pos+i].setSelected(true);
		}
	}
	else if (diff < 0) // move up
	{
		size_t i = 0;
		for (auto it = list.begin(); it != list.end(); ++it, ++i)
			move_fun(&Mpd, *it - begin, pos+i);
		Mpd.CommitCommandsList();
		i = 0;
		for (auto it = list.begin(); it != list.end(); ++it, ++i)
		{
			(*it)->setSelected(false);
			menu[pos+i].setSelected(true);
		}
	}
}

template <typename F>
void deleteSelectedSongs(NC::Menu<MPD::Song> &menu, F &&delete_fun)
{
	selectCurrentIfNoneSelected(menu);
	// We need to operate on the whole playlist to get positions right, but at the
	// same time we need to ignore all songs that are not filtered. We abuse the
	// fact that both ranges share the same values, i.e. we can compare addresses
	// of item values to check whether an item belongs to filtered range. TODO: do
	// something more sane here.
	NC::Menu<MPD::Song>::Iterator begin;
	NC::Menu<MPD::Song>::ReverseIterator real_begin, real_end;
	{
		ScopedUnfilteredMenu<MPD::Song> sunfilter(ReapplyFilter::No, menu);
		// obtain iterators for unfiltered range
		begin = menu.begin() + 1; // cancel reverse iterator's offset
		real_begin = menu.rbegin();
		real_end = menu.rend();
	};
	// get iterator to filtered range
	auto cur_filtered = menu.rbegin();
	Mpd.StartCommandsList();
	for (auto it = real_begin; it != real_end; ++it)
	{
		// current iterator belongs to filtered range, proceed
		if (&it->value() == &cur_filtered->value())
		{
			if (it->isSelected())
			{
				it->setSelected(false);
				delete_fun(Mpd, it.base() - begin);
			}
			++cur_filtered;
		}
	}
	Mpd.CommitCommandsList();
}

template <typename F>
void cropPlaylist(NC::Menu<MPD::Song> &m, F delete_fun)
{
	reverseSelectionHelper(m.begin(), m.end());
	deleteSelectedSongs(m, delete_fun);
}

template <typename Iterator>
std::string getSharedDirectory(Iterator first, Iterator last)
{
	assert(first != last);
	std::string result = first->getDirectory();
	while (++first != last)
	{
		result = getSharedDirectory(result, first->getDirectory());
		if (result == "/")
			break;
	}
	return result;
}

template <typename Iterator>
bool addSongsToPlaylist(Iterator first, Iterator last, bool play, int position)
{
	bool result = true;
	auto addSongNoError = [&](Iterator it) -> int {
		try
		{
			return Mpd.AddSong(*it, position);
		}
		catch (MPD::ServerError &e)
		{
			Status::handleServerError(e);
			result = false;
			return -1;
		}
	};

	if (last-first >= 1)
	{
		int id;
		while (true)
		{
			id = addSongNoError(first);
			if (id >= 0)
				break;
			++first;
			if (first == last)
				return result;
		}

		if (position == -1)
		{
			++first;
			for(; first != last; ++first)
				addSongNoError(first);
		}
		else
		{
			++position;
			--last;
			for (; first != last; --last)
				addSongNoError(last);
		}
		if (play)
			Mpd.PlayID(id);
	}

	return result;
}

template <typename T> void ShowTime(T &buf, size_t length, bool short_names)
{
	const unsigned MINUTE = 60;
	const unsigned HOUR = 60*MINUTE;
	const unsigned DAY = 24*HOUR;
	const unsigned YEAR = 365*DAY;
	
	unsigned years = length/YEAR;
	if (years)
	{
		buf << years << (short_names ? "y" : (years == 1 ? " year" : " years"));
		length -= years*YEAR;
		if (length)
			buf << ", ";
	}
	unsigned days = length/DAY;
	if (days)
	{
		buf << days << (short_names ? "d" : (days == 1 ? " day" : " days"));
		length -= days*DAY;
		if (length)
			buf << ", ";
	}
	unsigned hours = length/HOUR;
	if (hours)
	{
		buf << hours << (short_names ? "h" : (hours == 1 ? " hour" : " hours"));
		length -= hours*HOUR;
		if (length)
			buf << ", ";
	}
	unsigned minutes = length/MINUTE;
	if (minutes)
	{
		buf << minutes << (short_names ? "m" : (minutes == 1 ? " minute" : " minutes"));
		length -= minutes*MINUTE;
		if (length)
			buf << ", ";
	}
	if (length)
		buf << length << (short_names ? "s" : (length == 1 ? " second" : " seconds"));
}

template <typename BufferT>
void ShowTag(BufferT &buf, const std::string &tag)
{
	if (tag.empty())
		buf << Config.empty_tags_color
		    << Config.empty_tag
		    << NC::FormattedColor::End<>(Config.empty_tags_color);
	else
		buf << tag;
}

inline NC::Buffer ShowTag(const std::string &tag)
{
	NC::Buffer result;
	ShowTag(result, tag);
	return result;
}

template <typename T>
void setHighlightFixes(NC::Menu<T> &m)
{
	m.setHighlightPrefix(Config.current_item_prefix);
	m.setHighlightSuffix(Config.current_item_suffix);
}

template <typename T>
void setHighlightInactiveColumnFixes(NC::Menu<T> &m)
{
	m.setHighlightPrefix(Config.current_item_inactive_column_prefix);
	m.setHighlightSuffix(Config.current_item_inactive_column_suffix);
}

inline const char *withErrors(bool success)
{
	return success ? "" : " " "(with errors)";
}

void deleteSelectedSongsFromPlaylist(NC::Menu<MPD::Song> &playlist);

bool addSongToPlaylist(const MPD::Song &s, bool play, int position = -1);

const MPD::Song *currentSong(const BaseScreen *screen);

std::string timeFormat(const char *format, time_t t);

std::string Timestamp(time_t t);

std::wstring Scroller(const std::wstring &str, size_t &pos, size_t width);
void writeCyclicBuffer(const NC::WBuffer &buf, NC::Window &w, size_t &start_pos,
                       size_t width, const std::wstring &separator);

#endif // NCMPCPP_HELPERS_H
