package de.renew.fa.figures;

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.io.Serializable;
import java.rmi.RemoteException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.Vector;

import CH.ifa.draw.standard.AbstractFigure;
import CH.ifa.draw.standard.NullHandle;
import CH.ifa.draw.standard.RelativeLocator;
import CH.ifa.draw.util.AWTSynchronizedUpdate;
import de.renew.draw.storables.api.StorableApi;
import de.renew.draw.storables.ontology.FigureChangeEvent;
import de.renew.draw.storables.ontology.FigureChangeListener;
import de.renew.draw.storables.ontology.FigureEnumeration;
import de.renew.draw.ui.ontology.FigureHandle;
import de.renew.fa.FADrawing;
import de.renew.faformalism.util.FAAutomatonModelEnum;
import de.renew.faformalism.util.FAAutomatonSimulationHelper;
import de.renew.faformalism.util.SimulationSettingsManager;
import de.renew.gui.InstanceDrawing;
import de.renew.gui.InstanceFigure;
import de.renew.gui.PlaceFigure;
import de.renew.gui.TokenBagFigure;
import de.renew.remote.EventListener;
import de.renew.remote.ObjectAccessor;
import de.renew.remote.PlaceInstanceAccessor;
import de.renew.remote.RemoteEventForwarder;
import de.renew.remote.TokenCountsAccessor;

/**
 * A FAStateFigure instance. Contains a {@link de.renew.fa.figures.FAStateFigure FAStateFigure} and handles the interaction with it.
 */
public class FAStateInstanceFigure extends AbstractFigure
    implements InstanceFigure, EventListener, FigureChangeListener
{
    private static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(FAStateInstanceFigure.class);
    /**
     * True if the figure is currently active, false otherwise
     */
    private boolean _isActive;
    /**
     * The state figure passed in the constructor.
     */
    protected FAStateFigure _faStateFigure;
    /**
     * A PlaceInstanceAccessor used to modify net elements.
     */
    protected PlaceInstanceAccessor _placeInstance;
    /**
     * If this figure is marked as highlighted.
     */
    protected int _markingAppearance;
    /**
     * The drawing as passed by the constructor.
     */
    protected InstanceDrawing _drawing;
    /**
     * Used during update.
     */
    private AWTSynchronizedUpdate _updateTask;
    /**
     * Adds a RemoteEventForwarder to placeInstance.
     */
    protected RemoteEventForwarder _forwarder;

    /**
     * Constructs a FAStateInstanceFigure object.
     *
     * @param drawing The drawing it is on.
     * @param faf The faStateFigure.
     * @param netElements A hash table containing the net elements used during initialization.
     */
    public FAStateInstanceFigure(
        InstanceDrawing drawing, FAStateFigure faf,
        Hashtable<Serializable, ObjectAccessor> netElements)
    {
        this._faStateFigure = faf;
        this._drawing = drawing;
        initialize(netElements);

        FAWordTextFigure word = null;
        if (SimulationSettingsManager.getAutomatonModel() != FAAutomatonModelEnum.NET) {
            word = new FAWordTextFigure();
            word.setReadOnly(true);
            word.setVisible(false);
            drawing.add(word);
            word.moveBy(
                center().x - word.displayBox().width / 2 - word.displayBox().x,
                displayBox().y - word.displayBox().height - word.displayBox().y);
        }

        if (SimulationSettingsManager.getAutomatonModel() == FAAutomatonModelEnum.PDA
            && drawing.getCpnDrawing() instanceof FADrawing faDrawing) {
            if (faDrawing.getDataStructureText() == null) {
                faDrawing.initializeDataStructureText();
                drawing.add(faDrawing.getDataStructureText());
            }
            FAWordTextFigure stackText = faDrawing.getDataStructureText();
            this._updateTask =
                new AWTSynchronizedUpdate(new StateHighlightUpdateTask(this, word, stackText));
        } else {
            this._updateTask =
                new AWTSynchronizedUpdate(new StateHighlightUpdateTask(this, word, null));
        }
    }

    /**
     * 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) {
        // FAStates are compiled to Places
        Enumeration<ObjectAccessor> elems = netElements.elements();
        _placeInstance = (PlaceInstanceAccessor) elems.nextElement();

        _faStateFigure.addFigureChangeListener(this);
        _markingAppearance = _faStateFigure.getMarkingAppearance();
        //we do not want to update before being done initializing, as it causes an IllegalStateException if we are a start state,
        //in the FAAutomatonCompiler formalism, and our word has not yet been added to the InstanceDrawing.
        EventQueue.invokeLater(this::update);
        if (_markingAppearance != FAStateFigure.HIGHLIGHT) {
            addTokenBagFigure();
        }

        // TODO: What is this good for? Necessary?
        try {
            _forwarder = new RemoteEventForwarder(this);
            _placeInstance.addRemoteEventListener(_forwarder);
        } catch (RemoteException e) {
            LOGGER.error(e.getMessage(), e);
        }
    }

    /**
     * Receives the {@link EventListener} events.
     * <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() {
        if (_updateTask != null) {
            _updateTask.scheduleUpdate();
        }
    }

    /**
     * Returns the drawing.
     *
     * @return The drawing.
     */
    InstanceDrawing drawing() {
        return _drawing;
    } // TODO: Should this be called getDrawing instead?

    /**
     * Gets the figure as a string with the following format:
     * <p>
     * FAStateInstanceFigure([ID])
     * <p>
     * Example:
     * If the ID is 6, the output will be as follows:
     * FAStateInstanceFigure(6)
     *
     * @return the generated string.
     */
    @Override
    public String toString() {
        return "FAStateInstanceFigure(" + _faStateFigure.getID() + ")";
    }

    //------------------------------ Highlighting and mark ------------------------------

    /**
     * Sets this figure as either highlighted or not, according to input.
     *
     * @param newIsActive true if this figure is highlighted, false otherwise.
     */
    protected void setHighlighted(boolean newIsActive) {
        if (newIsActive != _isActive) {
            _isActive = newIsActive;
            invalidate();
            if (listener() != null) {
                listener().figureRequestUpdate(StorableApi.createFigureChangeEvent(this));
            }
        }
    }

    /**
     * Adds a new {@link de.renew.gui.TokenBagFigure TokenBagFigure} as part of this figure.
     *
     * @return The generated TokenBagFigure.
     */
    protected TokenBagFigure addTokenBagFigure() {
        TokenBagFigure tbf =
            new TokenBagFigure(drawing(), this, _placeInstance, _markingAppearance);

        _drawing.add(tbf);
        return tbf;
    }

    @Override
    public void setAttribute(String attribute, Object value) {
        LOGGER.debug("setAttribute(String, Object) called with " + attribute + " and " + value);

        if ("MarkingAppearance".equals(attribute)) {
            _markingAppearance = ((Integer) value).intValue();
            TokenBagFigure etbf = getTokenBagFigure();

            if (_markingAppearance == PlaceFigure.HIGHLIGHT) {
                if (etbf != null) {
                    _drawing.remove(etbf);
                }
            } else {
                if (etbf == null) {
                    addTokenBagFigure();
                } else {
                    etbf.setMarkingAppearance(_markingAppearance);
                }
            }
        } else {
            super.setAttribute(attribute, value);
        }
    }

    /**
     * Gets the associated TokenBagFigure.
     *
     * @return The TokenBagFigure if it exists or null.
     */
    private TokenBagFigure getTokenBagFigure() {
        LOGGER.debug("getTokenBagFigure called");

        FigureEnumeration childenumeration = children();

        if (childenumeration.hasMoreElements()) {
            return (TokenBagFigure) childenumeration.nextElement();
        } else {
            return null;
        }
    }

    //------------------------------ Figure (with highlight) ------------------------------

    /**
     * Only used when the state contains the empty String as a word.
     * In that case, we highlight the state green.
     *
     * @param g - The graphics of the state we draw into.
     */
    @Override
    public void draw(Graphics g) {
        if (SimulationSettingsManager.getAutomatonModel() != FAAutomatonModelEnum.NET
            && _faStateFigure.isEndState()) {
            try {
                if (!_placeInstance.getTokenCounts().isEmpty()) {
                    String[] tokens = _placeInstance.getMarking().getAllTokenStrings();
                    if (tokens.length == 0) { //the figure was released since checking for emptiness
                        return;
                    }
                    if (tokens[0] != null) {
                        String word = tokens[0].substring(1).split(",")[0];
                        word = word.replaceAll("\s+", "");
                        List<String> t =
                            FAAutomatonSimulationHelper.prepareTokenForConsumption(word);
                        if (t.contains("")) {
                            Color fill = _faStateFigure.getFillColor();
                            _faStateFigure.setFillColor(new Color(100, 200, 100));
                            _faStateFigure.internalDraw(g);
                            _faStateFigure.setFillColor(fill);
                        }
                    }
                }
            } catch (RemoteException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * Not used, use the DisplayBox of the FAStateFigure instead.
     *
     * @param dx Not used
     * @param dy Not used
     */
    @Override
    protected void basicMoveBy(int dx, int dy) {
        // do nothing, as the DisplayBox of the FAState is used.
    }

    /**
     * Not used, use the DisplayBox of the FAStateFigure instead.
     *
     * @param origin Not used
     * @param corner Not used
     */
    @Override
    public void basicDisplayBox(Point origin, Point corner) {
        // do nothing, as the DisplayBox of the FAState is used.
    }

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

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

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

        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;
    }

    //------------------------------ FigureChangeListener ------------------------------

    /**
     * Not used
     *
     * @param e Not used
     */
    @Override
    public void figureInvalidated(FigureChangeEvent e) {}

    /**
     * Calls the "changed" function
     *
     * @param e Not used
     */
    @Override
    public void figureChanged(FigureChangeEvent e) {
        // If place changes, change
        changed();
    }

    /**
     * Not used
     *
     * @param e Not used
     */
    @Override
    public void figureRemoved(FigureChangeEvent e) {}

    /**
     * Not used
     *
     * @param e Not used
     */
    @Override
    public void figureRequestRemove(FigureChangeEvent e) {}

    /**
     * Not used
     *
     * @param e Not used
     */
    @Override
    public void figureRequestUpdate(FigureChangeEvent e) {}

    /**
     * Not used
     *
     * @param e Not used
     */
    @Override
    public void figureHandlesChanged(FigureChangeEvent e) {}

    //------------------------------ HighlightUpdateTask ------------------------------

    /**
     * An instance of this class is associated to each
     * <code>FAStateInstanceFigure</code> to handle the marking update
     * notifications coming from the simulator.
     * This class is designed to be scheduled by a {@link AWTSynchronizedUpdate} instance.
     **/
    private static class StateHighlightUpdateTask implements Runnable {
        private FAStateInstanceFigure _figure;

        private FAWordTextFigure _word;

        private FAWordTextFigure _dataStructureText;

        /**
         * Creates a new <code>StateHighlightUpdateTask</code> instance
         * associated with the given <code>figure</code>.
         *
         * @param figure the <code>StateInstanceFigure</code> which should
         * be updated by this task.
         **/
        public StateHighlightUpdateTask(
            FAStateInstanceFigure figure, FAWordTextFigure word, FAWordTextFigure dataStructureText)
        {
            this._figure = figure;
            this._word = word;
            this._dataStructureText = dataStructureText;
        }

        /**
         * Updates the associated <code>FAStateInstanceFigure</code> as long
         * as the figure has not been released.
         **/
        @Override
        public void run() {
            // Now we have to check if the update check is still valid
            // or if the figure has already been released in between.
            InstanceDrawing theDrawing = _figure.drawing();
            PlaceInstanceAccessor thePlaceInstance = _figure._placeInstance;
            if (theDrawing == null || thePlaceInstance == null) {
                // The figure has already been released.
                return;
            }

            try {
                theDrawing.lock();
                if (_figure.drawing() == null || _figure._placeInstance == null) {
                    // The figure was just now concurrently released (such
                    // a check would not be needed if all modifications
                    // occur in synchronization with the EventQueue).
                    return;
                }

                boolean newIsActive;
                try {
                    TokenCountsAccessor tokenCountsAccessor = thePlaceInstance.getTokenCounts();
                    newIsActive = !tokenCountsAccessor.isEmpty();
                    LOGGER.debug(
                        "PlaceInstance (" + thePlaceInstance.getID() + ") is now "
                            + (newIsActive ? "active" : "inactive"));
                    if (_word != null) {
                        String[] tokens = thePlaceInstance.getMarking().getAllTokenStrings();
                        if (tokens != null && tokens.length != 0) {
                            if (_dataStructureText != null) {
                                _dataStructureText.setText(
                                    "Stack:" + tokens[0].substring(
                                        tokens[0].lastIndexOf('['), tokens[0].length() - 1));
                            }
                            String newWord = tokens[0].substring(1, tokens[0].indexOf(','));
                            if (newWord.isBlank()) {
                                newWord = "\u03B5";
                            }
                            _word.setText(newWord);

                        } else {
                            _word.setText(" ");
                        }
                        _word.setVisible(newIsActive);
                        theDrawing.figureChanged(StorableApi.createFigureChangeEvent(_word));
                    }
                } catch (RemoteException e) {
                    LOGGER.error(e.getMessage(), e);
                    newIsActive = false;
                }
                _figure.setHighlighted(newIsActive);
            } finally {
                theDrawing.unlock();
            }
        }
    }
}