/*******************************************************************************
 * Copyright (c) 2009, 2015 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
 *
 * Contributors:
 *   Francois Chouinard - Initial API and implementation
 *   Alexandre Montplaisir - Merge with TmfCoalescedDataRequest
 *   Bernd Hufmann - Updated dispatching of events and added requests cache
 *******************************************************************************/

package org.eclipse.tracecompass.internal.tmf.core.request;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.tracecompass.internal.tmf.core.Activator;
import org.eclipse.tracecompass.internal.tmf.core.TmfCoreTracer;
import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
import org.eclipse.tracecompass.tmf.core.request.ITmfEventRequest;
import org.eclipse.tracecompass.tmf.core.request.TmfEventRequest;
import org.eclipse.tracecompass.tmf.core.timestamp.ITmfTimestamp;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimeRange;

/**
 * The TMF coalesced event request
 *
 * @author Francois Chouinard
 */
public class TmfCoalescedEventRequest extends TmfEventRequest {

    // ------------------------------------------------------------------------
    // Attributes
    // ------------------------------------------------------------------------

    /** The list of coalesced requests */
    private final List<ITmfEventRequest> fRequests = new ArrayList<>();

    /**
     * We do not use super.fRange, because in the case of coalesced requests,
     * the global range can be modified as sub-request are added.
     */
    private TmfTimeRange fRange;

    /**
     * The requests cache to avoid iterating over all requests for each event.
     */
    private Map<String, Set<ITmfEventRequest>> fRequestsCache = new HashMap<>();

    // ------------------------------------------------------------------------
    // Constructor
    // ------------------------------------------------------------------------

    /**
     * Request 'n' events of a given type for the given time range (given
     * priority). Events are returned in blocks of the given size.
     *
     * @param dataType
     *            The requested data type
     * @param range
     *            The range of the request. You can use
     *            {@link TmfTimeRange#ETERNITY} to request all events.
     * @param index
     *            The index of the first event to retrieve. Use '0' to start at
     *            the beginning.
     * @param nbRequested
     *            The number of events requested. You can use
     *            {@link TmfEventRequest#ALL_DATA} to request all events.
     * @param priority
     *            The requested execution priority
     * @param dependencyLevel
     *            The dependency level. Use 0 if no dependency with other
     *            requests.
     */
    public TmfCoalescedEventRequest(Class<? extends ITmfEvent> dataType,
            TmfTimeRange range,
            long index,
            int nbRequested,
            ExecutionType priority,
            int dependencyLevel) {
        super(ITmfEvent.class, null, index, nbRequested, priority, dependencyLevel);
        fRange = range;

        if (TmfCoreTracer.isRequestTraced()) {
            String type = getClass().getName();
            type = type.substring(type.lastIndexOf('.') + 1);
            @SuppressWarnings("nls")
            String message = "CREATED "
                    + (getExecType() == ITmfEventRequest.ExecutionType.BACKGROUND ? "(BG)" : "(FG)")
                    + " Type=" + type + " Index=" + getIndex() + " NbReq=" + getNbRequested()
                    + " Range=" + getRange()
                    + " DataType=" + getDataType().getSimpleName();
            TmfCoreTracer.traceRequest(getRequestId(), message);
        }
    }

    @Override
    public TmfTimeRange getRange() {
        return fRange;
    }

    // ------------------------------------------------------------------------
    // Management
    // ------------------------------------------------------------------------

    /**
     * Add a request to this one.
     *
     * @param request
     *            The request to add
     */
    public void addRequest(ITmfEventRequest request) {
        // If it is a coalesced request only add the sub-requests
        if (request instanceof TmfCoalescedEventRequest) {
            TmfCoalescedEventRequest otherRequest = (TmfCoalescedEventRequest)request;
            for (ITmfEventRequest subRequest : otherRequest.fRequests) {
                fRequests.add(subRequest);
                merge(subRequest);
            }
        } else {
            fRequests.add(request);
            merge(request);
        }
    }

    /**
     * Check if a request is compatible with the current coalesced one
     *
     * @param request
     *            The request to verify
     * @return If the request is compatible, true or false
     */
    public boolean isCompatible(ITmfEventRequest request) {
        return (request.getExecType() == getExecType() &&
                request.getDependencyLevel() == getDependencyLevel() &&
                        ranksOverlap(request) && timeRangesOverlap(request));

    }

    private boolean ranksOverlap(ITmfEventRequest request) {
        long start = request.getIndex();
        long end = start + request.getNbRequested();

        // Return true if either the start or end index falls within
        // the coalesced request boundaries
        return (start <= (fIndex + fNbRequested + 1) && (end >= fIndex - 1));
    }

    private boolean timeRangesOverlap(ITmfEventRequest request) {
        ITmfTimestamp startTime = request.getRange().getStartTime();
        ITmfTimestamp endTime = request.getRange().getEndTime();
        return (startTime.compareTo(endTime) <= 0) &&
                (fRange.getStartTime().compareTo(fRange.getEndTime()) <= 0);
    }

    private void merge(ITmfEventRequest request) {
        long start = request.getIndex();
        long end = Math.min(start + request.getNbRequested(), ITmfEventRequest.ALL_DATA);

        if (start < fIndex) {
            if (fNbRequested != ITmfEventRequest.ALL_DATA) {
                fNbRequested += (fIndex - start);
            }
            fIndex = start;
        }
        if ((request.getNbRequested() == ITmfEventRequest.ALL_DATA) ||
                (fNbRequested == ITmfEventRequest.ALL_DATA)) {
            fNbRequested = ITmfEventRequest.ALL_DATA;
        } else {
            fNbRequested = (int) Math.max(end - fIndex, fNbRequested);
        }

        ITmfTimestamp startTime = request.getRange().getStartTime();
        ITmfTimestamp endTime = request.getRange().getEndTime();
        if (!fRange.contains(startTime) && fRange.getStartTime().compareTo(startTime) > 0) {
            fRange = new TmfTimeRange(startTime, fRange.getEndTime());
        }
        if (!fRange.contains(endTime) && fRange.getEndTime().compareTo(endTime) < 0) {
            fRange = new TmfTimeRange(fRange.getStartTime(), endTime);
        }
    }

    /**
     * @return The list of IDs of the sub-requests
     */
    @SuppressWarnings("nls")
    public String getSubRequestIds() {
        StringBuffer result = new StringBuffer("[");
        for (int i = 0; i < fRequests.size(); i++) {
            if (i != 0) {
                result.append(", ");
            }
            result.append(fRequests.get(i).getRequestId());
        }
        result.append("]");
        return result.toString();
    }

    // ------------------------------------------------------------------------
    // ITmfEventRequest
    // ------------------------------------------------------------------------

    @Override
    public void handleData(ITmfEvent data) {
        super.handleData(data);

        long index = getIndex() + getNbRead() - 1;

        String traceName = data.getTrace().getName();
        Set<ITmfEventRequest> requests = fRequestsCache.get(traceName);

        if (requests == null) {
            // Populate requests cache
            requests = new HashSet<>();
            for (ITmfEventRequest myRequest : fRequests) {
                if (myRequest.getProviderFilter().matches(data)) {
                    requests.add(myRequest);
                }
            }
            fRequestsCache.put(traceName, requests);
        }

        // dispatch event to relevant requests
        for (ITmfEventRequest request : requests) {
            long start = request.getIndex();
            if (!request.isCompleted() && index >= start && request.getNbRead() < request.getNbRequested()) {
                ITmfTimestamp ts = data.getTimestamp();
                if (request.getRange().contains(ts)) {
                    if (request.getDataType().isInstance(data)) {
                        try {
                            request.handleData(data);
                        } catch (Exception e) {
                            /*
                             * We don't usually catch all exception, but here it
                             * is important because this will cause the request
                             * thread to hang forever and the other requests to
                             * be stopped. This should properly cancel the
                             * request with the exception and let the rest
                             * continue.
                             */
                            Activator.logError("An uncaught exception happened on request " + request + ": " + e.getMessage());  //$NON-NLS-1$//$NON-NLS-2$
                            request.fail(e);
                        }
                    }
                }
            }
        }
    }

    @Override
    public synchronized void start() {
        for (ITmfEventRequest request : fRequests) {
            if (!request.isCompleted()) {
                request.start();
            }
        }
        super.start();
    }

    @Override
    public synchronized void done() {
        for (ITmfEventRequest request : fRequests) {
            if (!request.isCompleted()) {
                request.done();
            }
        }
        super.done();
    }

    @Override
    public void fail(Exception e) {
        for (ITmfEventRequest request : fRequests) {
            request.fail(e);
        }
        super.fail(e);
    }

    @Override
    public void cancel() {
        for (ITmfEventRequest request : fRequests) {
            if (!request.isCompleted()) {
                request.cancel();
            }
        }
        super.cancel();
    }

    @Override
    public synchronized boolean isCompleted() {
        // Firstly, check if coalescing request is completed
        if (super.isCompleted()) {
            return true;
        }

        // Secondly, check if all sub-requests are finished
        if (!fRequests.isEmpty()) {
            // If all sub requests are completed the coalesced request is
            // treated as completed, too.
            for (ITmfEventRequest request : fRequests) {
                if (!request.isCompleted()) {
                    return false;
                }
            }
            return true;
        }

        // Coalescing request is not finished if there are no sub-requests
        return false;
    }

    @Override
    public synchronized boolean isCancelled() {
        // Firstly, check if coalescing request is canceled
        if (super.isCancelled()) {
            return true;
        }

        // Secondly, check if all sub-requests are canceled
        if (!fRequests.isEmpty()) {
            // If all sub requests are canceled the coalesced request is
            // treated as completed, too.
            for (ITmfEventRequest request : fRequests) {
                if (!request.isCancelled()) {
                    return false;
                }
            }
            return true;
        }

        // Coalescing request is not canceled if there are no sub-requests
        return false;

    }

    // ------------------------------------------------------------------------
    // Object
    // ------------------------------------------------------------------------

    @Override
    @SuppressWarnings("nls")
    public String toString() {
        return "[TmfCoalescedEventRequest(" + getRequestId() + "," + getDataType().getSimpleName()
                + "," + getExecType() + "," + getRange() + "," + getIndex() + "," + getNbRequested()
                + ", " + fRequests.toString() + ")]";
    }

}
