/*******************************************************************************
 * Copyright (c) 2017, 2018 École Polytechnique de Montréal
 *
 * 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.analysis.profiling.core.callstack;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;

import org.apache.commons.lang3.StringUtils;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.analysis.profiling.core.base.IProfilingElement;
import org.eclipse.tracecompass.internal.analysis.profiling.core.callstack.InstrumentedProfilingElement;
import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem;
import org.eclipse.tracecompass.statesystem.core.exceptions.StateSystemDisposedException;
import org.eclipse.tracecompass.statesystem.core.interval.ITmfStateInterval;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;

/**
 * Contains interfaces and classes to resolve the host ID in instrumented
 * callstack analyses.
 *
 * @author Geneviève Bastien
 * @since 1.1
 */
public final class CallStackHostUtils {

    private CallStackHostUtils() {
        // Nothing to do
    }

    /**
     * Interface for classes that provide a host ID at time t for a callstack. The
     * host ID is used to identify the machine on which the callstack was taken and
     * can be correlated with the model of a system to obtain additional data.
     */
    public interface IHostIdProvider extends Function<Long, @NonNull String> {

    }

    /**
     * The host ID is always the same, so return that string
     */
    private static class TraceHostIdProvider implements IHostIdProvider {

        private final String fHostId;

        public TraceHostIdProvider(String hostId) {
            fHostId = hostId;
        }

        @Override
        public @NonNull String apply(Long time) {
            return fHostId;
        }

    }

    /**
     * This class uses the value of an attribute as a host ID.
     */
    private static final class AttributeValueHostProvider implements IHostIdProvider {

        private final ITmfStateSystem fSs;
        private final int fQuark;
        private @Nullable ITmfStateInterval fInterval;
        private String fLastHostId = StringUtils.EMPTY;

        public AttributeValueHostProvider(ITmfStateSystem ss, int quark) {
            fSs = ss;
            fQuark = quark;
        }

        @Override
        public @NonNull String apply(Long time) {
            ITmfStateInterval interval = fInterval;
            String hostId = fLastHostId;
            if (interval != null && interval.intersects(time)) {
                return hostId;
            }
            try {
                interval = fSs.querySingleState(time, fQuark);
                hostId = String.valueOf(interval.getValue());
            } catch (StateSystemDisposedException e) {
                interval = null;
                hostId = StringUtils.EMPTY;
            }
            fInterval = interval;
            fLastHostId = hostId;
            return hostId;
        }

    }

    /**
     * This class uses the value of an attribute as a host ID.
     */
    private static final class AttributeNameHostProvider implements IHostIdProvider {

        private final String fHostId;

        public AttributeNameHostProvider(ITmfStateSystem ss, int quark) {
            String host = StringUtils.EMPTY;
            try {
                host = ss.getAttributeName(quark);
            } catch (IndexOutOfBoundsException e) {

            }
            fHostId = host;
        }

        @Override
        public @NonNull String apply(Long time) {
            return fHostId;
        }

    }

    /**
     * Interface for describing how a callstack will get the host ID, it will return
     * the host ID provider for a callstack element
     */
    public interface IHostIdResolver extends Function<IProfilingElement, @NonNull IHostIdProvider> {

    }

    /**
     * A host ID resolver that gets the host ID from the trace
     */
    public static final class TraceHostIdResolver implements IHostIdResolver {

        private final String fHostId;

        /**
         * @param trace
         *            The trace to use to provide the host ID
         */
        public TraceHostIdResolver(@Nullable ITmfTrace trace) {
            fHostId = trace == null ? StringUtils.EMPTY : trace.getHostId();
        }

        @Override
        public @NonNull IHostIdProvider apply(IProfilingElement element) {
            return new TraceHostIdProvider(fHostId);
        }

    }

    /**
     * This class will resolve the host ID provider by the value of a attribute at
     * a given depth
     */
    public static final class AttributeValueHostResolver implements IHostIdResolver {

        private int fLevel;

        /**
         * Constructor
         *
         * @param level
         *            The depth of the element whose value will be used to retrieve the
         *            host ID
         */
        public AttributeValueHostResolver(int level) {
            fLevel = level;
        }

        @Override
        public @NonNull IHostIdProvider apply(IProfilingElement element) {
            if (!(element instanceof InstrumentedProfilingElement)) {
                throw new IllegalArgumentException();
            }
            InstrumentedProfilingElement insElement = (InstrumentedProfilingElement) element;

            List<InstrumentedProfilingElement> elements = new ArrayList<>();
            InstrumentedProfilingElement el = insElement;
            while (el != null) {
                elements.add(el);
                el = el.getParentElement();
            }
            Collections.reverse(elements);
            if (elements.size() <= fLevel) {
                throw new NullPointerException("The host should never resolve to null, level " + fLevel + " is not available"); //$NON-NLS-1$ //$NON-NLS-2$
            }
            InstrumentedProfilingElement stackElement = elements.get(fLevel);
            return new AttributeValueHostProvider(stackElement.getStateSystem(), stackElement.getQuark());
        }

    }

    /**
     * This class will resolve the thread ID provider by the value of a attribute at
     * a given depth
     */
    public static final class AttributeNameHostResolver implements IHostIdResolver {

        private int fLevel;

        /**
         * Constructor
         *
         * @param level
         *            The depth of the element whose value will be used to retrieve the
         *            thread ID
         */
        public AttributeNameHostResolver(int level) {
            fLevel = level;
        }

        @Override
        public @NonNull IHostIdProvider apply(IProfilingElement element) {
            if (!(element instanceof InstrumentedProfilingElement)) {
                throw new IllegalArgumentException();
            }
            InstrumentedProfilingElement insElement = (InstrumentedProfilingElement) element;

            List<InstrumentedProfilingElement> elements = new ArrayList<>();
            InstrumentedProfilingElement el = insElement;
            while (el != null) {
                elements.add(el);
                el = el.getParentElement();
            }
            Collections.reverse(elements);
            if (elements.size() <= fLevel) {
                throw new NullPointerException("The host should never resolve to null, level " + fLevel + " is not available"); //$NON-NLS-1$ //$NON-NLS-2$
            }
            InstrumentedProfilingElement stackElement = elements.get(fLevel);
            return new AttributeNameHostProvider(stackElement.getStateSystem(), stackElement.getQuark());
        }

    }

}
