// Imports
import java.text.SimpleDateFormat
import java.util.Date;
import java.util.List;
import java.util.Map
import java.util.Map.Entry;
import java.util.stream.Collectors

import com.gridnine.xtrip.common.l10n.model.LocaleHelper
import com.gridnine.xtrip.common.model.EntityReference
import com.gridnine.xtrip.common.model.dict.ContractType
import com.gridnine.xtrip.common.model.dict.CurrencyInfo
import com.gridnine.xtrip.common.model.dict.DictionaryReference
import com.gridnine.xtrip.common.model.entity.EntityStorage
import com.gridnine.xtrip.common.model.entity.parameters.EntityStorageActualizeParameters;
import com.gridnine.xtrip.common.model.finance.ChartOfAccountsElementType
import com.gridnine.xtrip.common.model.finance.DimensionType
import com.gridnine.xtrip.common.model.helpers.BalanceHelper
import com.gridnine.xtrip.common.model.helpers.FinanceHelper
import com.gridnine.xtrip.common.model.helpers.ProfileHelper
import com.gridnine.xtrip.common.model.helpers.ProjectionQueryHelper.GroupValues;
import com.gridnine.xtrip.common.model.profile.ContractCustomerIndex
import com.gridnine.xtrip.common.model.profile.Organization
import com.gridnine.xtrip.common.util.MiscUtil
import com.gridnine.xtrip.common.util.TextUtil
import com.gridnine.xtrip.common.util.MiscUtil.Pair

// Styles
createStyle(name: 'title', h_alignment: 'CENTER', v_alignment: 'CENTER')
createStyle(name: 'titleH1', fontHeight: 14, parent: 'title')
createStyle(name: 'titleH2', fontHeight: 9, parent: 'title')
createStyle(name: 'titleH3', fontHeight: 7, parent: 'title')
createStyle(name: 'header', h_alignment: 'CENTER', v_alignment: 'CENTER', fontHeight: 7)
createStyle(name: 'columnHeader', wrapText: true, parent: 'header')
createStyle(name: 'rowHeader', wrapText: true, parent: 'header')
createStyle(name: 'data', h_alignment: 'CENTER', v_alignment: 'CENTER', fontHeight: 7)
createStyle(name: 'dataText', parent: 'data')
createStyle(name: 'dataDate', format: 'm/d/yy', parent: 'data')
createStyle(name: 'dataNumber', h_alignment: 'RIGHT', format: '#,##0.00', parent: 'data')
createStyle(name: 'ahl', h_alignment: 'LEFT')
createStyle(name: 'ahc', h_alignment: 'CENTER')
createStyle(name: 'ahr', h_alignment: 'RIGHT')
createStyle(name: 'avt', v_alignment: 'TOP')
createStyle(name: 'avc', v_alignment: 'CENTER')
createStyle(name: 'avb', v_alignment: 'BOTTOM')
createStyle(name: 'aac', h_alignment: 'CENTER', v_alignment: 'CENTER')
createStyle(name: 'bold', fontBold: true)
createStyle(name: 'italic', fontItalic: true)
createStyle(name: 'bt', topBorder: 'THIN')
createStyle(name: 'bl', leftBorder: 'THIN')
createStyle(name: 'bb', bottomBorder: 'THIN')
createStyle(name: 'br', rightBorder: 'THIN')
createStyle(name: 'ba', topBorder: 'THIN', leftBorder: 'THIN', bottomBorder: 'THIN', rightBorder: 'THIN')
createStyle(name: 'grey25', foreground: 'GREY_25_PERCENT')
createStyle(name: 'locked', locked: true)

// Properties
def dateParameter = parameters['date'];
def agencyParameter = parameters['agency'];
def periodsParameter = parameters['periods'];
def groupsParameter = parameters['groups'];

// Closures
def agency = {

    def agency = EntityStorage.get().resolve(agencyParameter)?.entity;
    return agency ? ProfileHelper.getFullName(agency, LocaleHelper.getLocale('ru', 'RU'), false) : 'Не указано';
}

def date = {

    def format = new SimpleDateFormat('dd.MM.yy')
    return dateParameter ? format.format(dateParameter) : format.format(new Date())
}

// Functions
int countValues(map) {

    map.values().inject(0) { count, item -> count + (item instanceof Map ? countValues(item) : 1) }
}

Object getValues(operationDate, calculationDate, periods, groups, currency, contractType, contractor, dimensions, dimensionValues, index = 0) {

    def demensionType = groups[index]

    if(index < groups.size()) {

        def values = new TreeMap({a, b -> MiscUtil.compare(a.toString(), b.toString())})

        for(String dimensionValueItem : dimensionValues.values(groups[index], false)) {

            Object dimensionValue = dimensionValueItem ? FinanceHelper.getDimensionObject(demensionType, dimensionValueItem) : null

            dimensions[demensionType] = dimensionValue

            def dimensionValuesGroupByDimension = dimensionValues.groupBy(demensionType, dimensionValueItem, false)

            def childValues = getValues(operationDate, calculationDate, periods, groups, currency, contractType, contractor, dimensions, dimensionValuesGroupByDimension, index + 1)

            if(childValues) {
                values[dimensionValue] = childValues
            }
        }

        return values

    } else {

        def debit = MiscUtil.nonNull(BalanceHelper.calculateBalance(contractor, operationDate, calculationDate, ChartOfAccountsElementType.SUPPLIER, currency, dimensions, false))
        def credit = MiscUtil.nonNull(BalanceHelper.calculateBalance(contractor, operationDate, calculationDate, ChartOfAccountsElementType.SUPPLIER, currency, dimensions, true))

        def debt = MiscUtil.maximum(MiscUtil.sum(credit, MiscUtil.negate(debit)), BigDecimal.ZERO)

        if(debt.compareTo(BigDecimal.ZERO) != 0) {

            def deltas = [] as List

            def lastPeriodDebt = debt

            for(int i = 0; i < periods.size(); i++) {

                def period = periods.get(i)

                if(lastPeriodDebt.compareTo(BigDecimal.ZERO) > 0) {

                    def periodDate = MiscUtil.getBeforeTime(MiscUtil.clearTime(MiscUtil.addToDate(operationDate, -period.intValue(), Calendar.DAY_OF_MONTH)))
                    def periodCredit = MiscUtil.nonNull(BalanceHelper.calculateBalance(contractor, periodDate, calculationDate, ChartOfAccountsElementType.SUPPLIER, currency, dimensions, true))

                    def periodDebt = MiscUtil.maximum(MiscUtil.sum(periodCredit, MiscUtil.negate(debit)), BigDecimal.ZERO)
                    def delta = MiscUtil.sum(lastPeriodDebt, MiscUtil.negate(periodDebt))

                    lastPeriodDebt = periodDebt

                    deltas.add(delta)

                } else {
                    deltas.add(BigDecimal.ZERO)
                }

                if(i == periods.size() - 1) {
                    deltas.add(lastPeriodDebt)
                }
            }

            return new Pair(debt, deltas)

        } else {

            return null;
        }
    }
}

void printGroups(map, offset = 2) {

    for(int i = 0; i < map.entrySet().size(); i++) {

        def entry = map.entrySet()[i]

        def key = entry.key
        def value = entry.value

        if(i > 0) {
            offset.times{nextColumn()}
        }

        if(value instanceof Map) {

            text(key ? key.toString() : null, 'dataText|ba', 1, countValues(value))
            nextColumn()

            printGroups(value, offset + 1)

        } else {

            text(key ? key.toString() : null, 'dataText|ba')
            nextColumn()

            printDebt(value)
        }
    }
}

void printDebt(pair) {

    def debt = pair.getFirst()
    def deltas = pair.getSecond()

    number(debt, 'dataNumber|ba')
    nextColumn()

    for(BigDecimal delta : deltas) {

        number(delta, 'dataNumber|ba')
        nextColumn()
    }

    nextRow()
}

def fakeAgency = new EntityReference<Organization>(null, Organization.class, null)

def currentDate = new Date()

def operationDate = dateParameter ? dateParameter : currentDate
def calculationDate = currentDate

def groups = groupsParameter ? groupsParameter : Collections.emptyList()
def periods = periodsParameter ? Arrays.asList(periodsParameter.split("\\D+")).stream().filter({item -> !TextUtil.isBlank(item)}).map({item -> Integer.valueOf(item)}).distinct().sorted().collect(Collectors.toList()) : Collections.emptyList()

def debts = new TreeMap({a, b -> MiscUtil.compare(a.toString(), b.toString())})

// Initialization
for(ContractType contractType : Arrays.asList(ContractType.SUBAGENCY, ContractType.CLIENT)) {

    def contractValues = ProfileHelper.getContractValues(agencyParameter ? agencyParameter : fakeAgency, null, Collections.singleton(contractType), operationDate, new HashSet<>(Arrays.asList(ContractCustomerIndex.Property.paymentCurrency, ContractCustomerIndex.Property.customer)))

    for(String currencyValue : contractValues.values(ContractCustomerIndex.Property.paymentCurrency)) {

        DictionaryReference<CurrencyInfo> currency = TextUtil.nonBlank(currencyValue) ? FinanceHelper.getCurrency(currencyValue) : FinanceHelper.getDefaultCurrency()

        def contractValuesGroupByCurrency = contractValues.groupBy(ContractCustomerIndex.Property.paymentCurrency, currencyValue)

        for(String customerUidValue : contractValuesGroupByCurrency.values(ContractCustomerIndex.Property.customer)) {

            EntityReference<Organization> contractor = new EntityReference<Organization>(customerUidValue, Organization.class, null);

            EntityStorage.get().actualize(contractor, new EntityStorageActualizeParameters().useRemoteCallIfNecessary(true));

            def dimensions = [(DimensionType.ORGANIZATION) : agencyParameter ? agencyParameter : fakeAgency]

            def dimensionValues = BalanceHelper.getDimensionsValues(contractor, operationDate, calculationDate, ChartOfAccountsElementType.SUPPLIER, currency, dimensions, groups)

            def dimensionDebts = getValues(operationDate, calculationDate, periods, groups, currency, contractType, contractor, dimensions, dimensionValues)

            if(dimensionDebts) {

                if(!debts[currency]) {
                    debts[currency] = new TreeMap({a, b -> MiscUtil.compare(a.toString(), b.toString())})
                }

                if(!debts[currency][contractType]) {
                    debts[currency][contractType] = new TreeMap({a, b -> MiscUtil.compare(a.toString(), b.toString())})
                }

                debts[currency][contractType][contractor] = dimensionDebts
            }
        }
    }
}

def currencies = debts.keySet()

for(DictionaryReference<CurrencyInfo> currency : currencies ? currencies : [null]) {

    page { currency ? currency.code : 'EMPTY' } {

        // Set landcape mode
        landscape(true)

        // Set narrow margins
        margin(0.25, 0.25, 0.75, 0.75)

        def columnsCount = 2 + groups.size() + 1 + (periods.size() > 0 ? periods.size() + 1 : 0)

        // Report header
        text('Отчет по дебиторской задолженности', 'titleH1|ahl|bold', columnsCount, 1)

        // Column widths
        columnWidth(12)
        nextColumn()
        columnWidth(16)
        nextColumn()

        for(int i = 0; i < groups.size(); i++) {

            columnWidth(16)
            nextColumn()
        }

        columnWidth(16)
        nextColumn()

        for(int i = 0; i < periods.size(); i++) {

            columnWidth(12)
            nextColumn()

            if(i == periods.size() - 1) {

                columnWidth(12)
                nextColumn()
            }
        }

        2.times{nextRow()}

        text('Агентство', 'titleH2|ahr|bold')
        nextColumn()
        text(agency(), 'titleH2|ahl|bold')
        nextRow()
        text('Дата', 'titleH2|ahr|bold')
        nextColumn()
        text(date(), 'titleH2|ahl|bold')
        nextRow()
        text('Валюта', 'titleH2|ahr|bold')
        nextColumn()
        text(currency ? currency.code : '?', 'titleH2|ahl|bold')
        2.times{nextRow()}

        // Table header (first row)
        rowHeight(24, false)
        text('Контрагент', 'columnHeader|ba', 2, 2)
        2.times{nextColumn()}

        for(int i = 0; i < groups.size(); i++) {

            text(groups[i].toString(), 'columnHeader|ba', 1, 2)
            nextColumn()
        }

        text('Общая дебиторская задолженность', 'columnHeader|ba', 1, 2)
        nextColumn()

        if(periods.size() > 0 ) {

            text('В том числе по срокам наступления долга', 'columnHeader|ba', periods.size() + 1, 1)
            nextColumn()
        }

        nextRow()

        // Table header (second row)
        rowHeight(24, false)
        (2 + groups.size() + 1).times{nextColumn()}

        def periodStart = null
        def periodEnd = null

        for(int i = 0; i < periods.size(); i++) {

            periodStart = String.valueOf(periods.get(Math.max(i - 1, 0)))
            periodEnd = String.valueOf(periods.get(i))

            if(i == 0) {
                text("Не более ${periodEnd} дней", 'columnHeader|ba')
            } else {
                text("От ${periodStart} до ${periodEnd} дней", 'columnHeader|ba')
            }

            nextColumn()

            if(i == periods.size() - 1) {

                text("Не менее ${periodEnd} дней", 'columnHeader|ba')
                nextColumn()
            }
        }

        nextRow()

        if(currency) {

            // Table
            def contractTypes = debts[currency].keySet()

            for(ContractType contractType : contractTypes) {

                def contractors = debts[currency][contractType].keySet()

                text(contractType.toString(), 'dataText|ba', columnsCount, 1)
                nextRow()

                for(EntityReference<Organization> contractor : contractors) {

                    if(groups.size() > 0) {

                        def count = countValues(debts[currency][contractType][contractor])

                        text(contractor.toString(), 'dataText|ahl|ba', 2, groups.size() > 0 ? count + 1 : 1)
                        2.times{nextColumn()}

                        printGroups(debts[currency][contractType][contractor])

                        2.times{nextColumn()}
                        text('Итого', 'dataText|ahr|bold|italic|ba', groups.size(), 1)
                        groups.size().times{nextColumn()}

                        (1 + (periods.size() > 0 ? periods.size() + 1 : 0)).times{

                            formula("SUM(${cellIndex(-count, 0)}:${cellIndex(-1, 0)})", 'dataNumber|bold|italic|ba')
                            nextColumn()
                        }

                    } else {

                        text(contractor.toString(), 'dataText|ahl|ba', 2, 1)
                        2.times{nextColumn()}

                        printDebt(debts[currency][contractType][contractor])
                    }

                    nextRow()
                }
            }

        } else {

            // Table
            text('нет данных', 'dataText|ba', columnsCount, 1)
            nextRow()
        }
    }
}
