/*******************************************************************************
 * Copyright (c) 2015, 2019 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:
 *   Matthew Khouzam - Initial API and implementation
 *******************************************************************************/

package org.eclipse.tracecompass.tmf.ui.swtbot.tests.viewers.events;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.Logger;
import org.apache.log4j.SimpleLayout;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Sash;
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.matchers.WidgetOfType;
import org.eclipse.swtbot.swt.finder.utils.SWTBotPreferences;
import org.eclipse.swtbot.swt.finder.widgets.AbstractSWTBot;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotMenu;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotRootMenu;
import org.eclipse.tracecompass.internal.tmf.ui.views.TmfAlignmentSynchronizer;
import org.eclipse.tracecompass.tmf.core.io.BufferedRandomAccessFile;
import org.eclipse.tracecompass.tmf.ui.project.wizards.NewTmfProjectWizard;
import org.eclipse.tracecompass.tmf.ui.swtbot.tests.shared.SWTBotSash;
import org.eclipse.tracecompass.tmf.ui.swtbot.tests.shared.SWTBotTimeGraph;
import org.eclipse.tracecompass.tmf.ui.swtbot.tests.shared.SWTBotUtils;
import org.eclipse.tracecompass.tmf.ui.swtbot.tests.shared.TestingPerspectiveFactory;
import org.eclipse.tracecompass.tmf.ui.swtbot.tests.views.TimeGraphViewStub;
import org.eclipse.tracecompass.tmf.ui.tests.shared.WaitUtils;
import org.eclipse.tracecompass.tmf.ui.views.histogram.HistogramView;
import org.eclipse.tracecompass.tmf.ui.views.timechart.TimeChartView;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.widgets.TimeGraphControl;
import org.eclipse.ui.IFolderLayout;
import org.eclipse.ui.IPageLayout;
import org.eclipse.ui.IPerspectiveFactory;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Test common time axis for views
 *
 * @author Matthew Khouzam
 */
@RunWith(SWTBotJunit4ClassRunner.class)
public class TmfAlignTimeAxisTest {

    /**
     * wait for throttler (2x the throttler time)
     */
    private static final String TRACE_START = "<trace>";
    private static final String EVENT_BEGIN = "<event timestamp=\"";
    private static final String EVENT_MIDDLE = " \" name=\"event\"><field name=\"field\" value=\"";
    private static final String EVENT_END = "\" type=\"int\" />" + "</event>";
    private static final String TRACE_END = "</trace>";

    private static final String PROJET_NAME = "TestAxisAlignment";
    private static final int NUM_EVENTS = 100;

    private static final String ALIGN_VIEWS_ACTION_NAME = "Align Views";

    /**
     * Using a small ratio for the editor so that other views have enough space
     * to be drawn, even when a low screen resolution is used.
     */
    private static final float EDITOR_AREA_RATIO = 0.10f;

    /** The Log4j logger instance. */
    private static final Logger fLogger = Logger.getRootLogger();
    private static SWTWorkbenchBot fBot;

    private static String makeEvent(int ts, int val) {
        return EVENT_BEGIN + Integer.toString(ts) + EVENT_MIDDLE + Integer.toString(val) + EVENT_END + "\n";
    }

    private static File fLocation;

    /**
     * Initialization, creates a temp trace
     *
     * @throws IOException
     *             should not happen
     */
    @BeforeClass
    public static void init() throws IOException {
        SWTBotUtils.initialize();
        Thread.currentThread().setName("SWTBot Thread"); // for the debugger
        /* set up for swtbot */
        SWTBotPreferences.TIMEOUT = 20000; /* 20 second timeout */
        fLogger.removeAllAppenders();
        fLogger.addAppender(new ConsoleAppender(new SimpleLayout()));
        SWTWorkbenchBot bot = new SWTWorkbenchBot();

        /* finish waiting for eclipse to load */
        WaitUtils.waitForJobs();
        fLocation = File.createTempFile("sample", ".xml");
        try (BufferedRandomAccessFile braf = new BufferedRandomAccessFile(fLocation, "rw")) {
            braf.writeBytes(TRACE_START);
            for (int i = 0; i < NUM_EVENTS; i++) {
                braf.writeBytes(makeEvent(i * 100, i % 4));
            }
            braf.writeBytes(TRACE_END);
        }
        SWTBotUtils.createProject(PROJET_NAME);
        SWTBotUtils.selectTracesFolder(bot, PROJET_NAME);
    }

    /**
     * Delete file
     */
    @AfterClass
    public static void cleanup() {
        fBot.perspectiveById(TestingPerspectiveFactory.ID).close();
        fBot.perspectiveById(AlignPerspectiveFactory1.ID).close();
        fBot.perspectiveById(AlignPerspectiveFactory2.ID).close();
        fBot.perspectiveById(AlignPerspectiveFactory3.ID).close();
        SWTBotUtils.deleteProject(PROJET_NAME, new SWTWorkbenchBot());
        fLocation.delete();
        fLogger.removeAllAppenders();
    }

    /**
     * Open the trace
     */
    @Before
    public void before() {
        SWTBotUtils.openTrace(PROJET_NAME, fLocation.getAbsolutePath(), "org.eclipse.linuxtools.tmf.core.tests.xmlstub");
    }

    /**
     * Close the trace
     */
    @After
    public void after() {
        SWTWorkbenchBot bot = new SWTWorkbenchBot();
        SWTBotUtils.activeEventsEditor(bot).close();
        /* Switch back to Testing perspective since several tests change the perspective */
        SWTBotUtils.switchToPerspective(TestingPerspectiveFactory.ID);
        SWTBotUtils.closeSecondaryShells(fBot);
    }

    /**
     * Test 3 views, none overlap, the histogram, time graph stub and timechart are
     * aligned. The histogram is moved, we check that the time graph stub and
     * timechart follow.
     */
    @Test
    public void testMoveHistogramOthersFollow() {
        fBot = new SWTWorkbenchBot();
        switchToPerspective(AlignPerspectiveFactory1.ID);
        testAligned(HistogramView.ID, TimeGraphViewStub.ID, TimeChartView.ID);
    }

    /**
     * Test 3 views, none overlap, the histogram, time graph stub and timechart are
     * aligned. The time graph stub is moved, we check that the histogram and
     * timechart follow.
     */
    @Test
    public void testMoveTgStubOthersFollow() {
        fBot = new SWTWorkbenchBot();
        switchToPerspective(AlignPerspectiveFactory1.ID);
        testAligned(TimeGraphViewStub.ID, HistogramView.ID, TimeChartView.ID);
    }

    /**
     * Test 3 views, overlap on the resizing view. The histogram and timechart
     * are overlapping, and the time graph stub is aligned. The histogram is moved, we
     * check that the time graph stub follows. The hidden timechart is not checked.
     */
    @Test
    public void testOverlappingHistogramMove() {
        fBot = new SWTWorkbenchBot();
        switchToPerspective(AlignPerspectiveFactory2.ID);
        testAligned(HistogramView.ID, TimeGraphViewStub.ID);
    }

    /**
     * Test 3 views, overlap on the resizing view. The histogram and timechart are
     * overlapping, and the time graph stub is aligned. The time graph stub is
     * moved, we check that the histogram follows. The hidden timechart is not
     * checked.
     */
    @Test
    public void testOverlappingTgStubMove() {
        fBot = new SWTWorkbenchBot();
        switchToPerspective(AlignPerspectiveFactory2.ID);
        testAligned(TimeGraphViewStub.ID, HistogramView.ID);
    }

    /**
     * Test 3 views. No overlap. The histogram and timechart are aligned, but
     * the time graph stub is not aligned. The histogram is moved, we check that the
     * timechart follows and that the time graph stub does NOT follow.
     */
    @Test
    public void testNotOverlappingHistogramMove() {
        fBot = new SWTWorkbenchBot();
        switchToPerspective(AlignPerspectiveFactory3.ID);
        testAligned(HistogramView.ID, TimeChartView.ID);
        testNotAligned(HistogramView.ID, TimeGraphViewStub.ID);
    }

    /**
     * Test 3 views. No overlap. The histogram and timechart are aligned, but
     * the time graph stub is not aligned. The time graph stub is moved, we check that the
     * histogram and timechart do NOT follow.
     */
    @Test
    public void testNotOverlappingTgStubMove() {
        fBot = new SWTWorkbenchBot();
        switchToPerspective(AlignPerspectiveFactory3.ID);
        testNotAligned(TimeGraphViewStub.ID, HistogramView.ID, TimeChartView.ID);
    }

    /**
     * Test for the "Align Views" menu item
     */
    @Test
    public void testMenuItem() {
        fBot = new SWTWorkbenchBot();
        switchToPerspective(AlignPerspectiveFactory1.ID);
        SWTBotView viewBot = fBot.viewById(TimeGraphViewStub.ID);
        SWTBotRootMenu viewMenu = viewBot.viewMenu();

        SWTBotMenu menuItems = viewMenu.menu(ALIGN_VIEWS_ACTION_NAME);
        assertTrue("Align views", menuItems.isChecked());
    }

    private static void switchToPerspective(String id) {
        // switch to the proper perspective and wait for views to align
        SWTBotUtils.switchToPerspective(id);
        // the PerspectiveSwitcher triggers a deferred layout
        Display.getDefault().syncExec(() -> {});
        // the TmfView control listener triggers a deferred alignment
        Display.getDefault().syncExec(() -> {});
        // the TmfAlignmentSynchronizer queues the alignment
        SWTBotUtils.waitUntil(synchronizer -> !synchronizer.isBusy() , TmfAlignmentSynchronizer.getInstance(), "Alignment synchronizer is busy");
    }

    private static AbstractSWTBot<?> getAlignmentControl(String viewId) {
        SWTBotView viewBot = fBot.viewById(viewId);
        switch (viewId) {
        case HistogramView.ID:
            return new SWTBotSash(viewBot.bot().widget(WidgetOfType.widgetOfType(Sash.class)));
        case TimeGraphViewStub.ID:
        case TimeChartView.ID:
            return new SWTBotTimeGraph(viewBot.bot().widget(WidgetOfType.widgetOfType(TimeGraphControl.class)));
        default:
            return null;
        }
    }

    private static void testAligned(String masterView, String... slaveViews) {
        final int offset = 50;

        // select alignment controls and get their original alignment positions
        AbstractSWTBot<?> master = getAlignmentControl(masterView);
        int masterOrigin = getAlignmentPosition(master);
        Map<AbstractSWTBot<?>, Integer> slaveMap = new HashMap<>();
        for (String slaveView : slaveViews) {
            AbstractSWTBot<?> slave = getAlignmentControl(slaveView);
            int slaveOrigin = getAlignmentPosition(slave);
            slaveMap.put(slave, slaveOrigin);
        }

        // change master position
        setAlignmentPosition(master, masterOrigin + offset);

        // check resulting alignment positions, slaves follow
        assertEquals(masterOrigin + offset, getAlignmentPosition(master), 2);
        for (Entry<AbstractSWTBot<?>, Integer> slave : slaveMap.entrySet()) {
            assertEquals(slave.getValue() + offset, getAlignmentPosition(slave.getKey()), 2);
        }

        // reset original alignment position
        setAlignmentPosition(master, masterOrigin);
    }

    private static void testNotAligned(String masterView, String... nonSlaveViews) {
        final int offset = 50;

        // select alignment controls and get their original alignment positions
        AbstractSWTBot<?> master = getAlignmentControl(masterView);
        int masterOrigin = getAlignmentPosition(master);
        Map<AbstractSWTBot<?>, Integer> nonSlaveMap = new HashMap<>();
        for (String nonSlaveView : nonSlaveViews) {
            AbstractSWTBot<?> nonSlave = getAlignmentControl(nonSlaveView);
            int nonSlaveOrigin = getAlignmentPosition(nonSlave);
            nonSlaveMap.put(nonSlave, nonSlaveOrigin);
        }

        // change master position
        setAlignmentPosition(master, masterOrigin + offset);

        // check resulting alignment positions, non-slaves do not follow
        assertEquals(masterOrigin + offset, getAlignmentPosition(master), 2);
        for (Entry<AbstractSWTBot<?>, Integer> nonSlave : nonSlaveMap.entrySet()) {
            assertEquals((int) nonSlave.getValue(), getAlignmentPosition(nonSlave.getKey()));
        }

        // reset original alignment position
        setAlignmentPosition(master, masterOrigin);
    }

    private static int getAlignmentPosition(AbstractSWTBot<?> control) {
        if (control instanceof SWTBotSash) {
            Rectangle bounds = ((SWTBotSash) control).getBounds();
            return bounds.x + bounds.width / 2;
        } else if (control instanceof SWTBotTimeGraph) {
            return ((SWTBotTimeGraph) control).getNameSpace();
        }
        return 0;
    }

    private static void setAlignmentPosition(AbstractSWTBot<?> control, int position) {
        if (control instanceof SWTBotSash) {
            SWTBotSash sash = (SWTBotSash) control;
            Rectangle bounds = sash.getBounds();
            Point dst = new Point(position, bounds.y + bounds.height / 2);
            sash.drag(dst);
        } else if (control instanceof SWTBotTimeGraph) {
            ((SWTBotTimeGraph) control).setNameSpace(position);
        }
        // the TmfAlignmentSynchronizer queues the alignment
        SWTBotUtils.waitUntil(synchronizer -> !synchronizer.isBusy() , TmfAlignmentSynchronizer.getInstance(), "Alignment synchronizer is busy");
    }

    /**
     * Histogram, time graph stub and timechart aligned but in different sites
     */
    public static class AlignPerspectiveFactory1 implements IPerspectiveFactory {

        /** The Perspective ID */
        public static final String ID = "org.eclipse.linuxtools.tmf.test.align.1"; //$NON-NLS-1$

        @Override
        public void createInitialLayout(IPageLayout layout) {
            if (layout == null) {
                return;
            }

            // Editor area
            layout.setEditorAreaVisible(true);

            // Editor area
            layout.setEditorAreaVisible(true);

            // Create the top left folder
            IFolderLayout topLeftFolder = layout.createFolder("topLeftFolder", IPageLayout.LEFT, 0.4f, IPageLayout.ID_EDITOR_AREA); //$NON-NLS-1$
            topLeftFolder.addView(IPageLayout.ID_PROJECT_EXPLORER);

            // Create the top right folder
            IFolderLayout topRightFolder = layout.createFolder("topRightFolder", IPageLayout.BOTTOM, EDITOR_AREA_RATIO, IPageLayout.ID_EDITOR_AREA); //$NON-NLS-1$
            topRightFolder.addView(HistogramView.ID);

            // Create the middle right folder
            IFolderLayout middleRightFolder = layout.createFolder("middleRightFolder", IPageLayout.BOTTOM, 0.50f, "topRightFolder"); //$NON-NLS-1$
            middleRightFolder.addView(TimeGraphViewStub.ID);

            // Create the bottom right folder
            IFolderLayout bottomRightFolder = layout.createFolder("bottomRightFolder", IPageLayout.BOTTOM, 0.65f, "middleRightFolder"); //$NON-NLS-1$ //$NON-NLS-2$
            bottomRightFolder.addView(TimeChartView.ID);

            // Populate menus, etc
            layout.addPerspectiveShortcut(ID);
            layout.addNewWizardShortcut(NewTmfProjectWizard.ID);
        }

    }

    /**
     * Timechart and Histogram share a site, all views aligned
     */
    public static class AlignPerspectiveFactory2 implements IPerspectiveFactory {

        /** The Perspective ID */
        public static final String ID = "org.eclipse.linuxtools.tmf.test.align.2"; //$NON-NLS-1$

        @Override
        public void createInitialLayout(IPageLayout layout) {
            if (layout == null) {
                return;
            }

            // Editor area
            layout.setEditorAreaVisible(true);

            // Editor area
            layout.setEditorAreaVisible(true);

            // Create the top left folder
            IFolderLayout topLeftFolder = layout.createFolder("topLeftFolder", IPageLayout.LEFT, 0.4f, IPageLayout.ID_EDITOR_AREA); //$NON-NLS-1$
            topLeftFolder.addView(IPageLayout.ID_PROJECT_EXPLORER);

            // Create the middle right folder
            IFolderLayout middleRightFolder = layout.createFolder("middleRightFolder", IPageLayout.BOTTOM, EDITOR_AREA_RATIO, IPageLayout.ID_EDITOR_AREA); //$NON-NLS-1$
            middleRightFolder.addView(HistogramView.ID);
            middleRightFolder.addView(TimeChartView.ID);

            // Create the bottom right folder
            IFolderLayout bottomRightFolder = layout.createFolder("bottomRightFolder", IPageLayout.BOTTOM, 0.65f, "middleRightFolder"); //$NON-NLS-1$ //$NON-NLS-2$
            bottomRightFolder.addView(TimeGraphViewStub.ID);

            // Populate menus, etc
            layout.addPerspectiveShortcut(ID);
            layout.addNewWizardShortcut(NewTmfProjectWizard.ID);
        }

    }

    /**
     * Histogram and timechart aligned, time graph stub not aligned
     */
    public static class AlignPerspectiveFactory3 implements IPerspectiveFactory {

        /** The Perspective ID */
        public static final String ID = "org.eclipse.linuxtools.tmf.test.align.3"; //$NON-NLS-1$

        @Override
        public void createInitialLayout(IPageLayout layout) {
            if (layout == null) {
                return;
            }

            // Editor area
            layout.setEditorAreaVisible(true);

            // Editor area
            layout.setEditorAreaVisible(true);

            // Create the top left folder
            IFolderLayout topLeftFolder = layout.createFolder("topLeftFolder", IPageLayout.LEFT, 0.4f, IPageLayout.ID_EDITOR_AREA); //$NON-NLS-1$
            topLeftFolder.addView(IPageLayout.ID_PROJECT_EXPLORER);

            IFolderLayout bottomLeftFolder = layout.createFolder("bottomLeftFolder", IPageLayout.BOTTOM, 0.5f, "topLeftFolder"); //$NON-NLS-1$
            bottomLeftFolder.addView(TimeGraphViewStub.ID);

            // Create the middle right folder
            IFolderLayout middleRightFolder = layout.createFolder("middleRightFolder", IPageLayout.BOTTOM, EDITOR_AREA_RATIO, IPageLayout.ID_EDITOR_AREA); //$NON-NLS-1$
            middleRightFolder.addView(HistogramView.ID);

            // Create the bottom right folder
            IFolderLayout bottomRightFolder = layout.createFolder("bottomRightFolder", IPageLayout.BOTTOM, 0.65f, "middleRightFolder"); //$NON-NLS-1$ //$NON-NLS-2$
            bottomRightFolder.addView(TimeChartView.ID);

            // Populate menus, etc
            layout.addPerspectiveShortcut(ID);
            layout.addNewWizardShortcut(NewTmfProjectWizard.ID);
        }

    }

}