/*
 * Copyright (c) 2015 Ngewi Fet <ngewif@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.gnucash.android.model;

import android.support.annotation.NonNull;
import android.util.Log;

import org.joda.time.LocalDateTime;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Budgets model
 * @author Ngewi Fet <ngewif@gmail.com>
 */
public class Budget extends BaseModel {

    private String mName;
    private String mDescription;
    private Recurrence mRecurrence;
    private List<BudgetAmount> mBudgetAmounts = new ArrayList<>();
    private long mNumberOfPeriods = 12; //default to 12 periods per year

    /**
     * Default constructor
     */
    public Budget(){
        //nothing to see here, move along
    }

    /**
     * Overloaded constructor.
     * Initializes the name and amount of this budget
     * @param name String name of the budget
     */
    public Budget(@NonNull String name){
        this.mName = name;
    }

    public Budget(@NonNull String name, @NonNull Recurrence recurrence){
        this.mName = name;
        this.mRecurrence = recurrence;
    }

    /**
     * Returns the name of the budget
     * @return name of the budget
     */
    public String getName() {
        return mName;
    }

    /**
     * Sets the name of the budget
     * @param name String name of budget
     */
    public void setName(@NonNull String name) {
        this.mName = name;
    }

    /**
     * Returns the description of the budget
     * @return String description of budget
     */
    public String getDescription() {
        return mDescription;
    }

    /**
     * Sets the description of the budget
     * @param description String description
     */
    public void setDescription(String description) {
        this.mDescription = description;
    }

    /**
     * Returns the recurrence for this budget
     * @return Recurrence object for this budget
     */
    public Recurrence getRecurrence() {
        return mRecurrence;
    }

    /**
     * Set the recurrence pattern for this budget
     * @param recurrence Recurrence object
     */
    public void setRecurrence(@NonNull Recurrence recurrence) {
        this.mRecurrence = recurrence;
    }

    /**
     * Return list of budget amounts associated with this budget
     * @return List of budget amounts
     */
    public List<BudgetAmount> getBudgetAmounts() {
        return mBudgetAmounts;
    }

    /**
     * Set the list of budget amounts
     * @param budgetAmounts List of budget amounts
     */
    public void setBudgetAmounts(List<BudgetAmount> budgetAmounts) {
        this.mBudgetAmounts = budgetAmounts;
        for (BudgetAmount budgetAmount : mBudgetAmounts) {
            budgetAmount.setBudgetUID(getUID());
        }
    }

    /**
     * Adds a BudgetAmount to this budget
     * @param budgetAmount Budget amount
     */
    public void addBudgetAmount(BudgetAmount budgetAmount){
        budgetAmount.setBudgetUID(getUID());
        mBudgetAmounts.add(budgetAmount);
    }

    /**
     * Returns the budget amount for a specific account
     * @param accountUID GUID of the account
     * @return Money amount of the budget or null if the budget has no amount for the account
     */
    public Money getAmount(@NonNull String accountUID){
        for (BudgetAmount budgetAmount : mBudgetAmounts) {
            if (budgetAmount.getAccountUID().equals(accountUID))
                return budgetAmount.getAmount();
        }
        return null;
    }

    /**
     * Returns the budget amount for a specific account and period
     * @param accountUID GUID of the account
     * @param periodNum Budgeting period, zero-based index
     * @return Money amount or zero if no matching {@link BudgetAmount} is found for the period
     */
    public Money getAmount(@NonNull String accountUID, int periodNum){
        for (BudgetAmount budgetAmount : mBudgetAmounts) {
            if (budgetAmount.getAccountUID().equals(accountUID)
                    && (budgetAmount.getPeriodNum() == periodNum || budgetAmount.getPeriodNum() == -1)){
                return budgetAmount.getAmount();
            }
        }
        return Money.getZeroInstance();
    }

    /**
     * Returns the sum of all budget amounts in this budget
     * <p><b>NOTE:</b> This method ignores budgets of accounts which are in different currencies</p>
     * @return Money sum of all amounts
     */
    public Money getAmountSum(){
        Money sum = null; //we explicitly allow this null instead of a money instance, because this method should never return null for a budget
        for (BudgetAmount budgetAmount : mBudgetAmounts) {
            if (sum == null){
                sum = budgetAmount.getAmount();
            } else {
                try {
                    sum = sum.add(budgetAmount.getAmount().abs());
                } catch (Money.CurrencyMismatchException ex){
                    Log.i(getClass().getSimpleName(), "Skip some budget amounts with different currency");
                }
            }
        }
        return sum;
    }

    /**
     * Returns the number of periods covered by this budget
     * @return Number of periods
     */
    public long getNumberOfPeriods() {
        return mNumberOfPeriods;
    }

    /**
     * Returns the timestamp of the start of current period of the budget
     * @return Start timestamp in milliseconds
     */
    public long getStartofCurrentPeriod(){
        LocalDateTime localDate = new LocalDateTime();
        int interval = mRecurrence.getMultiplier();
        switch (mRecurrence.getPeriodType()){
            case HOUR:
                localDate = localDate.millisOfDay().withMinimumValue().plusHours(interval);
                break;
            case DAY:
                localDate = localDate.millisOfDay().withMinimumValue().plusDays(interval);
                break;
            case WEEK:
                localDate = localDate.dayOfWeek().withMinimumValue().minusDays(interval);
                break;
            case MONTH:
                localDate = localDate.dayOfMonth().withMinimumValue().minusMonths(interval);
                break;
            case YEAR:
                localDate = localDate.dayOfYear().withMinimumValue().minusYears(interval);
                break;
        }
        return localDate.toDate().getTime();
    }

    /**
     * Returns the end timestamp of the current period
     * @return End timestamp in milliseconds
     */
    public long getEndOfCurrentPeriod(){
        LocalDateTime localDate = new LocalDateTime();
        int interval = mRecurrence.getMultiplier();
        switch (mRecurrence.getPeriodType()){
            case HOUR:
                localDate = localDate.millisOfDay().withMaximumValue().plusHours(interval);
                break;
            case DAY:
                localDate = localDate.millisOfDay().withMaximumValue().plusDays(interval);
                break;
            case WEEK:
                localDate = localDate.dayOfWeek().withMaximumValue().plusWeeks(interval);
                break;
            case MONTH:
                localDate = localDate.dayOfMonth().withMaximumValue().plusMonths(interval);
                break;
            case YEAR:
                localDate = localDate.dayOfYear().withMaximumValue().plusYears(interval);
                break;
        }
        return localDate.toDate().getTime();
    }

    public long getStartOfPeriod(int periodNum){
        LocalDateTime localDate = new LocalDateTime(mRecurrence.getPeriodStart().getTime());
        int interval = mRecurrence.getMultiplier() * periodNum;
        switch (mRecurrence.getPeriodType()){
            case HOUR:
                localDate = localDate.millisOfDay().withMinimumValue().plusHours(interval);
                break;
            case DAY:
                localDate = localDate.millisOfDay().withMinimumValue().plusDays(interval);
                break;
            case WEEK:
                localDate = localDate.dayOfWeek().withMinimumValue().minusDays(interval);
                break;
            case MONTH:
                localDate = localDate.dayOfMonth().withMinimumValue().minusMonths(interval);
                break;
            case YEAR:
                localDate = localDate.dayOfYear().withMinimumValue().minusYears(interval);
                break;
        }
        return localDate.toDate().getTime();
    }

    /**
     * Returns the end timestamp of the period
     * @param periodNum Number of the period
     * @return End timestamp in milliseconds of the period
     */
    public long getEndOfPeriod(int periodNum){
        LocalDateTime localDate = new LocalDateTime();
        int interval = mRecurrence.getMultiplier() * periodNum;
        switch (mRecurrence.getPeriodType()){
            case HOUR:
                localDate = localDate.plusHours(interval);
                break;
            case DAY:
                localDate = localDate.millisOfDay().withMaximumValue().plusDays(interval);
                break;
            case WEEK:
                localDate = localDate.dayOfWeek().withMaximumValue().plusWeeks(interval);
                break;
            case MONTH:
                localDate = localDate.dayOfMonth().withMaximumValue().plusMonths(interval);
                break;
            case YEAR:
                localDate = localDate.dayOfYear().withMaximumValue().plusYears(interval);
                break;
        }
        return localDate.toDate().getTime();
    }

    /**
     * Sets the number of periods for the budget
     * @param numberOfPeriods Number of periods as long
     */
    public void setNumberOfPeriods(long numberOfPeriods) {
        this.mNumberOfPeriods = numberOfPeriods;
    }

    /**
     * Returns the number of accounts in this budget
     * @return Number of budgeted accounts
     */
    public int getNumberOfAccounts(){
        Set<String> accountSet = new HashSet<>();
        for (BudgetAmount budgetAmount : mBudgetAmounts) {
            accountSet.add(budgetAmount.getAccountUID());
        }
        return accountSet.size();
    }

    /**
     * Returns the list of budget amounts where only one BudgetAmount is present if the amount of the budget amount
     * is the same for all periods in the budget.
     * BudgetAmounts with different amounts per period are still return separately
     * <p>
     *     This method is used during import because GnuCash desktop saves one BudgetAmount per period for the whole budgeting period.
     *     While this can be easily displayed in a table form on the desktop, it is not feasible in the Android app.
     *     So we display only one BudgetAmount if it covers all periods in the budgeting period
     * </p>
     * @return List of {@link BudgetAmount}s
     */
    public List<BudgetAmount> getCompactedBudgetAmounts(){

        Map<String, List<BigDecimal>> accountAmountMap = new HashMap<>();
        for (BudgetAmount budgetAmount : mBudgetAmounts) {
            String accountUID = budgetAmount.getAccountUID();
            BigDecimal amount = budgetAmount.getAmount().asBigDecimal();
            if (accountAmountMap.containsKey(accountUID)){
                accountAmountMap.get(accountUID).add(amount);
            } else {
                List<BigDecimal> amounts = new ArrayList<>();
                amounts.add(amount);
                accountAmountMap.put(accountUID, amounts);
            }
        }

        List<BudgetAmount> compactBudgetAmounts = new ArrayList<>();
        for (Map.Entry<String, List<BigDecimal>> entry : accountAmountMap.entrySet()) {
            List<BigDecimal> amounts = entry.getValue();
            BigDecimal first = amounts.get(0);
            boolean allSame = true;
            for (BigDecimal bigDecimal : amounts) {
                allSame &= bigDecimal.equals(first);
            }

            if (allSame){
                if (amounts.size() == 1) {
                    for (BudgetAmount bgtAmount : mBudgetAmounts) {
                        if (bgtAmount.getAccountUID().equals(entry.getKey())) {
                            compactBudgetAmounts.add(bgtAmount);
                            break;
                        }
                    }
                } else {
                    BudgetAmount bgtAmount = new BudgetAmount(getUID(), entry.getKey());
                    bgtAmount.setAmount(new Money(first, Commodity.DEFAULT_COMMODITY));
                    bgtAmount.setPeriodNum(-1);
                    compactBudgetAmounts.add(bgtAmount);
                }
            } else {
                //if not all amounts are the same, then just add them as we read them
                for (BudgetAmount bgtAmount : mBudgetAmounts) {
                    if (bgtAmount.getAccountUID().equals(entry.getKey())){
                        compactBudgetAmounts.add(bgtAmount);
                    }
                }
            }
        }

        return compactBudgetAmounts;
    }

    /**
     * Returns a list of budget amounts where each period has it's own budget amount
     * <p>Any budget amounts in the database with a period number of -1 are expanded to individual budget amounts for all periods</p>
     * <p>This method is useful with exporting budget amounts to XML</p>
     * @return List of expande
     */
    public List<BudgetAmount> getExpandedBudgetAmounts(){
        List<BudgetAmount> amountsToAdd = new ArrayList<>();
        List<BudgetAmount> amountsToRemove = new ArrayList<>();
        for (BudgetAmount budgetAmount : mBudgetAmounts) {
            if (budgetAmount.getPeriodNum() == -1){
                amountsToRemove.add(budgetAmount);
                String accountUID = budgetAmount.getAccountUID();
                for (int period = 0; period < mNumberOfPeriods; period++) {
                    BudgetAmount bgtAmount = new BudgetAmount(getUID(), accountUID);
                    bgtAmount.setAmount(budgetAmount.getAmount());
                    bgtAmount.setPeriodNum(period);
                    amountsToAdd.add(bgtAmount);
                }
            }
        }

        List<BudgetAmount> expandedBudgetAmounts = new ArrayList<>(mBudgetAmounts);
        for (BudgetAmount bgtAmount : amountsToRemove) {
            expandedBudgetAmounts.remove(bgtAmount);
        }

        for (BudgetAmount bgtAmount : amountsToAdd) {
            expandedBudgetAmounts.add(bgtAmount);
        }
        return expandedBudgetAmounts;
    }
}
