// *****************************************************************************
// Copyright (C) 2019 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

/* eslint-disable @typescript-eslint/no-explicit-any */

import { inject, injectable, postConstruct, named } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { UserPreferenceProvider, UserPreferenceProviderFactory } from '../common/user-preference-provider';
import { PreferenceProviderImpl, PreferenceConfigurations, PreferenceResolveResult, PreferenceUtils, ILogger } from '@theia/core';

export const UserStorageLocationProvider = Symbol('UserStorageLocationProvider');

/**
 * Binds together preference section prefs providers for user-level preferences.
 */
@injectable()
export class UserConfigsPreferenceProvider extends PreferenceProviderImpl {

    @inject(UserPreferenceProviderFactory)
    protected readonly providerFactory: UserPreferenceProviderFactory;

    @inject(UserStorageLocationProvider)
    private userStorageLocationProvider: () => Promise<URI>;

    @inject(PreferenceConfigurations)
    protected readonly configurations: PreferenceConfigurations;

    @inject(ILogger) @named('preferences:UserConfigsPreferenceProvider')
    protected readonly logger: ILogger;

    protected readonly providers = new Map<string, UserPreferenceProvider>();

    @postConstruct()
    protected init(): void {
        this.doInit();
    }

    protected async doInit(): Promise<void> {
        const userStorageUri = await this.userStorageLocationProvider();
        this.createProviders(userStorageUri);

        const readyPromises: Promise<void>[] = [];
        for (const provider of this.providers.values()) {
            readyPromises.push(provider.ready.catch(e => this.logger.error(e)));
        }
        Promise.all(readyPromises).then(() => this._ready.resolve());
    }

    protected createProviders(userStorageLocation: URI): void {
        for (const configName of [...this.configurations.getSectionNames(), this.configurations.getConfigName()]) {
            const sectionUri = userStorageLocation.resolve(configName + '.json');
            const sectionKey = sectionUri.toString();
            if (!this.providers.has(sectionKey)) {
                const provider = this.createProvider(sectionUri, configName);
                this.providers.set(sectionKey, provider);
            }
        }
    }

    override getConfigUri(resourceUri?: string, sectionName: string = this.configurations.getConfigName()): URI | undefined {
        for (const provider of this.providers.values()) {
            const configUri = provider.getConfigUri(resourceUri);
            if (configUri && this.configurations.getName(configUri) === sectionName) {
                return configUri;
            }
        }
        return undefined;
    }

    override resolve<T>(preferenceName: string, resourceUri?: string): PreferenceResolveResult<T> {
        const result: PreferenceResolveResult<T> = {};
        for (const provider of this.providers.values()) {
            const { value, configUri } = provider.resolve<T>(preferenceName, resourceUri);
            if (configUri && value !== undefined) {
                result.configUri = configUri;
                result.value = PreferenceUtils.merge(result.value as any, value as any) as any;
            }
        }
        return result;
    }

    getPreferences(resourceUri?: string): { [p: string]: any } {
        let result = {};
        for (const provider of this.providers.values()) {
            const preferences = provider.getPreferences();
            result = PreferenceUtils.merge(result, preferences) as any;
        }
        return result;
    }

    async setPreference(preferenceName: string, value: any, resourceUri?: string): Promise<boolean> {
        const sectionName = preferenceName.split('.', 1)[0];
        const defaultConfigName = this.configurations.getConfigName();
        const configName = this.configurations.isSectionName(sectionName) ? sectionName : defaultConfigName;

        const setWithConfigName = async (name: string): Promise<boolean> => {
            for (const provider of this.providers.values()) {
                if (this.configurations.getName(provider.getConfigUri()) === name) {
                    if (await provider.setPreference(preferenceName, value, resourceUri)) {
                        return true;
                    }
                }
            }
            return false;
        };

        if (await setWithConfigName(configName)) { // Try in the section we believe it belongs in.
            return true;
        } else if (configName !== defaultConfigName) { // Fall back to `settings.json` if that fails.
            return setWithConfigName(defaultConfigName);
        }
        return false;
    }

    protected createProvider(uri: URI, sectionName: string): UserPreferenceProvider {
        const provider = this.providerFactory(uri, sectionName);
        this.toDispose.push(provider);
        this.toDispose.push(provider.onDidPreferencesChanged(change => this.onDidPreferencesChangedEmitter.fire(change)));
        return provider;
    }
}
