/**********************************************************************
 * 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 java.util.Comparator;

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;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.util.SortAsyncForBackward;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.util.SortAsyncMessageComparator;

/**
 * A AsyncMessage is a asynchronous message which appear at two different event occurrences on each lifeline ends (sender
 * and receiver).<br>
 * <br>
 * <br>
 * Usage example:
 *
 * <pre>
 * Frame frame;
 * Lifeline lifeLine1;
 * Lifeline lifeLine2;
 *
 * AsyncMessage message = new AsyncMessage();
 * // Create a new event occurrence on each lifeline
 * lifeline1.getNewOccurrenceIndex();
 * lifeline2.getNewOccurrenceIndex();
 * // Set the message sender and receiver
 * message.setStartLifeline(lifeLine1);
 * message.setEndLifline(lifeline2);
 * message.setName(&quot;Message label&quot;);
 * // add the message to the frame
 * frame.addMessage(message);
 * </pre>
 *
 * @see Lifeline Lifeline for more event occurence details
 * @version 1.0
 * @author sveyrier
 */
public class AsyncMessage extends BaseMessage implements ITimeRange {

    // ------------------------------------------------------------------------
    // Constants
    // ------------------------------------------------------------------------
    /**
    * The grahNode ID constant
    */
    public static final String ASYNC_MESS_TAG = "AsyncMessage"; //$NON-NLS-1$

    // ------------------------------------------------------------------------
    // Attributes
    // ------------------------------------------------------------------------
    /**
     * Flag whether message has time information or not.
     */
    private boolean fHasTime = false;
    /**
     * The time when the message begin
     */
    private ITmfTimestamp fEndTime = TmfTimestamp.fromSeconds(0);
    /**
     * The time when the message end
     */
    private ITmfTimestamp fStartTime = TmfTimestamp.fromSeconds(0);
    /**
     * The associated message.
     */
    protected AsyncMessageReturn fMessageReturn = null;

    // ------------------------------------------------------------------------
    // Constructors
    // ------------------------------------------------------------------------
    /**
     * Default constructor
     */
    public AsyncMessage() {
        setColorPrefId(ISDPreferences.PREF_ASYNC_MESS);
    }

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

    @Override
    public int getX() {
        int x = super.getX(true);
        int activationWidth = Metrics.EXECUTION_OCCURRENCE_WIDTH / 2;
        if ((getStartLifeline() != null) && (getEndLifeline() != null) && (getStartLifeline().getX() > getEndLifeline().getX())) {
            activationWidth = -activationWidth;
        }

        if (isMessageStartInActivation(getStartOccurrence())) {
            x = x + activationWidth;
        }
        return x;
    }

    @Override
    public int getY() {
        if ((getStartLifeline() != null) && (getEndLifeline() != null)) {
            return getEndLifeline().getY() + getEndLifeline().getHeight() + (Metrics.getMessageFontHeigth() + Metrics.getMessagesSpacing()) * getStartOccurrence();
        }
        return super.getY();
    }

    @Override
    public int getWidth() {
        int width = super.getWidth(true);
        int activationWidth = Metrics.EXECUTION_OCCURRENCE_WIDTH / 2;
        if ((getStartLifeline() != null) && (getEndLifeline() != null) && (getStartLifeline().getX() > getEndLifeline().getX())) {
            activationWidth = -activationWidth;
        }

        if (isMessageStartInActivation(getStartOccurrence())) {
            width = width - activationWidth;
        }

        if (isMessageEndInActivation(getEndOccurrence())) {
            width = width - activationWidth;
        }

        return width;
    }

    @Override
    public int getHeight() {
        if ((getStartLifeline() != null) && (getEndLifeline() != null)) {
            return (getEndLifeline().getY() + getEndLifeline().getHeight() + (Metrics.getMessageFontHeigth() + Metrics.getMessagesSpacing()) * getEndOccurrence()) - getY();
        }
        return super.getHeight();
    }

    /**
     * Set the message return associated with this message.
     *
     * @param message the message return to associate
     */
    protected void setMessageReturn(AsyncMessageReturn message) {
        fMessageReturn = message;
    }

    /**
     * Set the event occurrence attached to this message for its end lifeline
     *
     * @param occurrence the event occurrence to set
     */
    @Override
    public void setEndOccurrence(int occurrence) {
        super.setEndOccurrence(occurrence);
        if (getStartLifeline() == null) {
            setStartOccurrence(occurrence);
        }
        informFrame(getEndLifeline(), occurrence);
    }

    /**
     * Informs the given lifeline about the maximum occurrence if applicable.
     *
     * @param lifeLine
     *            Concerned lifeline
     * @param occurrence
     *            Occurrence number
     */
    protected void informFrame(Lifeline lifeLine, int occurrence) {
        if ((lifeLine != null) && (lifeLine.getFrame() != null) && (lifeLine.getFrame().getMaxEventOccurrence() < occurrence)) {
            lifeLine.getFrame().setMaxEventOccurrence(occurrence);
        }
    }

    /**
     * Set the event occurrence attached to this message for its start lifeline
     *
     * @param occurrence the event occurrence to set
     */
    @Override
    public void setStartOccurrence(int occurrence) {
        super.setStartOccurrence(occurrence);
        if (getEndLifeline() == null) {
            setEndOccurrence(getStartOccurrence());
        }
        informFrame(getStartLifeline(), occurrence);
    }

    /**
     * Set the lifeLine which has sent the message.<br>
     * A new EventOccurence will be create on this lifeLine.<br>
     *
     * @param lifeline the message sender
     */
    public void autoSetStartLifeline(Lifeline lifeline) {
        lifeline.getNewEventOccurrence();
        setStartLifeline(lifeline);
    }

    /**
     * Set the lifeLine which has received the message.<br>
     * A new EventOccurence will be create on this lifeLine.<br>
     *
     * @param lifeline the message receiver
     */
    public void autoSetEndLifeline(Lifeline lifeline) {
        lifeline.getNewEventOccurrence();
        setEndLifeline(lifeline);
    }

    @Override
    public void setStartLifeline(Lifeline lifeline) {
        super.setStartLifeline(lifeline);
        setStartOccurrence(getStartLifeline().getEventOccurrence());
        if (getEndLifeline() == null) {
            setEndOccurrence(getStartOccurrence());
        }
    }

    @Override
    public void setEndLifeline(Lifeline lifeline) {
        super.setEndLifeline(lifeline);
        setEventOccurrence(getEndLifeline().getEventOccurrence());
    }

    /**
     * Returns true if the point C is on the segment defined with the point A and B
     *
     * @param xA point A x coordinate
     * @param yA point A y coordinate
     * @param xB point B x coordinate
     * @param yB point B y coordinate
     * @param xC point C x coordinate
     * @param yC point C y coordinate
     * @return Return true if the point C is on the segment defined with the point A and B, else otherwise
     */
    protected boolean isNearSegment(int xA, int yA, int xB, int yB, int xC, int yC) {
        if ((xA > xB) && (xC > xA)) {
            return false;
        }
        if ((xA < xB) && (xC > xB)) {
            return false;
        }
        if ((xA < xB) && (xC < xA)) {
            return false;
        }
        if ((xA > xB) && (xC < xB)) {
            return false;
        }
        double distAB = Math.sqrt((xB - xA) * (xB - xA) + (yB - yA) * (yB - yA));
        double scalar = ((xB - xA) * (xC - xA) + (yB - yA) * (yC - yA)) / distAB;
        double distAC = Math.sqrt((xC - xA) * (xC - xA) + (yC - yA) * (yC - yA));
        double distToSegment = Math.sqrt(Math.abs(distAC * distAC - scalar * scalar));
        return (distToSegment <= Metrics.MESSAGE_SELECTION_TOLERANCE);
    }

    @Override
    public boolean contains(int x, int y) {
        // Is it a self message?
        if (getStartLifeline() == getEndLifeline()) {
            return super.contains(x, y);
        }
        if (isNearSegment(getX(), getY(), getX() + getWidth(), getY() + getHeight(), x, y)) {
            return true;
        }
        int messageMaxWidth = Metrics.swimmingLaneWidth() - Metrics.EXECUTION_OCCURRENCE_WIDTH;
        int nameWidth = getName().length() * Metrics.getAverageCharWidth();
        if (getName().length() * Metrics.getAverageCharWidth() > messageMaxWidth) {
            if (GraphNode.contains(getX(), getY() - Metrics.MESSAGES_NAME_SPACING - Metrics.getMessageFontHeigth(), messageMaxWidth, Metrics.getMessageFontHeigth(), x, y)) {
                return true;
            }
        } else {
            if (GraphNode.contains(getX() + (messageMaxWidth - nameWidth) / 2, getY() + getHeight() / 2 - Metrics.MESSAGES_NAME_SPACING - Metrics.getMessageFontHeigth(), nameWidth, Metrics.getMessageFontHeigth(), x, y)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Draws the asynchronous message using giving graphical context.
     *
     * @param context A graphical context to draw in.
     */
    protected void drawAsyncMessage(IGC context) {
        if (getStartLifeline() != null && getEndLifeline() != null && getStartLifeline() == getEndLifeline() && (getStartOccurrence() != getEndOccurrence())) {
            int x = getX();
            int y = getY();
            int height = getHeight();
            int tempx = 0;
            boolean startInActivation = isMessageStartInActivation(getStartOccurrence());
            boolean endInActivation = isMessageEndInActivation(getEndOccurrence());

            if (endInActivation && !startInActivation) {
                tempx = Metrics.EXECUTION_OCCURRENCE_WIDTH / 2;
            }
            if (startInActivation && !endInActivation) {
                tempx = -Metrics.EXECUTION_OCCURRENCE_WIDTH / 2;
            }

            int tempy = Metrics.INTERNAL_MESSAGE_WIDTH / 2;
            if (getHeight() <= Metrics.INTERNAL_MESSAGE_WIDTH) {
                tempy = getHeight() / 2;
            }

            context.drawLine(x, y, x + Metrics.INTERNAL_MESSAGE_WIDTH / 2, y);
            context.drawLine(x + Metrics.INTERNAL_MESSAGE_WIDTH, y + tempy, x + Metrics.INTERNAL_MESSAGE_WIDTH, y + height - tempy);
            context.drawLine(x + tempx, y + height, x + Metrics.INTERNAL_MESSAGE_WIDTH / 2, y + height);

            Double xt = Double.valueOf(Math.cos(0.75) * 7);
            Double yt = Double.valueOf(Math.sin(0.75) * 7);

            context.drawLine(x + xt.intValue() + tempx, y + height + yt.intValue(), x + tempx, y + height);
            context.drawArc(x, y, Metrics.INTERNAL_MESSAGE_WIDTH, 2 * tempy, 0, 90);
            context.drawArc(x, y + height, Metrics.INTERNAL_MESSAGE_WIDTH, -2 * tempy, 0, -90);
            context.drawLine(x + xt.intValue() + tempx, y + height - yt.intValue(), x + tempx, y + height);

            context.drawTextTruncated(getName(), x + Metrics.INTERNAL_MESSAGE_WIDTH + Metrics.INTERNAL_MESSAGE_V_MARGIN, y, Metrics.swimmingLaneWidth() - Metrics.EXECUTION_OCCURRENCE_WIDTH + -Metrics.INTERNAL_MESSAGE_WIDTH,
                    +Metrics.MESSAGES_NAME_SPACING - Metrics.getMessageFontHeigth(), !isSelected());
        } else {
            super.draw(context);
        }
    }

    @Override
    public void draw(IGC context) {
        if (!isVisible()) {
            return;
        }

        ISDPreferences pref = SDViewPref.getInstance();

        // Draw it selected?
        if (isSelected() && (getStartLifeline() != null && getEndLifeline() != null && getStartLifeline() == getEndLifeline() && (getStartOccurrence() != getEndOccurrence()))) {
            /*
             * Draw it twice First time, bigger inverting selection colors Second time, regular drawing using selection
             * colors This create the highlight effect
             */
            context.setForeground(pref.getBackGroundColorSelection());
            context.setLineWidth(Metrics.SELECTION_LINE_WIDTH);
            drawAsyncMessage(context);
            context.setBackground(pref.getBackGroundColorSelection());
            context.setForeground(pref.getForeGroundColorSelection());
            // Second drawing is done after the else
        } else {
            context.setBackground(pref.getBackGroundColor(getColorPrefId()));
            context.setForeground(pref.getForeGroundColor(getColorPrefId()));
        }
        if (hasFocus()) {
            context.setDrawTextWithFocusStyle(true);
        }
        context.setLineWidth(Metrics.NORMAL_LINE_WIDTH);
        drawAsyncMessage(context);
        if (hasFocus()) {
            context.setDrawTextWithFocusStyle(false);
        }
    }

    /**
     * Set the time when the message end
     *
     * @param time the time when the message end
     */
    public void setEndTime(ITmfTimestamp time) {
        fEndTime = time;
        fHasTime = true;
        if (getStartLifeline() != null && getStartLifeline().getFrame() != null) {
            getStartLifeline().getFrame().setHasTimeInfo(true);
        } else if (getEndLifeline() != null && getEndLifeline().getFrame() != null) {
            getEndLifeline().getFrame().setHasTimeInfo(true);
        }
    }

    /**
     * Set the time when the message start
     *
     * @param time the time when the message start
     */
    public void setStartTime(ITmfTimestamp time) {
        fStartTime = time;
        fHasTime = true;
        if (getStartLifeline() != null && getStartLifeline().getFrame() != null) {
            getStartLifeline().getFrame().setHasTimeInfo(true);
        } else if (getEndLifeline() != null && getEndLifeline().getFrame() != null) {
            getEndLifeline().getFrame().setHasTimeInfo(true);
        }
    }

    @Override
    public ITmfTimestamp getEndTime() {
        return fEndTime;
    }

    @Override
    public ITmfTimestamp getStartTime() {
        return fStartTime;
    }

    @Override
    public boolean hasTimeInfo() {
        return fHasTime;
    }

    /**
     *  @return message return instance or null
     */
    public AsyncMessageReturn getMessageReturn() {
        return fMessageReturn;
    }

    @Override
    public boolean isVisible(int x, int y, int width, int height) {
        int toDrawY = getY();
        int toDrawHeight = getHeight();
        if ((toDrawY > y + height + Metrics.MESSAGES_NAME_SPACING + Metrics.getMessageFontHeigth()) && (toDrawY + toDrawHeight > y + height + Metrics.MESSAGES_NAME_SPACING + Metrics.getMessageFontHeigth())) {
            return false;
        }
        if (toDrawY < y && (toDrawY + toDrawHeight < y)) {
            return false;
        }
        return super.isVisible(x, y, width, height);
    }

    @Override
    public Comparator<GraphNode> getComparator() {
        return new SortAsyncMessageComparator();
    }

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

    @Override
    public Comparator<GraphNode> getBackComparator() {
        return new SortAsyncForBackward();
    }

    @Override
    public boolean positiveDistanceToPoint(int x, int y) {
        int mY = getY();
        int mH = getHeight();
        return ((mY > y) || (mY + mH > y));
    }
}
