

import {
    ActionContribution,
    ActionItem,
    ContributableMenu,
    Contributions,
    MenuContributions,
    MenuItemContribution,
} from '@workbench-stack/extension-api';
import { ofType } from 'redux-observable';
import { IExtensionManifest, isErrorLike, IExecutableExtension } from '@workbench-stack/extension-api';
import { ConfigurationScope } from '@workbench-stack/core';
import { BehaviorSubject, Subscribable, Observable } from 'rxjs';
import * as Apollo from 'apollo-client';
import { TEMPLATE_BEGIN } from '../context/expr/lexer';
import { ComputedContext, evaluate, evaluateTemplate } from '../context/expr/evaluator';
import { distinctUntilChanged, map, switchMap, tap, catchError, mergeMap } from 'rxjs/operators';

/** Manages and executes contributions from all extensions. */
// export class ContributionRegistry {
//     /** All entries, including entries that are not enabled in the current context. */
//     private _entries = new BehaviorSubject<ContributionEntry[]>([]);

//     public constructor(
//         private model,
//         private configurationService: Pick<IConfigurationService, 'data'>,
//         private context: Subscribable<Context<any>>,
//     ) { }


// }

export const registerContributionEpic: (action$: Observable<any>, state$: Observable<any>, { apolloClient }: { apolloClient: Apollo.ApolloClient<any> }) => any
    = (action$, state$, { apolloClient }) => { }

export const getContributionsEpic: (action$: Observable<any>, state$: Observable<any>, { apolloClient }: { apolloClient: Apollo.ApolloClient<any> }) => any
    = (action$, state$, { apolloClient }) =>
        action$.pipe(
            ofType('REGISTER_EXTENSION_CONTRIBUTION'),
            mergeMap(() => state$.value.activeExtensions),
            map(extensions =>
                (extensions as IExecutableExtension[])
                    .map(({ manifest }) => manifest)
                    .filter((manifest): manifest is IExtensionManifest => manifest !== null && !isErrorLike(manifest))
                    .map(({ contributes }) => contributes)
                    .filter((contributions): contributions is Contributions => !!contributions),
            ),
        );


/**
 * Merges the contributions.
 *
 * Most callers should use ContributionRegistry#getContributions, which merges all registered
 * contributions.
 */
export function mergeContributions(contributions: Contributions[]): Contributions {
    if (contributions.length === 0) {
        return {};
    }
    if (contributions.length === 1) {
        return contributions[0];
    }
    const merged: Contributions = {};
    for (const c of contributions) {
        if (c.actions) {
            if (!merged.actions) {
                merged.actions = [...c.actions];
            } else {
                merged.actions = [...merged.actions, ...c.actions];
            }
        }
        if (c.menus) {
            if (!merged.menus) {
                merged.menus = { ...c.menus };
            } else {
                for (const [menu, items] of (Object as any).entries(c.menus) as [ContributableMenu, MenuItemContribution[]][]) {
                    if (!merged.menus[menu]) {
                        merged.menus[menu] = [...items];
                    } else {
                        merged.menus[menu] = [...merged.menus[menu]!, ...items];
                    }
                }
            }
        }
        if (c.searchFilters) {
            if (!merged.searchFilters) {
                merged.searchFilters = [...c.searchFilters];
            } else {
                merged.searchFilters = [...merged.searchFilters, ...c.searchFilters];
            }
        }
    }
    return merged;
}

/** Filters out items whose `when` context expression evaluates to false (or a falsey value). */
export function contextFilter<T extends { when?: string }>(
    context: ComputedContext,
    items: T[],
    evaluateExpr = evaluate,
): T[] {
    const keep: T[] = [];
    for (const item of items) {
        if (item.when !== undefined && !evaluateExpr(item.when, context)) {
            continue; // omit
        }
        keep.push(item);
    }
    return keep;
}

/** Filters the contributions to only those that are enabled in the current context. */
export function filterContributions(
    context: ComputedContext,
    contributions: Contributions,
    evaluateExpr = evaluate,
): Contributions {
    if (!contributions.menus) {
        return contributions;
    }
    const filteredMenus: MenuContributions = {};
    for (const [menu, items] of (Object as any).entries(contributions.menus) as [ContributableMenu, MenuItemContribution[]][]) {
        filteredMenus[menu] = contextFilter(context, items, evaluateExpr);
    }
    return { ...contributions, menus: filteredMenus };
}

const DEFAULT_TEMPLATE_EVALUATOR: {
    evaluateTemplate: (template: string, context: ComputedContext) => any

    /**
     * Reports whether the string needs evaluation. Skipping evaluation for strings where it is unnecessary is an
     * optimization.
     */
    needsEvaluation: (template: string) => boolean;
} = {
    evaluateTemplate,
    needsEvaluation: (template: string) => template.includes(TEMPLATE_BEGIN),
};

/**
 * Evaluates expressions in contribution definitions agains the given context.
 */
export function evaluateContributions(
    context: ComputedContext,
    contributions: Contributions,
    // tslint:disable-next-line:no-shadowed-variable
    { evaluateTemplate, needsEvaluation } = DEFAULT_TEMPLATE_EVALUATOR,
): Contributions {
    if (!contributions.actions || contributions.actions.length === 0) {
        return contributions;
    }
    const evaluatedActions: ActionContribution[] = [];
    for (const action of contributions.actions as Readonly<ActionContribution>[]) {
        const changed: Partial<ActionContribution> = {};
        if (action.commandArguments) {
            for (const [i, arg] of action.commandArguments.entries()) {
                if (typeof arg === 'string' && needsEvaluation(arg)) {
                    const evaluatedArg = evaluateTemplate(arg, context);
                    if (changed.commandArguments) {
                        changed.commandArguments.push(evaluatedArg);
                    } else {
                        changed.commandArguments = action.commandArguments.slice(0, i).concat(evaluatedArg);
                    }
                } else if (changed.commandArguments) {
                    changed.commandArguments.push(arg);
                }
            }
        }
        if (action.title && needsEvaluation(action.title)) {
            changed.title = evaluateTemplate(action.title, context);
        }
        if (action.category && needsEvaluation(action.category)) {
            changed.category = evaluateTemplate(action.category, context);
        }
        if (action.description && needsEvaluation(action.description)) {
            changed.description = evaluateTemplate(action.description, context);
        }
        if (action.iconURL && needsEvaluation(action.iconURL)) {
            changed.iconURL = evaluateTemplate(action.iconURL, context);
        }
        if (action.actionItem) {
            const changedActionItem: Partial<ActionItem> = {};
            if (action.actionItem.label && needsEvaluation(action.actionItem.label)) {
                changedActionItem.label = evaluateTemplate(action.actionItem.label, context);
            }
            if (action.actionItem.description && needsEvaluation(action.actionItem.description)) {
                changedActionItem.description = evaluateTemplate(action.actionItem.description, context);
            }
            if (action.actionItem.iconURL && needsEvaluation(action.actionItem.iconURL)) {
                changedActionItem.iconURL = evaluateTemplate(action.actionItem.iconURL, context);
            }
            if (action.actionItem.iconDescription && needsEvaluation(action.actionItem.iconDescription)) {
                changedActionItem.iconDescription = evaluateTemplate(action.actionItem.iconDescription, context);
            }
            if (Object.keys(changedActionItem).length !== 0) {
                changed.actionItem = { ...action.actionItem, ...changedActionItem };
            }
        }
        const modified = Object.keys(changed).length !== 0;
        evaluatedActions.push(modified ? { ...action, ...changed } : action);
    }
    return { ...contributions, actions: evaluatedActions };
}
