/*******************************************************************************
***  This file is part of Dux Rei Nummariae.                                 ***
***                                                                          ***
***  Copyright (C) 2017, 2018, 2019, 2020, 2021                              ***
***  John Schneiderman <Licensing _AT_ Schneiderman _DOT_ me>                ***
***                                                                          ***
***  This program is free software: you can redistribute it and/or modify it ***
***  under the terms of the GNU Affero General Public License as published   ***
***  by the Free Software Foundation, either version 3 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 Affero General Public License for more details.             ***
***                                                                          ***
***  You should have received a copy of the GNU Affero General Public License***
***  along with this program. If not, see <http://www.gnu.org/licenses/>.    ***
*******************************************************************************/
#include "Budget.h"
using pecunia::currency::Iso4217Codes;
using pecunia::currency::Money;
using drn::foundation::Days;
using drn::foundation::ObserverPtr;
using std::chrono::hours;
using std::make_pair;
using std::map;
using std::pair;
using std::set;
using std::size_t;
using std::type_index;
using drn::budgeting::Budget;
using drn::budgeting::BudgetItemIdentifier;
using drn::budgeting::BudgetTotals;
using drn::budgeting::Nontrack;
using drn::budgeting::EventFrequency;
using drn::budgeting::BillMap;
using drn::budgeting::GoalMap;
using drn::budgeting::WageMap;
using drn::budgeting::DebtMap;

#include <ostream>
using std::ostream;
#include <utility>
using std::move;

#include "Bill.h"
using drn::budgeting::Bill;
#include "BudgetedMoney.h"
using drn::budgeting::BudgetedMoney;
#include "BudgetItemTypes.h"
using drn::budgeting::BudgetItemTypes;
#include "BudgetSource.h"
using drn::budgeting::BudgetSource;
#include "Debt.h"
using drn::budgeting::Debt;
#include <foundation/Optional.hpp>
using drn::foundation::Optional;
#include "Goal.h"
using drn::budgeting::Goal;
#include "Nontrack.h"
using drn::budgeting::Nontrack;
#include "Wage.h"
using drn::budgeting::Wage;


//{ Budget Iterator

Budget::Iterator::Iterator(const Budget& budget) :
	budget_{&budget},
	billIter_{budget.bills_.cbegin()},
	debtIter_{budget.debts_.cbegin()},
	goalIter_{budget.goals_.cbegin()},
	nontrackIter_{budget.nontracks_.cbegin()},
	wageIter_{budget.wages_.cbegin()},
	current_{Budget::Iterator::endValue_}
{}

Budget::Iterator::const_reference Budget::Iterator::operator*() const
{
	return this->current_;
}

Budget::Iterator::const_pointer Budget::Iterator::operator->() const
{
	return &this->current_;
}

Budget::Iterator& Budget::Iterator::operator++()
{
	// Increments to the next item in the budget item map in preparation for the next read,
	// since all the iters are pointing at the first element upon construction. Returns true if the
	// current value is valid and readable or false if the next element to be read was at the end of
	// the map.
	const auto advanceIter{
		[this] (auto& iter, const BudgetItemTypes& type, const auto& iterEnd)
		{
			// This can happen when a budget does not contain a type.
			if (iter == iterEnd)
				return false;
			this->current_.first = {type, iter->first};
			this->current_.second = &iter->second;
			++iter;
			return true;
		}
	};

	switch (this->current_.first.type_)
	{
	case BudgetItemTypes::Bill:
		if (advanceIter(this->billIter_, BudgetItemTypes::Bill, this->budget_->bills_.cend()))
			break;
		[[fallthrough]];
	case BudgetItemTypes::Debt:
		if (advanceIter(this->debtIter_, BudgetItemTypes::Debt, this->budget_->debts_.cend()))
			break;
		[[fallthrough]];
	case BudgetItemTypes::Goal:
		if (advanceIter(this->goalIter_, BudgetItemTypes::Goal, this->budget_->goals_.cend()))
			break;
		[[fallthrough]];
	case BudgetItemTypes::Nontrack:
		if (
			advanceIter(
				this->nontrackIter_,
				BudgetItemTypes::Nontrack,
				this->budget_->nontracks_.cend()
			)
		)
			break;
		[[fallthrough]];
	case BudgetItemTypes::Wage:
		if (advanceIter(this->wageIter_, BudgetItemTypes::Wage, this->budget_->wages_.cend()))
			break;
		[[fallthrough]];
	case BudgetItemTypes::Unknown:
	default:
		this->current_ = Budget::Iterator::endValue_;
	};
	return *this;
}

Budget::Iterator Budget::Iterator::operator++(int)
{
	Budget::Iterator copy{*this};
	++(*this);
	return copy;
}

Budget::Iterator Budget::Iterator::begin() const
{
	Budget::Iterator iter{*this->budget_};
	const auto setIter{
		[&iter] (const auto& beginIter, const BudgetItemTypes& type)
		{
			iter.current_.first = {type, beginIter->first};
			iter.current_.second = &beginIter->second;
			// Just like in the increment operator, we need to set up for the next read.
			++iter;
		}
	};

	if ( ! this->budget_->bills_.empty())
		setIter(this->budget_->bills_.cbegin(), BudgetItemTypes::Bill);
	else if ( ! this->budget_->debts_.empty())
		setIter(this->budget_->debts_.cbegin(), BudgetItemTypes::Debt);
	else if ( ! this->budget_->goals_.empty())
		setIter(this->budget_->goals_.cbegin(), BudgetItemTypes::Goal);
	else if ( ! this->budget_->nontracks_.empty())
		setIter(this->budget_->nontracks_.cbegin(), BudgetItemTypes::Nontrack);
	else if ( ! this->budget_->wages_.empty())
		setIter(this->budget_->wages_.cbegin(), BudgetItemTypes::Wage);
	else
		iter.current_ = Budget::Iterator::endValue_;
	return iter;
}

Budget::Iterator Budget::Iterator::end() const
{
	// Already pointing to the "end" from the constructor.
	return Budget::Iterator{*this->budget_};
}

const Budget::Iterator::value_type Budget::Iterator::endValue_{
	make_pair(BudgetItemIdentifier{}, nullptr)
};

//}

//{ Budget

Budget::Budget(
	WageMap wages,
	BillMap bills,
	DebtMap debts,
	GoalMap goals,
	NontrackMap nontracks
) :
	wages_{move(wages)},
	bills_{move(bills)},
	debts_{move(debts)},
	goals_{move(goals)},
	nontracks_{move(nontracks)}
{}

bool Budget::operator==(const Budget& other) const
{
	return this->wages_ == other.wages_
		&& this->bills_ == other.bills_
		&& this->debts_ == other.debts_
		&& this->goals_ == other.goals_
		&& this->nontracks_ == other.nontracks_;
}

bool Budget::isEmpty() const
{
	return this->wages_.empty()
		&& this->bills_.empty()
		&& this->debts_.empty()
		&& this->goals_.empty()
		&& this->nontracks_.empty();
}

size_t Budget::size() const
{
	return this->wages_.size()
			+ this->bills_.size()
			+ this->debts_.size()
			+ this->goals_.size()
			+ this->nontracks_.size();
}

map<type_index, set<BudgetSource>> Budget::sources() const
{
	map<type_index, set<BudgetSource>> typeSources;
	typeSources[typeid(Bill)] = this->bills_.sources();
	typeSources[typeid(Debt)] = this->debts_.sources();
	typeSources[typeid(Goal)] = this->goals_.sources();
	typeSources[typeid(Nontrack)] = this->nontracks_.sources();
	typeSources[typeid(Wage)] = this->wages_.sources();
	return typeSources;
}

Budget::Iterator Budget::begin() const
{
	return Iterator{*this}.begin();
}

Budget::Iterator Budget::end() const
{
	return Iterator{*this}.end();
}

//}

//{ BudgetTotals

BudgetTotals::BudgetTotals(
	const Budget& budget,
	const Iso4217Codes& currency,
	const EventFrequency period,
	const hours& hrInWeek,
	const Days& dayInWeek
) :
	wage_{budget.wages_.total(currency, period, hrInWeek, dayInWeek)},
	goals_{budget.goals_.total(currency, period, hrInWeek, dayInWeek)},
	bills_{budget.bills_.total(currency, period, hrInWeek, dayInWeek)},
	debts_{budget.debts_.total(currency, period, hrInWeek, dayInWeek)},
	nontrack_{budget.nontracks_.total(currency, period, hrInWeek, dayInWeek)},
	period_{period},
	remaining_{this->wage_ - this->nontrack_ - this->goals_ - this->bills_ - this->debts_}
{}

//}

set<Iso4217Codes> drn::budgeting::containedCurrencies(const Budget& b)
{
	set<Iso4217Codes> codes;
	const auto addUniqueCurrency{
		[&codes] (const Iso4217Codes& code)
		{
			if (code == Iso4217Codes::XXX)
				return;

			if (codes.count(code) == 0)
				codes.insert(code);
		}
	};

	for (const auto& wage : b.wages_)
		addUniqueCurrency(wage.second.amount().code());

	for (const auto& bill : b.bills_)
		addUniqueCurrency(bill.second.amount().code());

	for (const auto& debt : b.debts_)
		addUniqueCurrency(debt.second.amount().code());

	for (const auto& goal : b.goals_)
		addUniqueCurrency(goal.second.amount().code());

	for (const auto& nontrack : b.nontracks_)
		addUniqueCurrency(nontrack.second.amount().code());
	return codes;
}

ostream& drn::budgeting::operator<<(ostream& o, const Budget& b)
{
	const auto streamBudgetItems{
		[] (ostream& stream, const auto& items)
		{
			bool isFirst{true};
			for (const auto& item : items)
			{
				stream << (isFirst ? "" : ", ") << item.second;
				isFirst = false;
			}
		}
	};
	o << "Wages (";
	streamBudgetItems(o, b.wages_);
	o << ")\nBills (";
	streamBudgetItems(o, b.bills_);
	o << ")\nDebts (";
	streamBudgetItems(o, b.debts_);
	o << ")\nGoals (";
	streamBudgetItems(o, b.goals_);
	o << ")\nNontrack (";
	streamBudgetItems(o, b.nontracks_);
	return o  << ')';
}

bool drn::budgeting::operator!=(const Budget& lhs, const Budget& rhs)
{
	return ! (lhs == rhs);
}

template<>
Optional<Bill> drn::budgeting::findBudgetItem<Bill>(
	const BudgetSource& needle,
	const Budget& haystack
)
{
	if (haystack.bills_.has(needle))
		return haystack.bills_.at(needle);
	return {};
}

template<>
Optional<Debt> drn::budgeting::findBudgetItem<Debt>(
	const BudgetSource& needle,
	const Budget& haystack
)
{
	if (haystack.debts_.has(needle))
		return haystack.debts_.at(needle);
	return {};
}

template<>
Optional<Goal> drn::budgeting::findBudgetItem<Goal>(
	const BudgetSource& needle,
	const Budget& haystack
)
{
	if (haystack.goals_.has(needle))
		return haystack.goals_.at(needle);
	return {};
}

template<>
Optional<Nontrack> drn::budgeting::findBudgetItem<Nontrack>(
	const BudgetSource& needle,
	const Budget& haystack
)
{
	if (haystack.nontracks_.has(needle))
		return haystack.nontracks_.at(needle);
	return {};
}

template<>
Optional<Wage> drn::budgeting::findBudgetItem<Wage>(
	const BudgetSource& needle,
	const Budget& haystack
)
{
	if (haystack.wages_.has(needle))
		return haystack.wages_.at(needle);
	return {};
}

const BudgetedMoney& drn::budgeting::findBudgetItem(const BudgetItemIdentifier& id, const Budget& b)
{
	if (b.bills_.has(id.source_))
		return b.bills_.at(id.source_);
	if (b.debts_.has(id.source_))
		return b.debts_.at(id.source_);
	if (b.goals_.has(id.source_))
		return b.goals_.at(id.source_);
	if (b.nontracks_.has(id.source_))
		return b.nontracks_.at(id.source_);
	if (b.wages_.has(id.source_))
		return b.wages_.at(id.source_);
	throw BudgetItemError{
		id.source_,
		QStringLiteral("The source does not exist in the budget.")
	};
}

bool drn::budgeting::operator==(const Budget::Iterator& lhs, const Budget::Iterator& rhs)
{
	return lhs.current_ == rhs.current_;
}

bool drn::budgeting::operator!=(const Budget::Iterator& lhs, const Budget::Iterator& rhs)
{
	return lhs.current_ != rhs.current_;
}
