import NumberFormatter from "./NumberFormatter";
import { compact, gt, gte, isEmpty, lte, map, range, replace, size, split, trim } from "lodash";
import {
    LINE_SPLITTER,
    MEMBER_INDEXES_PATTERN,
    MEMBER_NAMES_PATTERN,
    NOT_ARITHMETIC_CHARS_PATTERN,
    VALUE_SPLITTER
} from "../constants/patterns";
import { CalculatedMembers, Item, ItemsResult, Summary } from "../types/types";
import MembersParser from "./MembersParser";
import {splitLineText} from "./itemSplitter";

export default class ItemsParser {

    public static getItemsResult(exp: string): ItemsResult {
        return ItemsParser.calculateResultFromExpression(exp);
    }

    public static getExpItems(exp: string): Item[] {
        if (!isEmpty(trim(exp))) {
            return ItemsParser.getSplitItems(exp)
                .map(trim)
                .filter(l => l.match(/\d/g))
                .map((l, ind) => {
                    const members = MembersParser.getUserIndexes(l);
                    const cleanValue = trim(replace(l, MEMBER_INDEXES_PATTERN, ''));

                    return {
                        name: `Item ${ind + 1}`,
                        order: ind + 1,
                        members,
                        ...ItemsParser.getValueAndType(cleanValue, members),
                    };
                });
        }
        return [];
    }

    public static getTemplateItems(templateExp?: string): Item[] {
        if (templateExp && !isEmpty(trim(templateExp))) {
            // const error = getInvalidMessage(text);
            // if(error){
            //   throw new Error(error)
            // }
            templateExp = templateExp.replace(MEMBER_NAMES_PATTERN, '')

            if (!templateExp.match(/[a-zA-Z]/)) {
                return ItemsParser.getExpItems(templateExp);
            }

            return split(templateExp, LINE_SPLITTER)
                .map(trim)
                .map(splitLineText)
                .filter(split => {
                    return gte(size(split), 2);
                })
                .map(([name, value], ind): Item => {
                    const obj = ItemsParser.getMembersFromName(name);

                    return {
                        order: ind + 1,
                        ...obj,
                        ...ItemsParser.getValueAndType(value, obj.members),
                    };
                });
        }
        return [];
    }

    public static calculateResultFromExpression(exp?: string): ItemsResult {
        const items = ItemsParser.getTemplateItems(exp)
        const usernames = MembersParser.getUsernames(exp)
        const cache: CalculatedMembers = {
            membersMap: {},
            membersCount: size(usernames) || 0,
        };

        if (!cache.membersCount) {
            cache.membersCount = items.reduce((max, product) => {
                return Math.max(max, ...product.members, 1);
            }, 0);
            if (cache.membersCount > 20) cache.membersCount = 20;
        }
        items
            .map(p => ({
                product: p,
                pMembersCount: size(p.members),
            }))
            .filter(obj => obj.product.valueType === 'currency' && gt(obj.pMembersCount, 0))
            .forEach(obj => {
                obj.product.members.forEach(memberIndex => {
                    const bracketValueObj = Object.keys(obj.product.values || {}).length && obj.product.values?.[memberIndex] || undefined;
                    const pTotal = bracketValueObj?.total || NumberFormatter.round(obj.product.value / obj.pMembersCount);

                    if (!cache.membersMap[memberIndex]) {
                        cache.membersMap[memberIndex] = {
                            index: memberIndex,
                            total: 0,
                            items: [{ ...obj.product, value: pTotal }],
                            productNames: [],
                            totalExpression: '',
                        };
                    } else {
                        cache.membersMap[memberIndex].items?.push(obj.product);
                    }
                    cache.membersMap[memberIndex].total += pTotal;

                    if (cache.membersMap[memberIndex].totalExpression.length) {
                        cache.membersMap[memberIndex].totalExpression += ' + ';
                    }
                    cache.membersMap[memberIndex].totalExpression += pTotal;
                    cache.membersMap[memberIndex].productNames?.push(obj.product.name);
                    cache.membersMap[memberIndex].totalExpression += bracketValueObj?.total
                        ? `(${bracketValueObj.origin})`
                        : `(${obj.product.originValue}/${obj.pMembersCount})`;
                });
            });

        items //common products with currency valueType
            .filter(p => p.valueType === 'currency' && lte(size(p.members), 0))
            .forEach(product => {
                range(1, cache.membersCount + 1).forEach(memberIndex => {
                    if (!cache.membersMap[memberIndex]) {
                        cache.membersMap[memberIndex] = {
                            index: memberIndex,
                            total: 0,
                            items: [],
                            productNames: [],
                            totalExpression: '',
                        };
                    }

                    const pTotal = NumberFormatter.round(product.value / cache.membersCount);
                    cache.membersMap[memberIndex].items?.push({ ...product, value: pTotal });
                    cache.membersMap[memberIndex].total += pTotal;

                    if (cache.membersMap[memberIndex].totalExpression.length) {
                        cache.membersMap[memberIndex].totalExpression += ' + ';
                    }
                    cache.membersMap[memberIndex].totalExpression += pTotal;
                    cache.membersMap[memberIndex].productNames?.push(product.name);
                    cache.membersMap[memberIndex].totalExpression += `(${product.value}/${cache.membersCount})`;
                });
            });

        items //percentage valueType
            .filter(p => p.valueType === 'percentage' && lte(size(p.members), 0))
            .forEach((product, ind) => {
                let percentageTotal = 0;
                Object.keys(cache.membersMap)
                    .forEach(memberIndex => {
                        const oldTotal = cache.membersMap[memberIndex].total;
                        const pTotal = NumberFormatter.round((oldTotal * product.value) / 100);
                        cache.membersMap[memberIndex].items?.push({ ...product, value: pTotal });
                        cache.membersMap[memberIndex].total += pTotal;
                        percentageTotal += pTotal;

                        if (cache.membersMap[memberIndex].totalExpression.length) {
                            cache.membersMap[memberIndex].totalExpression += ' + ';
                        }
                        cache.membersMap[memberIndex].totalExpression += pTotal;
                        cache.membersMap[memberIndex].productNames?.push(product.name);
                        cache.membersMap[memberIndex].totalExpression += `(${oldTotal}*${product.value / 100})`;
                    });
                product.value = percentageTotal;
            });

        const summary = Object.keys(cache.membersMap)
            .map(ind => {
                delete cache.membersMap[ind].items;

                return cache.membersMap[ind];
            })
            .reduce(
                (obj, member) => {
                    obj.total += member.total;
                    obj.total = NumberFormatter.round(obj.total);
                    if (usernames.length >= member.index) {
                        member.username = usernames[member.index - 1];
                    }
                    obj.members.push(member);

                    return obj;
                },
                { membersCount: cache.membersCount, members: [], total: 0, inputText: exp } as Summary,
            );

        return {
            items,
            summary
        }
    }


    private static getValueAndType(valueStr: string, members: number[] = []): Pick<Item, 'value' | 'valueType' | 'originValue' | 'values' | 'quantity' | 'unitValue'> {
        const valueType = valueStr.includes('%') ? 'percentage' : 'currency';
        const value = NumberFormatter.evaluateAndRound(valueStr.replace(NOT_ARITHMETIC_CHARS_PATTERN, ''));
        const quantity = NumberFormatter.evaluateAndRound(valueStr.match(/([^*]+)\*.+/)?.[1] || '') || 1;
        const membersCount = size(members);
        const values: Item['values'] = {};
        if (membersCount > 0 && valueStr.includes('(')) {
            const arr = valueStr.split(/\)?\*/);
            if (arr.length > 1) {
                const bracketStr = arr[0].slice(1);
                if (bracketStr) {
                    const bracketNumbers = bracketStr.split('+');

                    members.forEach((m, ind) => {
                        values[m] = {
                            total: NumberFormatter.evaluateAndRound(bracketNumbers[ind]) * NumberFormatter.evaluateAndRound(arr[1]),
                            origin: `${NumberFormatter.evaluateAndRound(bracketNumbers[ind])}*${NumberFormatter.evaluateAndRound(arr[1])}`,
                        };
                    });
                }
            }
        }
        return {
            value,
            quantity: quantity || 1,
            unitValue: value / quantity,
            originValue: valueStr,
            values,
            valueType,
        };
    }

    private static getSplitItems(exp: string): string[] {
        const state = {
            bracketOpened: false,
            arr: [] as string[],
        };
        for (const char of exp) {
            if (char === '(') {
                state.bracketOpened = true;
            } else if (char === ')') {
                state.bracketOpened = false;
            }
            if (!state.bracketOpened && char === '+') {
                state.arr.push('');
                continue;
            }
            if (state.arr.length === 0) {
                state.arr.push(char);
            } else {
                state.arr[state.arr.length - 1] += char;
            }
        }
        return compact(state.arr);

    }

    private static getMembersFromName(label: string): Pick<Item, 'name' | 'members'> {
        const members = MembersParser.getUserIndexes(label)
        const cleanLabel = trim(replace(label, MEMBER_INDEXES_PATTERN, ''));
        return {
            members,
            name: cleanLabel,
        };
    }

}
