/**********************************************************************
 * Copyright (c) 2017 Ericsson
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License 2.0 which
 * accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 **********************************************************************/

package org.eclipse.tracecompass.internal.tmf.core.model.xy;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.internal.tmf.core.model.TmfXyResponseFactory;
import org.eclipse.tracecompass.internal.tmf.core.model.filters.FetchParametersUtils;
import org.eclipse.tracecompass.internal.tmf.core.model.tree.TmfTreeCompositeDataProvider;
import org.eclipse.tracecompass.tmf.core.dataprovider.DataProviderManager;
import org.eclipse.tracecompass.tmf.core.model.CommonStatusMessage;
import org.eclipse.tracecompass.tmf.core.model.filters.TimeQueryFilter;
import org.eclipse.tracecompass.tmf.core.model.tree.ITmfTreeDataModel;
import org.eclipse.tracecompass.tmf.core.model.xy.ISeriesModel;
import org.eclipse.tracecompass.tmf.core.model.xy.ITmfCommonXAxisModel;
import org.eclipse.tracecompass.tmf.core.model.xy.ITmfTreeXYDataProvider;
import org.eclipse.tracecompass.tmf.core.model.xy.ITmfXyModel;
import org.eclipse.tracecompass.tmf.core.model.xy.IYModel;
import org.eclipse.tracecompass.tmf.core.response.ITmfResponse;
import org.eclipse.tracecompass.tmf.core.response.TmfModelResponse;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;

/**
 * Represents a base implementation of {@link ITmfTreeXYDataProvider} that
 * supports experiments. Clients of this data provider must provide a list of
 * {@link ITmfTreeXYDataProvider} for each trace in the experiment which
 * supports the provider. From the list of sub data provider, this data provider
 * will merge all responses into one.
 *
 * @param <M>
 *            The type of {@link ITmfTreeDataModel} that this composite's tree
 *            provider must return.
 * @param <P>
 *            The type of {@link ITmfTreeXYDataProvider} that this composite
 *            must encapsulate
 * @author Yonni Chen
 * @since 4.0
 */
public class TmfTreeXYCompositeDataProvider<M extends ITmfTreeDataModel, P extends ITmfTreeXYDataProvider<M>>
        extends TmfTreeCompositeDataProvider<M, P> implements ITmfTreeXYDataProvider<M> {
    private final String fTitle;

    /**
     * Constructor
     *
     * @param providers
     *            A factory that creates a list of data provider. Each data provider
     *            should be associated to a different trace.
     * @param title
     *            Chart's title
     * @param id
     *            the provider's ID
     */
    public TmfTreeXYCompositeDataProvider(List<P> providers, String title, String id) {
        super(providers, id);
        fTitle = title;
    }

    /**
     * Return a composite {@link ITmfTreeXYDataProvider} from a list of traces.
     *
     * @param traces
     *            A list of traces from which to generate a provider.
     * @param title
     *            Chart's title
     * @param id
     *            the provider's ID
     * @return null if the non of the traces returns a provider, the provider if the
     *         lists only return one, else a {@link TmfTreeXYCompositeDataProvider}
     *         encapsulating the providers
     */
    public static @Nullable ITmfTreeXYDataProvider<ITmfTreeDataModel> create(Collection<ITmfTrace> traces, String title, String id) {
        return create(traces, title, id, null);
    }

    /**
     * Return a composite {@link ITmfTreeXYDataProvider} from a list of traces.
     *
     * @param traces
     *            A list of traces from which to generate a provider.
     * @param title
     *            Chart's title
     * @param id
     *            the provider's ID
     * @param secondaryId
     *            The provider's secondaryId
     * @return null if the non of the traces returns a provider, the provider if the
     *         lists only return one, else a {@link TmfTreeXYCompositeDataProvider}
     *         encapsulating the providers
     */
    public static @Nullable ITmfTreeXYDataProvider<ITmfTreeDataModel> create(Collection<ITmfTrace> traces, String title, String id, @Nullable String secondaryId) {
        String providerId = secondaryId == null ? id : id + ':' + secondaryId;
        List<@NonNull ITmfTreeXYDataProvider<ITmfTreeDataModel>> providers = new ArrayList<>();
        for (ITmfTrace child : traces) {
            ITmfTreeXYDataProvider<ITmfTreeDataModel> provider = DataProviderManager.getInstance().getDataProvider(child, providerId, ITmfTreeXYDataProvider.class);
            if (provider != null) {
                providers.add(provider);
            }
        }
        if (providers.isEmpty()) {
            return null;
        } else if (providers.size() == 1) {
            return providers.get(0);
        }
        return new TmfTreeXYCompositeDataProvider<>(providers, title, providerId);
    }

    @Override
    public TmfModelResponse<ITmfXyModel> fetchXY(Map<String, Object> fetchParameters, @Nullable IProgressMonitor monitor) {
        /**
         * <pre>
         * Response status according to the provider's reponse statuses:
         *
         * * Cancelled -> The monitor is cancelled
         * * Failed -> At least one provider has failed
         * * Running -> At least one of the providers is running
         * * Completed -> All providers have completed
         * </pre>
         */
        List<P> providers = getProviders();
        // Get all the responses
        Collection<TmfModelResponse<ITmfXyModel>> responses = getXyResponses(fetchParameters, monitor, providers);

        if (monitor != null && monitor.isCanceled()) {
            return TmfXyResponseFactory.createCancelledResponse(CommonStatusMessage.TASK_CANCELLED);
        }
        // If one response is failed, return a failed response with a concatenation of the messages
        String failedMsg = handleFailedStatus(responses);
        if (failedMsg != null) {
            return TmfXyResponseFactory.createFailedResponse(failedMsg);
        }

        boolean allCommon = Iterables.all(providers, ITmfCommonXAxisModel.class::isInstance);
        // The query is considered complete if all providers are completed
        boolean isComplete = Iterables.all(responses, response -> response.getStatus() == ITmfResponse.Status.COMPLETED);
        if (allCommon) {
            ImmutableList.Builder<IYModel> series = ImmutableList.builder();
            responses.forEach(response -> {
                ITmfCommonXAxisModel model = (ITmfCommonXAxisModel) response.getModel();
                if (model != null) {
                    series.addAll(model.getYSeriesData());
                }
            });
            TimeQueryFilter filter = FetchParametersUtils.createTimeQuery(fetchParameters);
            if (filter == null) {
                return TmfXyResponseFactory.createFailedResponse(CommonStatusMessage.INCORRECT_QUERY_PARAMETERS);
            }
            return TmfXyResponseFactory.create(fTitle, filter.getTimesRequested(), series.build(), isComplete);
        }
        ImmutableList.Builder<ISeriesModel> series = ImmutableList.builder();
        responses.forEach(response -> {
            ITmfXyModel model = response.getModel();
            if (model != null) {
                series.addAll(model.getSeriesData());
            }
        });
        return TmfXyResponseFactory.create(fTitle, series.build(), isComplete);
    }

    private static @Nullable String handleFailedStatus(Collection<TmfModelResponse<ITmfXyModel>> responses) {
        if (Iterables.any(responses, response -> response.getStatus() == ITmfResponse.Status.FAILED)) {
            // All requests have failed, return a concatenation of their errors
            return responses.stream().map(TmfModelResponse::getStatusMessage)
                    .collect(Collectors.joining("\n")); //$NON-NLS-1$
        }
        // At least one is good, return null
        return null;
    }

    private Collection<TmfModelResponse<ITmfXyModel>> getXyResponses(Map<String, Object> fetchParameters, @Nullable IProgressMonitor monitor, List<P> providers) {
        List<TmfModelResponse<ITmfXyModel>> responses = new ArrayList<>();
        for (P dataProvider : providers) {
            TmfModelResponse<ITmfXyModel> response = dataProvider.fetchXY(fetchParameters, monitor);
            responses.add(response);

            if (monitor != null && monitor.isCanceled()) {
                return responses;
            }
        }
        return responses;
    }

}
