/*******************************************************************************
 * Copyright (c) 2016, 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.tmf.ui.swtbot.tests.perf.views;

import static org.junit.Assert.assertNotNull;

import java.io.File;
import java.util.Collection;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.swtbot.eclipse.finder.SWTWorkbenchBot;
import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotView;
import org.eclipse.swtbot.swt.finder.junit.SWTBotJunit4ClassRunner;
import org.eclipse.swtbot.swt.finder.utils.SWTBotPreferences;
import org.eclipse.swtbot.swt.finder.waits.Conditions;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotShell;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotText;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotTreeItem;
import org.eclipse.test.performance.Dimension;
import org.eclipse.test.performance.Performance;
import org.eclipse.test.performance.PerformanceMeter;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager;
import org.eclipse.tracecompass.tmf.core.signal.TmfWindowRangeUpdatedSignal;
import org.eclipse.tracecompass.tmf.core.timestamp.ITmfTimestamp;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimeRange;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimestamp;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.core.trace.TmfTraceManager;
import org.eclipse.tracecompass.tmf.ui.swtbot.tests.shared.ConditionHelpers;
import org.eclipse.tracecompass.tmf.ui.swtbot.tests.shared.SWTBotUtils;
import org.eclipse.tracecompass.tmf.ui.tests.shared.WaitUtils;
import org.eclipse.tracecompass.tmf.ui.views.xychart.TmfChartView;
import org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView;
import org.eclipse.ui.IWorkbenchPart;
import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;

/**
 * Base class to test the responsiveness of any views. The main method of this
 * class, {@link #runTestWithTrace(String, String, Collection)}, will receive a
 * collection of view IDs. For each view, the trace under test will be navigated
 * when only the view is opened (closing all other listed views, all other
 * opened views will remain opened) and also when all the views are opened
 * simultaneously. In this last case, it will rename the trace so that events
 * from when all the views are opened can be listed separately.
 *
 * @author Geneviève Bastien
 */
@RunWith(SWTBotJunit4ClassRunner.class)
public abstract class ViewsResponseTest {

    private static final String PROJECT_NAME = "test";

    private final @NonNull SWTWorkbenchBot fBot = new SWTWorkbenchBot();

    /**
     * Specific tests will prepare the workspace for the run. For example,
     * concrete classes can open perspectives, open views, prepare the layout,
     * etc.
     */
    protected abstract void prepareWorkspace();

    /**
     * Things to setup
     */
    @Before
    public void beforeClass() {

        SWTBotUtils.initialize();
        Thread.currentThread().setName("SWTBotTest");
        /* set up for swtbot */
        SWTBotPreferences.TIMEOUT = 60000; /* 60 second timeout */
        SWTBotPreferences.KEYBOARD_LAYOUT = "EN_US";
        SWTWorkbenchBot bot = new SWTWorkbenchBot();
        SWTBotUtils.closeView("welcome", bot);
        /* Prepare the workspace */
        prepareWorkspace();
        /* Finish waiting for eclipse to load */
        WaitUtils.waitForJobs();

        /* Create project */
        SWTBotUtils.createProject(PROJECT_NAME);
    }

    /**
     * Deletes the project from the workspace
     */
    @After
    public void cleanUp() {
        /* Close editors and delete project */
        fBot.closeAllEditors();
        SWTBotUtils.deleteProject(PROJECT_NAME, fBot);
    }

    private void closeAllViews(Collection<String> viewIDs) {
        viewIDs.forEach(id -> {
            SWTBotUtils.openView(id);
            SWTBotUtils.closeViewById(id, fBot);
        });
    }

    /**
     * This method will be run when all views are still close, but the trace has
     * been opened. For instance, if any analysis module need to have completed
     * before the test, it can wait for completion here.
     *
     * @param trace
     *            The trace used for this test
     */
    protected abstract void beforeRunningTest(ITmfTrace trace);

    /**
     * Run this swtbot with the trace specified at the specified path. The trace
     * will be navigate for each view ID separately, then, after renaming the
     * trace, with all the views opened. After this test, all views will be
     * closed.
     *
     * @param tracePath
     *            The full path of the trace to open
     * @param traceType
     *            The trace type of the trace to open
     * @param viewIDs
     *            The IDs of the views to test.
     */
    protected void runTestWithTrace(String tracePath, String traceType, Collection<String> viewIDs) {
        closeAllViews(viewIDs);
        /* Open the trace */
        String traceName = new File(tracePath).getName();
        SWTBotUtils.openTrace(PROJECT_NAME, tracePath, traceType);

        // Make sure all the analyses we'll need are done
        ITmfTrace trace = TmfTraceManager.getInstance().getActiveTrace();
        beforeRunningTest(trace);
        WaitUtils.waitForJobs();

        SWTBotView view;

        for (String viewID : viewIDs) {
            SWTBotUtils.openView(viewID);
            view = fBot.viewById(viewID);
            prepareView(view);
            navigateTrace(view);
            SWTBotUtils.closeViewById(viewID, fBot);
        }

        // Close the trace
        fBot.closeAllEditors();

        // If there is only 1 view to test, return
        if (viewIDs.size() <= 1) {
            // Close the views
            closeAllViews(viewIDs);
            return;
        }

        // Open all the views
        view = null;
        for (String viewID : viewIDs) {
            SWTBotUtils.openView(viewID);
            if (view == null) {
                view = fBot.viewById(viewID);
            }
        }

        // Rename the trace, so the results appear under another trace and
        // navigate it
        renameTrace(traceName, traceName + " full");
        navigateTrace(view);

        // Close the trace
        fBot.closeAllEditors();

        // Close the views
        closeAllViews(viewIDs);
    }

    private void renameTrace(String oldName, String newName) {

        SWTBotTreeItem traceItem = SWTBotUtils.getTraceProjectItem(fBot, SWTBotUtils.selectTracesFolder(fBot, PROJECT_NAME), oldName);

        traceItem.contextMenu().menu("Rename...").click();
        final String RENAME_TRACE_DIALOG_TITLE = "Rename Trace";
        SWTBotShell shell = fBot.shell(RENAME_TRACE_DIALOG_TITLE).activate();
        SWTBotText text = shell.bot().textWithLabel("New Trace name:");
        text.setText(newName);
        shell.bot().button("OK").click();
        fBot.waitUntil(Conditions.shellCloses(shell));
        fBot.waitWhile(new ConditionHelpers.ActiveEventsEditor(fBot, null));

        SWTBotTreeItem copiedItem = SWTBotUtils.getTraceProjectItem(fBot, SWTBotUtils.selectTracesFolder(fBot, PROJECT_NAME), newName);
        copiedItem.contextMenu().menu("Open").click();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        WaitUtils.waitForJobs();
    }

    // TODO: Add some vertical scrollings. With eventual 2D queries, that will
    // be something to test as well
    private void navigateTrace(SWTBotView view) {
        TmfTimeRange originalWindowRange = TmfTraceManager.getInstance().getCurrentTraceContext().getWindowRange();
        TmfTimeRange selectionRange = TmfTraceManager.getInstance().getCurrentTraceContext().getSelectionRange();
        IWorkbenchPart part = view.getViewReference().getPart(false);

        ITmfTrace activeTrace = TmfTraceManager.getInstance().getActiveTrace();
        assertNotNull(activeTrace);

        Performance perf = Performance.getDefault();
        PerformanceMeter pmBuild = perf.createPerformanceMeter("UI responsiveness: " + activeTrace.getName() + "/" + view.getTitle());
        perf.tagAsSummary(pmBuild, view.getTitle() + " CPU", Dimension.CPU_TIME);

        // Set the time range to the full trace range
        TmfTimeRange fullRange = new TmfTimeRange(activeTrace.getStartTime(), activeTrace.getEndTime());
        TmfSignalManager.dispatchSignal(new TmfWindowRangeUpdatedSignal(this, fullRange));
        pmBuild.start();
        waitViewReady(part, selectionRange, fullRange.getEndTime());
        pmBuild.stop();
        TmfTimeRange windowRange = fullRange;

        // Zoom in 10 times 15 percent of the range and wait for the view to be
        // ready
        for (int i = 0; i < 10; i++) {
            double delta = (windowRange.getEndTime().getValue() - windowRange.getStartTime().getValue()) * 0.15;
            TmfTimeRange newWindowRange = new TmfTimeRange(TmfTimestamp.fromNanos((long) (windowRange.getStartTime().toNanos() + delta)), TmfTimestamp.fromNanos((long) (windowRange.getEndTime().toNanos() - delta)));
            TmfSignalManager.dispatchSignal(new TmfWindowRangeUpdatedSignal(this, newWindowRange));
            pmBuild.start();
            windowRange = newWindowRange;
            waitViewReady(part, selectionRange, newWindowRange.getEndTime());
            pmBuild.stop();
        }

        // At this zoom level, go to the end
        long scrollTime = (windowRange.getEndTime().toNanos() - windowRange.getStartTime().toNanos()) / 2;
        windowRange = new TmfTimeRange(TmfTimestamp.fromNanos(fullRange.getEndTime().toNanos() - (2 * scrollTime)), fullRange.getEndTime());
        TmfSignalManager.dispatchSignal(new TmfWindowRangeUpdatedSignal(this, windowRange));
        pmBuild.start();
        waitViewReady(part, selectionRange, windowRange.getEndTime());
        pmBuild.stop();

        // Scroll back horizontally half the range at a time
        for (int i = 0; i < 10; i++) {
            TmfTimeRange newWindowRange = new TmfTimeRange(TmfTimestamp.fromNanos(windowRange.getStartTime().toNanos() - scrollTime), TmfTimestamp.fromNanos(windowRange.getEndTime().toNanos() - scrollTime));
            TmfSignalManager.dispatchSignal(new TmfWindowRangeUpdatedSignal(this, newWindowRange));
            pmBuild.start();
            windowRange = newWindowRange;
            waitViewReady(part, selectionRange, newWindowRange.getEndTime());
            pmBuild.stop();
        }

        // then go all the way back to the beginning
        windowRange = new TmfTimeRange(fullRange.getStartTime(), TmfTimestamp.fromNanos(fullRange.getStartTime().toNanos() + scrollTime));
        TmfSignalManager.dispatchSignal(new TmfWindowRangeUpdatedSignal(this, windowRange));
        pmBuild.start();
        waitViewReady(part, selectionRange, windowRange.getEndTime());
        pmBuild.stop();

        // and zoom out again
        TmfSignalManager.dispatchSignal(new TmfWindowRangeUpdatedSignal(this, fullRange));
        pmBuild.start();
        waitViewReady(part, selectionRange, fullRange.getEndTime());
        pmBuild.stop();

        // Reset the original window range
        TmfSignalManager.dispatchSignal(new TmfWindowRangeUpdatedSignal(this, originalWindowRange));
        pmBuild.start();
        waitViewReady(part, selectionRange, originalWindowRange.getEndTime());
        pmBuild.stop();

        pmBuild.commit();
    }

    private void waitViewReady(IWorkbenchPart part, @NonNull TmfTimeRange selectionRange, @NonNull ITmfTimestamp visibleTime) {
        if (part instanceof AbstractTimeGraphView) {
            fBot.waitUntil(ConditionHelpers.timeGraphIsReadyCondition((AbstractTimeGraphView) part, selectionRange, visibleTime));
        } else if (part instanceof TmfChartView) {
            TmfChartView tmfChartView = (TmfChartView) part;
            WaitUtils.waitUntil(cv -> !cv.isDirty(), tmfChartView, "TmfChartView is dirty");
        }
        // TODO Add conditions for other kind of views
    }

    /**
     * Actions to run on the view before benchmarking
     *
     * @param view
     *            view to benchmark
     */
    public void prepareView(SWTBotView view) {
        // do nothing by default
    }

}
