package de.renew.gui;

import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.rmi.RemoteException;
import java.util.Hashtable;
import java.util.Vector;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import CH.ifa.draw.standard.AbstractFigure;
import CH.ifa.draw.standard.NullHandle;
import CH.ifa.draw.standard.RelativeLocator;
import de.renew.draw.storables.api.StorableApi;
import de.renew.draw.storables.ontology.DrawingChangeEvent;
import de.renew.draw.storables.ontology.Figure;
import de.renew.draw.storables.ontology.FigureChangeEvent;
import de.renew.draw.ui.ontology.DrawingView;
import de.renew.draw.ui.ontology.FigureHandle;
import de.renew.remote.ObjectAccessor;
import de.renew.remote.RemoteEventForwarder;
import de.renew.remote.TransitionInstanceAccessor;


public class TransitionInstanceFigure extends AbstractFigure
    implements InstanceFigure, de.renew.remote.EventListener
{
    private static final org.apache.log4j.Logger logger =
        org.apache.log4j.Logger.getLogger(TransitionInstanceFigure.class);
    private final TransitionFigure transitionFigure;
    private TransitionInstanceAccessor transitionInstance;
    private boolean afterglow = false;
    private RemoteEventForwarder forwarder;
    protected final InstanceDrawing drawing;
    private static final int AFTERGLOW_TIME = 100;
    private static final int COOLDOWN_TIME = 10; // the time between two blinks of a transition
    private final Runnable afterglowRunnable;
    private final ThreadPoolExecutor executor;

    /**
     * Creates the transition instance figure.
     * @param drawing The CPN instance drawing to create for.
     * @param tf The corresponding transition figure.
     * @param netElements A lookup mapping net element group ids to net elements.
     */
    public TransitionInstanceFigure(
        InstanceDrawing drawing, TransitionFigure tf,
        Hashtable<Serializable, ObjectAccessor> netElements)
    {
        this.drawing = drawing;
        this.transitionFigure = tf;

        this.afterglowRunnable = () -> {
            try {
                EventQueue.invokeAndWait(() -> updateAfterglow(true));
                do {
                    Thread.sleep(AFTERGLOW_TIME);
                } while (isFiring());
                EventQueue.invokeAndWait(() -> updateAfterglow(false));
                Thread.sleep(COOLDOWN_TIME);
            } catch (InvocationTargetException | InterruptedException e) {
                // FIXME: add a comment why this can be ignored
                //  or add some handling/logging
            }
        };

        this.executor = new ThreadPoolExecutor(
            0, 1, 10, TimeUnit.SECONDS, new SynchronousQueue<>(),
            new ThreadPoolExecutor.DiscardPolicy());
        initialize(netElements);

        if (isFiring()) {
            update();
        }
    }

    /**
     * Sets the afterglow value and triggers a redraw of the figure.
     * @param newValue the new afterglow value
     */
    private void updateAfterglow(boolean newValue) {
        if (this.afterglow != newValue) {
            this.afterglow = newValue;
            if (listener() != null) {
                invalidate();
                invalidateHighlight();
                FigureChangeEvent event = StorableApi.createFigureChangeEvent(this);
                listener().figureRequestUpdate(event);
            }
        }
    }

    /**
     * Initializes the figure with the net element lookup.
     * @param netElements A lookup mapping net element group ids to net elements.
     */
    protected void initialize(Hashtable<Serializable, ObjectAccessor> netElements) {
        if (netElements.size() != 1) {
            logger.error(
                "Error: There is not exactly one transition instance for the figure " + this);
            return;
        }

        transitionInstance = (TransitionInstanceAccessor) netElements.elements().nextElement();

        try {
            forwarder = new RemoteEventForwarder(this);
            transitionInstance.addRemoteEventListener(forwarder);
        } catch (RemoteException e) {
            logger.error(e.getMessage(), e);
        }
    }

    /**
     * Returns whether the figure shall display firing state.
     * @return Whether the figure is firing.
     */
    public boolean isFiring() {
        boolean result = false;
        try {
            if (transitionInstance != null) {
                result = transitionInstance.isFiring();
            }
        } catch (RemoteException e) {
            // FIXME: add a comment why this can be ignored
            //  or add some handling/logging
        }
        return result;
    }

    @Override
    public boolean isHighlighted() {
        return afterglow;
    }

    private void invalidateHighlight() {
        Figure highlight = transitionFigure.getHighlightFigure();

        if (highlight != null) {
            Rectangle area = highlight.displayBox();
            DrawingChangeEvent dce = StorableApi.createDrawingChangeEvent(drawing, area);
            drawing.drawingInvalidated(dce);
        }
    }

    /**
     * This event signals that a Transition Occurrence has started or completed.
     * <p>
     * This method is naturally called asynchronously to the AWT event
     * queue, therefore the real update is scheduled to be executed within
     * the AWT thread. If multiple update notifications occur before the
     * update execution, those are ignored.
     * </p>
     **/
    @Override
    public void update() {
        this.executor.execute(afterglowRunnable);
    }

    @Override
    public void basicDisplayBox(Point origin, Point corner) {
        // do nothing, as the DisplayBox of the Transition is used!
    }

    @Override
    public Vector<FigureHandle> handles() {
        Vector<FigureHandle> handles = new Vector<>();

        handles.addElement(new NullHandle(this, RelativeLocator.northWest()));
        handles.addElement(new NullHandle(this, RelativeLocator.northEast()));
        handles.addElement(new NullHandle(this, RelativeLocator.southWest()));
        handles.addElement(new NullHandle(this, RelativeLocator.southEast()));
        return handles;
    }

    @Override
    public Rectangle displayBox() {
        return transitionFigure.displayBox();
    }

    @Override
    protected void basicMoveBy(int x, int y) {
        // do nothing, as the DisplayBox of the Transition is used!
    }

    @Override
    public void draw(Graphics g) {
        /*
         * if (actionCounter > 0) {
         * g.setColor(Color.red);
         * Rectangle r = displayBox();
         * g.fillRect(r.x, r.y, r.width, r.height);
         * }
         */
    }

    @Override
    public boolean inspect(DrawingView view, boolean alternate) {
        if (alternate) { // right-click
            if (fire()) {
                String name;
                try {
                    name = transitionInstance.asString();
                } catch (RemoteException e) {
                    name = e.toString();
                }
                view.editor().showStatus("Transition " + name + " fired.");
            } else {
                view.editor().showStatus("No binding for this transition.");
            }
        } else { // double-click
            logger.debug("Searching bindings for " + transitionInstance + "...");
            BindingSelectionFrame.open(transitionInstance, getSimulation());
        }
        return true;
    }

    protected CPNSimulation getSimulation() {
        return ModeReplacement.getInstance().getSimulation();
    }

    public boolean fire() {
        CPNSimulation simulation = getSimulation();

        simulation.getBreakpointManager().clearLog();
        boolean result;
        try {
            result = transitionInstance.fireOneBinding();
        } catch (RemoteException e) {
            logger.error(e.getMessage(), e);
            result = false;
        }

        simulation.simulationRefresh();
        return result;
    }

    @Override
    public void release() {
        if (transitionInstance != null) {
            try {
                transitionInstance.removeRemoteEventListener(forwarder);
            } catch (RemoteException e) {
                logger.error(e.getMessage(), e);
            }


            // OK: Release reference to transition instance so that it
            // can be garbage collected.
            transitionInstance = null;
        }
        super.release();
    }

    protected TransitionInstanceAccessor getInstance() {
        return transitionInstance;
    }
}
