/**********************************************************************
 * Copyright (c) 2005, 2014 IBM Corporation, 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:
 *     IBM - Initial API and implementation
 *     Bernd Hufmann - Updated for TMF
 **********************************************************************/

package org.eclipse.tracecompass.tmf.ui.views.uml2sd.core;

import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.eclipse.tracecompass.tmf.core.timestamp.ITmfTimestamp;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimestamp;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.drawings.IGC;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.preferences.ISDPreferences;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.preferences.SDViewPref;

/**
 * The Frame class is the base sequence diagram graph nodes container.<br>
 * For instance, only one frame can be drawn in the View.<br>
 * Lifelines, Messages and Stop which are supposed to represent a Sequence diagram are drawn in a Frame.<br>
 * Only the graph node added to their representing list will be drawn.
 *
 * The lifelines are appended along the X axsis when added in a frame.<br>
 * The syncMessages are ordered along the Y axsis depending on the event occurrence they are attached to.<br>
 *
 * @see org.eclipse.tracecompass.tmf.ui.views.uml2sd.core.Lifeline Lifeline for more event occurence details
 * @author sveyrier
 * @version 1.0
 */
public class BasicFrame extends GraphNode {

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

    /**
     * Contains the max elapsed time between two consecutive messages in the whole frame
     */
    private ITmfTimestamp fMaxTime = TmfTimestamp.fromSeconds(0);
    /**
     * Contains the min elapsed time between two consecutive messages in the whole frame
     */
    private ITmfTimestamp fMinTime = TmfTimestamp.fromSeconds(0);
    /**
     * Indicate if the min and max elapsed time between two consecutive messages in the whole frame need to be computed
     */
    private boolean fComputeMinMax = true;
    /**
     * Store the preference set by the user regarding the external time. This flag is used determine if the min and max
     * need to be recomputed in case this preference is changed.
     */
    private boolean fLastExternalTimePref = SDViewPref.getInstance().excludeExternalTime();
    /**
     * The greater event occurrence created on graph nodes drawn in this Frame This directly impact the Frame height
     */
    private int fVerticalIndex = 0;
    /**
     * The index along the x axis where the next lifeline will is drawn This directly impact the Frame width
     */
    private int fHorizontalIndex = 0;
    /**
     * The time information flag.
     */
    private boolean fHasTimeInfo = false;
    /**
     * The current Frame visible area - x coordinates
     */
    private int fVisibleAreaX;
    /**
     * The current Frame visible area - y coordinates
     */
    private int fVisibleAreaY;
    /**
     * The current Frame visible area - width
     */
    private int fVisibleAreaWidth;
    /**
     * The current Frame visible area - height
     */
    private int fVisibleAreaHeight;
    /**
     * The event occurrence spacing (-1 for none)
     */
    private int fForceEventOccurrenceSpacing = -1;
    /**
     * Flag to indicate customized minumum and maximum.
     */
    private boolean fCustomMinMax = false;
    /**
     * The minimum time between messages of the sequence diagram frame.
     */
    private ITmfTimestamp fMinSDTime = TmfTimestamp.fromSeconds(0);
    /**
     * The maximum time between messages of the sequence diagram frame.
     */
    private ITmfTimestamp fMaxSDTime = TmfTimestamp.fromSeconds(0);
    /**
     * Flag to indicate that initial minimum has to be computed.
     */
    private boolean fInitSDMin = true;

    // ------------------------------------------------------------------------
    // Constructors
    // ------------------------------------------------------------------------

    /**
     * Creates an empty frame.
     */
    public BasicFrame() {
        Metrics.setForcedEventSpacing(fForceEventOccurrenceSpacing);
    }

    // ------------------------------------------------------------------------
    // Methods
    // ------------------------------------------------------------------------

    /**
     *
     * Returns the greater event occurence known by the Frame
     *
     * @return the greater event occurrence
     */
    protected int getMaxEventOccurrence() {
        return fVerticalIndex;
    }

    /**
     * Set the greater event occurrence created in GraphNodes included in the frame
     *
     * @param eventOccurrence the new greater event occurrence
     */
    protected void setMaxEventOccurrence(int eventOccurrence) {
        fVerticalIndex = eventOccurrence;
    }

    /**
     * This method increase the lifeline place holder The return value is usually assign to a lifeline. This can be used
     * to set the lifelines drawing order. Also, calling this method two times and assigning only the last given index
     * to a lifeline will increase this lifeline draw spacing (2 times the default spacing) from the last added
     * lifeline.
     *
     * @return a new lifeline index
     */
    protected int getNewHorizontalIndex() {
        return ++fHorizontalIndex;
    }

    /**
     * Returns the current horizontal index
     *
     * @return the current horizontal index
     * @see Frame#getNewHorizontalIndex() for horizontal index description
     */
    protected int getHorizontalIndex() {
        return fHorizontalIndex;
    }

    @Override
    public void addNode(GraphNode nodeToAdd) {
        setComputeMinMax(true);
        super.addNode(nodeToAdd);
    }

    @Override
    public int getX() {
        return Metrics.FRAME_H_MARGIN;
    }

    @Override
    public int getY() {
        return Metrics.FRAME_V_MARGIN;
    }

    @Override
    public int getWidth() {
        if (fHorizontalIndex == 0) {
            return 3 * Metrics.swimmingLaneWidth() + Metrics.LIFELINE_H_MAGIN * 2 - Metrics.FRAME_H_MARGIN - Metrics.LIFELINE_SPACING / 2;
        }
        return fHorizontalIndex * Metrics.swimmingLaneWidth() + Metrics.LIFELINE_H_MAGIN * 2 + 1 - Metrics.LIFELINE_SPACING;
    }

    @Override
    public int getHeight() {
        // The Frame height depends on the maximum number of messages added to a lifeline
        if (fVerticalIndex == 0) {
            return 5 * (Metrics.getMessagesSpacing() + Metrics.getMessageFontHeigth()) + Metrics.LIFELINE_NAME_H_MARGIN + Metrics.FRAME_NAME_H_MARGIN + Metrics.getFrameFontHeigth() + Metrics.LIFELINE_VT_MAGIN + Metrics.LIFELINE_VB_MAGIN
                    + Metrics.LIFELINE_NAME_H_MARGIN + Metrics.FRAME_NAME_H_MARGIN + Metrics.getLifelineFontHeigth() * 2;
        }
        if (fForceEventOccurrenceSpacing >= 0) {
            Metrics.setForcedEventSpacing(fForceEventOccurrenceSpacing);
        }
        return fVerticalIndex * (Metrics.getMessagesSpacing() + Metrics.getMessageFontHeigth()) + Metrics.LIFELINE_NAME_H_MARGIN + Metrics.FRAME_NAME_H_MARGIN + Metrics.getFrameFontHeigth() + Metrics.LIFELINE_VT_MAGIN + Metrics.LIFELINE_VB_MAGIN
                + Metrics.LIFELINE_NAME_H_MARGIN + Metrics.FRAME_NAME_H_MARGIN + Metrics.getLifelineFontHeigth() * 2;
    }

    /**
     * @return true if mininum and maximum time needs to be calculated else false
     */
    protected boolean isComputeMinMax() {
        return fComputeMinMax;
    }

    /**
     * @return true if mininum and maximum time needs to be calculated else false
     */
    protected boolean isCustomMinMax() {
        return fCustomMinMax;
    }

    /**
     * gets the initialization flag for SD minimum.
     *
     * @return the initialization flag for SD minimum
     */
    protected boolean getInitSDMin() {
        return fInitSDMin;
    }

    /**
     * Returns the graph node which contains the point given in parameter for the given graph node list and starting the
     * iteration at the given index<br>
     * WARNING: Only graph nodes with smaller coordinates than the current visible area can be returned.<br>
     *
     * @param x the x coordinate of the point to test
     * @param y the y coordinate of the point to test
     * @param list the list to search in
     * @param fromIndex list browsing starting point
     * @return the graph node containing the point given in parameter, null otherwise
     *
     * @see org.eclipse.tracecompass.tmf.ui.views.uml2sd.core.GraphNode#getNodeFromListAt(int, int, java.util.List, int)
     */
    @Override
    protected GraphNode getNodeFromListAt(int x, int y, List<GraphNode> list, int fromIndex) {
        if (list == null) {
            return null;
        }
        for (int i = fromIndex; i < list.size(); i++) {
            GraphNode node = list.get(i);
            // only lifeline list is x ordered
            // Stop browsing the list if the node is outside the visible area
            // all others nodes will be not visible
            if ((node instanceof Lifeline) && (node.getX() > fVisibleAreaX + fVisibleAreaWidth)) {
                break;
            }
            if (node.getHeight() < 0) {
                if (node.getY() + node.getHeight() > fVisibleAreaY + fVisibleAreaHeight) {
                    break;
                }
            } else {
                if (node.getY() > fVisibleAreaY + fVisibleAreaHeight) {
                    break;
                }
            }
            if (node.contains(x, y)) {
                return node;
            }
        }
        return null;
    }

    /**
     * Draw the Frame rectangle
     *
     * @param context the context to draw to
     */
    protected void drawFrame(IGC context) {

        ISDPreferences pref = SDViewPref.getInstance();

        context.setBackground(pref.getBackGroundColor(ISDPreferences.PREF_FRAME));
        context.setForeground(pref.getForeGroundColor(ISDPreferences.PREF_FRAME));

        int x = getX();
        int y = getY();
        int w = getWidth();
        int h = getHeight();

        // Draw the frame main rectangle
        context.fillRectangle(x, y, w, h);
        context.drawRectangle(x, y, w, h);

        context.setBackground(pref.getBackGroundColor(ISDPreferences.PREF_FRAME_NAME));
        context.setForeground(pref.getForeGroundColor(ISDPreferences.PREF_FRAME_NAME));
        context.setFont(pref.getFont(ISDPreferences.PREF_FRAME_NAME));

        int nameWidth = context.textExtent(getName()) + 2 * Metrics.FRAME_NAME_V_MARGIN;
        int nameHeight = Metrics.getFrameFontHeigth() + +Metrics.FRAME_NAME_H_MARGIN * 2;

        // Draw the frame name area
        if (nameWidth > w) {
            nameWidth = w;
        }

        int[] points = { x, y, x + nameWidth, y, x + nameWidth, y - 11 + nameHeight, x - 11 + nameWidth, y + nameHeight, x, y + nameHeight, x, y + nameHeight };
        context.fillPolygon(points);
        context.drawPolygon(points);
        context.drawLine(x, y, x, y + nameHeight);

        context.setForeground(pref.getFontColor(ISDPreferences.PREF_FRAME_NAME));
        context.drawTextTruncatedCentred(getName(), x, y, nameWidth - 11, nameHeight, false);

        context.setBackground(pref.getBackGroundColor(ISDPreferences.PREF_FRAME));
        context.setForeground(pref.getForeGroundColor(ISDPreferences.PREF_FRAME));
    }

    @Override
    public void draw(IGC context) {
        draw(context, true);
    }

    /**
     * Draws the Frame on the given context.<br>
     * This method start width GraphNodes ordering if needed.<br>
     * After, depending on the visible area, only visible GraphNodes are drawn.<br>
     *
     * @param context the context to draw to
     * @param drawFrame indicate if the frame rectangle need to be redrawn
     * @see org.eclipse.tracecompass.tmf.ui.views.uml2sd.core.GraphNode#draw(IGC)
     */
    protected void draw(IGC context, boolean drawFrame) {
        fVisibleAreaHeight = context.getVisibleHeight();
        fVisibleAreaWidth = context.getVisibleWidth();
        fVisibleAreaX = context.getContentsX();
        fVisibleAreaY = context.getContentsY();

        if (fForceEventOccurrenceSpacing >= 0) {
            Metrics.setForcedEventSpacing(fForceEventOccurrenceSpacing);
        } else {
            Metrics.setForcedEventSpacing(-1);
        }

        super.drawChildenNodes(context);
    }

    /**
     * Sets the event occurrence spacing (-1 for none)
     *
     * @param space A spacing to set.
     */
    public void forceEventOccurrenceSpacing(int space) {
        fForceEventOccurrenceSpacing = space;
    }

    /**
     * Return the X coordinates of the frame visible area
     *
     * @return the X coordinates of the frame visible area
     */
    public int getVisibleAreaX() {
        return fVisibleAreaX;
    }

    /**
     * Return the frame visible area width
     *
     * @return the frame visible area width
     */
    public int getVisibleAreaWidth() {
        return fVisibleAreaWidth;
    }

    /**
     * Return the frame visible area height
     *
     * @return the frame visible area height
     */
    public int getVisibleAreaHeight() {
        return fVisibleAreaHeight;
    }

    /**
     * Return the X coordinates of the frame visible area
     *
     * @return the X coordinates of the frame visible area
     */
    public int getVisibleAreaY() {
        return fVisibleAreaY;
    }

    /**
     * Return the minimum time stored in the frame taking all GraphNodes into account
     *
     * @return the minimum GraphNode time
     */
    public ITmfTimestamp getMinTime() {
        if (fLastExternalTimePref != SDViewPref.getInstance().excludeExternalTime()) {
            fLastExternalTimePref = SDViewPref.getInstance().excludeExternalTime();
            setComputeMinMax(true);
        }
        if ((fComputeMinMax) && (!fCustomMinMax)) {
            computeMinMax();
            setComputeMinMax(false);
        }
        return fMinTime;
    }

    /**
     * Set the minimum timestamp of the frame.
     *
     * @param min
     *            The minimum timestamp
     */
    public void setMin(ITmfTimestamp min) {
        fMinTime = min;
        fCustomMinMax = true;
    }

    /**
     * Set the maximum timestamp of the frame.
     *
     * @param max
     *            The maximum timestamp
     */
    public void setMax(ITmfTimestamp max) {
        fMaxTime = max;
        fCustomMinMax = true;
    }

    /**
     * Reset min/max timestamp values to the default ones.
     */
    public void resetCustomMinMax() {
        fCustomMinMax = false;
        setComputeMinMax(true);
    }

    /**
     * Return the maximum time stored in the frame taking all GraphNodes into account
     *
     * @return the maximum GraphNode time
     */
    public ITmfTimestamp getMaxTime() {
        if (fLastExternalTimePref != SDViewPref.getInstance().excludeExternalTime()) {
            fLastExternalTimePref = SDViewPref.getInstance().excludeExternalTime();
            setComputeMinMax(true);
        }
        if (fComputeMinMax) {
            computeMinMax();
            setComputeMinMax(false);
        }
        return fMaxTime;
    }

    /**
     * Computes the minimum and maximum time between consecutive messages within the frame.
     */
    protected void computeMaxMinTime() {
        if (!fInitSDMin) {
            return;
        }

        List<SDTimeEvent> timeArray = buildTimeArray();

        if ((timeArray == null) || timeArray.isEmpty()) {
            return;
        }
        for (int i = 0; i < timeArray.size(); i++) {
            SDTimeEvent m = timeArray.get(i);

            if (m.getTime().compareTo(fMaxSDTime) > 0) {
                fMaxSDTime = m.getTime();
            }

            if ((m.getTime().compareTo(fMinSDTime) < 0) || fInitSDMin) {
                fMinSDTime = m.getTime();
                fInitSDMin = false;
            }
        }
    }

    /**
     * Returns the minimum time between consecutive messages.
     *
     * @return the minimum time between consecutive messages
     */
    public ITmfTimestamp getSDMinTime() {
        computeMaxMinTime();
        return fMinSDTime;
    }

    /**
     * Returns the maximum time between consecutive messages.
     *
     * @return the maximum time between consecutive messages
     */
    public ITmfTimestamp getSDMaxTime() {
        computeMaxMinTime();
        return fMaxSDTime;
    }

    /**
     * Browse all the GraphNode to compute the min and max times store in the Frame
     */
    protected void computeMinMax() {
        List<SDTimeEvent> timeArray = buildTimeArray();

        if ((timeArray == null) || timeArray.isEmpty()) {
            return;
        }
        for (int i = 0; i < timeArray.size() - 1; i++) {
            SDTimeEvent m1 = timeArray.get(i);
            SDTimeEvent m2 = timeArray.get(i + 1);

            updateMinMax(m1, m2);
        }
    }

    /**
     * Updates the minimum and maximum time between consecutive message within the frame based on the given values.
     *
     * @param m1 A first SD time event.
     * @param m2 A second SD time event.
     */
    protected void updateMinMax(SDTimeEvent m1, SDTimeEvent m2) {
        ITmfTimestamp delta = m2.getTime().getDelta(m1.getTime());
        if (fComputeMinMax) {
            fMinTime = delta;
            if (fMinTime.compareTo(TmfTimestamp.ZERO) < 0) {
                fMinTime = TmfTimestamp.create(0, m1.getTime().getScale());
            }
            fMaxTime = fMinTime;
            setComputeMinMax(false);
        }

        if ((delta.compareTo(fMinTime) < 0) && (delta.compareTo(TmfTimestamp.ZERO) > 0)) {
            fMinTime = delta;
        }

        if ((delta.compareTo(fMaxTime) > 0) && (delta.compareTo(TmfTimestamp.ZERO) > 0)) {
            fMaxTime = delta;
        }
    }

    /**
     * Builds the time array based on the list of graph nodes.
     *
     * @return the time array else empty list.
     */
    protected List<SDTimeEvent> buildTimeArray() {
        if (!hasChildren()) {
            return new ArrayList<>();
        }

        Iterator<String> it = getForwardSortMap().keySet().iterator();
        List<SDTimeEvent> timeArray = new ArrayList<>();
        while (it.hasNext()) {
            String nodeType = it.next();
            List<GraphNode> list = checkNotNull(getNodeMap().get(nodeType));
            for (int i = 0; i < list.size(); i++) {
                Object timedNode = list.get(i);
                if ((timedNode instanceof ITimeRange) && ((ITimeRange) timedNode).hasTimeInfo()) {
                    int event = list.get(i).getStartOccurrence();
                    ITmfTimestamp time = ((ITimeRange) list.get(i)).getStartTime();
                    SDTimeEvent f = new SDTimeEvent(time, event, (ITimeRange) list.get(i));
                    timeArray.add(f);
                    if (event != list.get(i).getEndOccurrence()) {
                        event = (list.get(i)).getEndOccurrence();
                        time = ((ITimeRange) list.get(i)).getEndTime();
                        f = new SDTimeEvent(time, event, (ITimeRange) list.get(i));
                        timeArray.add(f);
                    }
                }
            }
        }
        return timeArray;
    }

    @Override
    public String getArrayId() {
        return null;
    }

    @Override
    public boolean contains(int x, int y) {
        return false;
    }

    /**
     * @return true if frame has time info else false
     */
    public boolean hasTimeInfo() {
        return fHasTimeInfo;
    }

    /**
     * Sets the flag whether the frame has time info or not
     *
     * @param hasTimeInfo
     *          true if frame has time info else false
     */
    public void setHasTimeInfo(boolean hasTimeInfo) {
        fHasTimeInfo = hasTimeInfo;
    }

    /**
     * Sets the flag for minimum and maximum computation.
     *
     * @param computeMinMax
     *          true if mininum and maximum time needs to be calculated else false
     */
    public void setComputeMinMax(boolean computeMinMax) {
        fComputeMinMax = computeMinMax;
    }

    /**
     * Sets the initialization flag for SD minimum.
     *
     * @param initSDMin
     *          the flag to set
     */
    public void setInitSDMin(boolean initSDMin) {
        fInitSDMin = initSDMin;
    }
}
