/*
 * Copyright (C) 2011 4th Line GmbH, Switzerland
 *
 * 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/>.
 */

package org.fourthline.konto.server.dao;

import org.hibernate.Query;
import org.hibernate.transform.ResultTransformer;
import org.seamless.util.time.DateRange;
import org.fourthline.konto.shared.LedgerCoordinates;
import org.fourthline.konto.shared.MonetaryAmount;
import org.fourthline.konto.shared.entity.Account;
import org.fourthline.konto.shared.entity.Entry;
import org.fourthline.konto.shared.entity.MonetaryUnit;
import org.fourthline.konto.shared.entity.Split;
import org.fourthline.konto.shared.query.EntriesQueryCriteria;
import org.fourthline.konto.shared.query.LedgerLinesQueryCriteria;
import org.fourthline.konto.shared.result.EntryReportLine;
import org.fourthline.konto.shared.result.LedgerLines;
import org.fourthline.konto.shared.result.AccountReportLine;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Christian Bauer
 */
public class EntryDAO extends HibernateDAO {

    public Entry getEntry(Long id) {
        return (Entry) getCurrentSession().get(Entry.class, id);
    }

    public Entry populateSplits(final Entry entry) {
        StringBuilder sb = new StringBuilder();
        sb.append("select s, sa, eaUnit, saUnit from Entry e, Split s, ");
        sb.append("Account ea, MonetaryUnit eaUnit, Account sa, MonetaryUnit saUnit");
        sb.append(" where s.entryId = :entryId");
        sb.append(" and e.id = s.entryId");
        sb.append(" and ea.id = e.accountId and sa.id = s.accountId");
        sb.append(" and ea.monetaryUnitId = eaUnit.id and sa.monetaryUnitId = saUnit.id");

        Query q = getCurrentSession().createQuery(sb.toString());
        q.setLong("entryId", entry.getId());

        q.setResultTransformer(new ResultTransformer() {
            @Override
            public Object transformTuple(Object[] objects, String[] strings) {
                Split split = (Split) objects[0];
                split.setAccount((Account) objects[1]);
                split.setEntryMonetaryUnit((MonetaryUnit) objects[2]);
                split.setMonetaryUnit((MonetaryUnit) objects[3]);
                split.setEntry(entry);
                return split;
            }

            @Override
            public List transformList(List list) {
                return list;
            }
        });

        entry.setSplits(q.list());
        return entry;
    }

    public Split getSplit(Long id) {
        StringBuilder sb = new StringBuilder();
        sb.append("select s, sa, eaUnit, saUnit from Entry e, Split s, ");
        sb.append("Account ea, MonetaryUnit eaUnit, Account sa, MonetaryUnit saUnit");
        sb.append(" where e.id = s.entryId");
        sb.append(" and ea.id = e.accountId and sa.id = s.accountId");
        sb.append(" and ea.monetaryUnitId = eaUnit.id and sa.monetaryUnitId = saUnit.id");
        sb.append(" and s.id = :id");

        Query q = getCurrentSession().createQuery(sb.toString());
        q.setLong("id", id);

        q.setResultTransformer(new ResultTransformer() {
            @Override
            public Object transformTuple(Object[] objects, String[] strings) {
                Split split = (Split) objects[0];
                split.setAccount((Account) objects[1]);
                split.setEntryMonetaryUnit((MonetaryUnit) objects[2]);
                split.setMonetaryUnit((MonetaryUnit) objects[3]);
                return split;
            }

            @Override
            public List transformList(List list) {
                return list;
            }
        });

        return (Split) q.uniqueResult();
    }

    public LedgerLines getLedgerLines(Account account,
                                      LedgerLinesQueryCriteria criteria) {

        MonetaryAmount startingBalance;

        if (criteria.isStringFiltered()) {
            // Always start at zero balance because not all lines are shown
            startingBalance = new MonetaryAmount(account.getMonetaryUnit());
        } else {
            // We might possibly show all lines...
            if (criteria.isStartingAfterAccountEffectiveOn(account)) {
                // No, we show a subset of all lines restricted by time, so calculate
                // the starting balance up to our starting timepoint (minus one day)
                Date oneDayBefore = criteria.getEffectiveOn().getOneDayBeforeStart();
                List<AccountReportLine> balanceOfDayBefore =
                        getAccountReportLines(Arrays.asList(account), new DateRange(null, oneDayBefore), true);
                startingBalance = balanceOfDayBefore.get(0).getAmount();
            } else {
                // Yes, we show all lines, so start with the account's initial balance
                startingBalance = account.getInitialBalance();
            }
        }

        return getLedgerLines(account, criteria, startingBalance);
    }

    protected LedgerLines getLedgerLines(Account account,
                                         LedgerLinesQueryCriteria criteria,
                                         MonetaryAmount startingBalance) {

        List<Entry> entries = getEntries(account, criteria);

        LedgerLines lines = new LedgerLines(account, criteria.getEffectiveOn(), startingBalance);
        for (Entry entry : entries) {
            lines.addEntry(entry);
        }

        // TODO: More flexible sorting and balance calculation
        if (criteria.getOrderBy() == null || criteria.getOrderBy().equals(Entry.Property.effectiveOn)) {
            boolean balanceAscending = criteria.getOrderBy() == null || !criteria.isSortAscending();
            lines.updateBalances(balanceAscending);
        }

        return lines;
    }

    public List<Entry> getEntries(final Account account, EntriesQueryCriteria criteria) {

        StringBuilder sb = new StringBuilder();
        sb.append("select e, s, ea, sa, eaUnit, saUnit from Entry e, Split s, ");
        sb.append("Account ea, MonetaryUnit eaUnit, Account sa, MonetaryUnit saUnit");
        sb.append(" where e.id = s.entryId");
        sb.append(" and ea.id = e.accountId and sa.id = s.accountId");
        sb.append(" and ea.monetaryUnitId = eaUnit.id and sa.monetaryUnitId = saUnit.id");
        sb.append(" and (e.accountId = :accountId or s.accountId = :accountId)");

        if (!criteria.isStringFilterEmpty())
            sb.append(" and (lower(e.description) like :desc or lower(s.description) like :desc)");

        if (criteria.getEffectiveOn() != null && criteria.getEffectiveOn().getStart() != null)
            sb.append(" and e.effectiveOn >= :effectiveOnStart");

        if (criteria.getEffectiveOn() != null && criteria.getEffectiveOn().getEnd() != null)
            sb.append(" and e.effectiveOn <= :effectiveOnEnd");

        sb.append(" order by ");
        if (criteria.getOrderBy() == null) {
            sb.append("e.effectiveOn desc");
        } else if (criteria.getOrderBy().equals(Entry.Property.description)) {
            sb.append("e.description ").append(criteria.isSortAscending() ? "asc" : "desc");
            sb.append(", s.description ").append(criteria.isSortAscending() ? "asc" : "desc");
        } else if (criteria.getOrderBy().equals(Entry.Property.enteredOn)) {
            sb.append("e.enteredOn ").append(criteria.isSortAscending() ? "asc" : "desc");
        } else {
            sb.append("e.effectiveOn ").append(criteria.isSortAscending() ? "asc" : "desc");
        }
        sb.append(", e.enteredOn desc, s.enteredOn desc");

        Query q = getCurrentSession().createQuery(sb.toString());

        q.setLong("accountId", account.getId());

        if (!criteria.isStringFilterEmpty())
            q.setString("desc", criteria.getStringFilterWildcards());

        if (criteria.getEffectiveOn() != null && criteria.getEffectiveOn().getStart() != null)
            q.setDate("effectiveOnStart", criteria.getEffectiveOn().getStart());

        if (criteria.getEffectiveOn() != null && criteria.getEffectiveOn().getEnd() != null)
            q.setDate("effectiveOnEnd", criteria.getEffectiveOn().getEnd());

        // TODO: Pagination?

        final Map<Long, Entry> entries = new LinkedHashMap();

        q.setResultTransformer(new ResultTransformer() {
            @Override
            public Object transformTuple(Object[] objects, String[] strings) {
                Entry entry = (Entry) objects[0];
                Split split = (Split) objects[1];
                entry.setAccount((Account) objects[2]);
                split.setAccount((Account) objects[3]);
                split.setEntryMonetaryUnit((MonetaryUnit) objects[4]);
                split.setMonetaryUnit((MonetaryUnit) objects[5]);

                Entry existingEntry = entries.get(entry.getId());
                if (existingEntry != null) {
                    split.setEntry(existingEntry);
                    existingEntry.getSplits().add(split);
                } else {
                    // Clear the splits, this entry might have passed through
                    // this transformer before (querying several times with the same PC)
                    entry.getSplits().clear();
                    entry.getSplits().add(split);
                    split.setEntry(entry);
                    entries.put(entry.getId(), entry);
                }
                return null;
            }

            @Override
            public List transformList(List list) {
                return list;
            }
        });

        q.list();

        return new ArrayList(entries.values());
    }

    protected List<Long> getAccountIds(List<Account> accounts) {
        List<Long> ids = new ArrayList(accounts.size());
        for (Account account : accounts) {
            ids.add(account.getId());
        }
        return ids;
    }

    public List<AccountReportLine> getAccountReportLines(List<Account> accounts, DateRange dateRange, boolean useInitialBalance) {
        List<Long> accountIds = getAccountIds(accounts);

        Query q = getCurrentSession().getNamedQuery("sumOfAccounts");
        q.setParameterList("ids", accountIds);
        // Default to 01.01.1900 -> Today if there is no start or end in the given date range
        q.setDate("rangeStart", dateRange.getStart() != null ? dateRange.getStart() : new Date(0, 0, 1));
        q.setDate("rangeEnd", dateRange.getEnd() != null ? dateRange.getEnd() : new Date());

        List<Object[]> result = q.list();

        List<AccountReportLine> lines = new ArrayList(accounts.size());

        for (Account account : accounts) {

            // Start with account's initial balance or zero, then add the query result balance
            MonetaryAmount amount =
                    useInitialBalance
                            ? account.getInitialBalance()
                            : new MonetaryAmount(account.getMonetaryUnit());

            for (Object[] r : result) {
                if (account.getId().equals(r[0])) {
                    MonetaryAmount m =
                            new MonetaryAmount(account.getMonetaryUnit(), (BigDecimal) r[1]);
                    amount = amount.add(m);
                    break;
                }
            }
            lines.add(new AccountReportLine(account, amount));
        }
        return lines;
    }

    public Map<Account, List<EntryReportLine>> getEntryReportLines(List<Account> accounts, DateRange dateRange) {
        List<Long> accountIds = getAccountIds(accounts);

        Query q = getCurrentSession().getNamedQuery("cashflowOfAccounts");
        q.setParameterList("ids", accountIds);

        // Default to 01.01.1900 -> Today if there is no start or end in the given date range
        q.setDate("rangeStart", dateRange.getStart() != null ? dateRange.getStart() : new Date(0, 0, 1));
        q.setDate("rangeEnd", dateRange.getEnd() != null ? dateRange.getEnd() : new Date());

        List<Object[]> result = q.list();

        Map<Account, List<EntryReportLine>> accountEntryLines = new LinkedHashMap(accounts.size());

        for (Account account : accounts) {

            boolean haveEntries = false;
            for (Object[] r : result) {
                if (account.getId().equals(r[0])) {

                    List<EntryReportLine> lines;
                    if ((lines = accountEntryLines.get(account)) == null) {
                        lines = new ArrayList();
                        accountEntryLines.put(account, lines);
                    }

                    Long entryId = (Long) r[1];
                    Long splitId = (Long) r[2];
                    Date effectiveOn = (Date) r[3];
                    String description = (String) r[4];
                    Long fromToAccountId = (Long) r[5];
                    String fromToAccountGroup = (String) r[6];
                    String fromToAccount = (String) r[7];
                    BigDecimal amount = (BigDecimal) r[8];

                    lines.add(
                            new EntryReportLine(
                                    description,
                                    new MonetaryAmount(account.getMonetaryUnit(), amount),
                                    new LedgerCoordinates(account.getId(), entryId, splitId),
                                    effectiveOn,
                                    fromToAccountId,
                                    fromToAccountGroup,
                                    fromToAccount
                            )
                    );
                    haveEntries = true;
                }
            }
            if (!haveEntries) {
                accountEntryLines.put(account, new ArrayList());
            }
        }
        return accountEntryLines;
    }

    public void persist(Entry entry) {

        getCurrentSession().saveOrUpdate(entry);
        getCurrentSession().flush();

        for (Split split : entry.getSplits()) {
            split.setEntryId(entry.getId());
            persist(split);
        }

        for (Split split : entry.getOrphanedSplits()) {
            delete(split);
        }
    }

    public void delete(Entry entry) {
        getCurrentSession().delete(entry);
    }

    public void persist(Split split) {
        getCurrentSession().saveOrUpdate(split);
    }

    public void delete(Split split) {
        getCurrentSession().delete(split);
        deleteOrphanedEntries();
    }

    public void deleteOrphanedEntries() {
        // Remove entries that now have no more splits
        getCurrentSession().createQuery(
                "delete from Entry e where not e.id in " +
                        "(select distinct(s.entryId) from Split s)"
        ).executeUpdate();
    }

}
