package de.renew.gui;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.io.File;
import java.io.Serializable;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import javax.swing.JOptionPane;

import CH.ifa.draw.framework.FigureWithID;
import CH.ifa.draw.standard.StandardDrawing;
import de.renew.database.TransactionSource;
import de.renew.draw.storables.api.StorableApi;
import de.renew.draw.storables.ontology.DrawingChangeEvent;
import de.renew.draw.storables.ontology.DrawingChangeListener;
import de.renew.draw.storables.ontology.Figure;
import de.renew.draw.storables.ontology.FigureChangeEvent;
import de.renew.draw.storables.ontology.StorableOutput;
import de.renew.draw.ui.ontology.FigureDrawingContext;
import de.renew.net.NetInstance;
import de.renew.remote.NetInstanceAccessor;
import de.renew.remote.NetInstanceAccessorImpl;
import de.renew.remote.ObjectAccessor;
import de.renew.util.StringUtil;


public class CPNInstanceDrawing extends StandardDrawing
    implements DrawingChangeListener, FigureDrawingContext, InstanceDrawing
{
    private static final org.apache.log4j.Logger logger =
        org.apache.log4j.Logger.getLogger(CPNInstanceDrawing.class);
    public static final String ID_MACRO = "$ID";
    protected static final Hashtable<String, InstanceDrawing> drawingsByInstance =
        new Hashtable<>();
    private static final HashMap<Class<?>, InstanceDrawingFactory> instanceDrawingFactories =
        new HashMap<>();
    protected NetInstanceAccessor netInstance;
    private final CPNDrawing cpnDrawing;

    protected Hashtable<FigureWithID, Figure> instanceLookup;

    /**
     * Protected constructor for CPNInstanceDrawings, using the connect()-Method to
     * create a CPNInstanceDrawing based on the given CPNDrawing.
     * @param netInstance the netInstance accessed by the NetInstanceAccessor
     * @param drawing the CPNDrawing the CPNInstanceDrawing will be based on
     * @throws RemoteException If an RMI Problem occurred.
     */
    public CPNInstanceDrawing(NetInstanceAccessor netInstance, CPNDrawing drawing)
        throws RemoteException
    {
        this.cpnDrawing = drawing;
        connect(netInstance);
        cpnDrawing.addDrawingChangeListener(this);
    }

    @Override
    public NetInstanceAccessor getNetInstance() {
        return netInstance;
    }

    @Override
    public Rectangle displayBox() {
        return super.displayBox().union(cpnDrawing.displayBox());
    }

    /**
     * Returns the instance drawing associated to the given net
     * instance accessor, creates the drawing if necessary.
     *
     * @param netInstance the net instance accessor to look up
     *                    the drawing for
     * @return InstanceDrawing which displays the given netInstance.
     * If there is currently no such drawing, one is created.
     * If there is not even any CPNDrawing matching the net
     * known to the active simulation, returns
     * <code>null</code>.
     * @exception RemoteException If an RMI problem occurred.
     */
    public static InstanceDrawing getInstanceDrawing(NetInstanceAccessor netInstance)
        throws RemoteException
    {
        // forbid concurrent searches for instance drawings.
        synchronized (drawingsByInstance) {
            // find instance drawing:
            String key = netInstance.getNet().getName() + ":" + netInstance.getID();
            if (drawingsByInstance.containsKey(key)) {
                return drawingsByInstance.get(key);
            } else {
                // find drawing to base the instance drawing on
                CPNDrawing drawing = ModeReplacement.getInstance().getDrawingLoader()
                    .getDrawing(netInstance.getNet().getName());

                if (drawing != null) {
                    InstanceDrawing d = null;
                    InstanceDrawingFactory idf = instanceDrawingFactories.get(drawing.getClass());
                    if (idf != null) {
                        d = idf.getInstanceDrawing(netInstance, drawing);
                    }
                    if (d == null) {
                        d = new CPNInstanceDrawing(netInstance, drawing);
                    }
                    return d;
                } else {
                    String msg = "Cannot display net instance \"" + netInstance.asString()
                        + "\": net drawing not found.";
                    logger.error(msg);
                    JOptionPane.showMessageDialog(
                        null, msg + "\nPlease load the net drawing and try again.", "Renew",
                        JOptionPane.ERROR_MESSAGE);
                }
                return null;
            }
        }
    }

    /**
     * Returns all instance drawings based on the specified CPNDrawing.
     * Returns <code>null</code>, if no such drawing exists.
     */
    public static Enumeration<InstanceDrawing> getDependentInstanceDrawings(
        CPNDrawing baseDrawing)
    {
        Enumeration<InstanceDrawing> allDrawings = drawingsByInstance.elements();
        List<InstanceDrawing> dependentDrawings = new ArrayList<>();

        while (allDrawings.hasMoreElements()) {
            InstanceDrawing oneDrawing = allDrawings.nextElement();

            if (oneDrawing.getCpnDrawing() == baseDrawing) {
                dependentDrawings.add(oneDrawing);
            }
        }

        if (!dependentDrawings.isEmpty()) {
            return Collections.enumeration(dependentDrawings);
        } else {
            return null;
        }
    }

    @Override
    public CPNDrawing getCpnDrawing() {
        return cpnDrawing;
    }

    /**
     * Draws the instance drawing. The drawing is usually composed of the
     * figures of the drawing template with additional highlighting of figure
     * activity and the instance figures that are used to interact with the
     * simulation and to represent the current simulation state of places.
     * <p>
     * Calls {@link #drawTemplateFigures(Graphics)} and
     * {@link #drawInstanceFigures(Graphics)}, which can be overridden
     * separately.
     *
     * @param g the Graphics to draw into
     */
    @Override
    public void draw(Graphics g) {
        drawTemplateFigures(g);
        drawInstanceFigures(g);
    }

    /**
     * Draws the figures of the drawing template with additional highlighting
     * of figure activity. The template figures are drawn with this instance
     * drawing as drawing context which reflects the simulation state with
     * highlighting of figures.
     * <p>
     * This method is called by the {@link #draw(Graphics)} method and can be
     * overridden to change the drawing behavior.
     *
     * @param g the Graphics to draw into
     */
    protected void drawTemplateFigures(Graphics g) {
        cpnDrawing.draw(g, this);
    }

    /**
     * Draws the instance figures that are used to interact with the simulation
     * and to represent the current simulation state of places.
     * <p>
     * This method is called by the {@link #draw(Graphics)} method and can be
     * overridden to change the drawing behavior.
     * 
     * @param g the Graphics to draw into
     */
    protected void drawInstanceFigures(Graphics g) {
        super.draw(g);
    }

    @Override
    public InstanceFigure getInstanceFigure(Figure fig) {
        try {
            return (InstanceFigure) instanceLookup.get(fig);
        } catch (Exception e) {
            return null;
        }
    }

    @Override
    public InstanceFigure getInstanceFigureOfFigureWithID(int id) {
        Figure baseFigure = cpnDrawing.getFigureWithID(id);
        if (baseFigure != null) {
            return getInstanceFigure(baseFigure);
        }
        return null;
    }

    private boolean isEmphasized(Figure fig) {
        FigureWithHighlight node = cpnDrawing.getFigureForHighlight(fig);

        if (node != null) {
            fig = node;
        }
        InstanceFigure instFig = getInstanceFigure(fig);

        if (instFig != null) {
            return instFig.isHighlighted();
        }
        return false;
    }

    @Override
    public boolean isLocal() {
        return netInstance instanceof NetInstanceAccessorImpl;
    }

    @Override
    public boolean isHighlighted(Figure fig) {
        return fig.isVisible() && isEmphasized(fig);
    }

    /**
     * @return true if the figure is visible or emphasized and
     * not instance of a CPNTextFigure or the IconFigure of the corresponding CPNDrawing.
     */
    @Override
    public boolean isVisible(Figure fig) {
        return (fig.isVisible() || isEmphasized(fig)) && (!((fig instanceof CPNTextFigure
            && ((CPNTextFigure) fig).getType() == CPNTextFigure.INSCRIPTION
            && ((CPNTextFigure) fig).parent() instanceof PlaceFigure)
            || fig == cpnDrawing.getIconFigure()));
    }

    @Override
    public String expandMacro(String text) {
        return expandMacro(text, netInstance);
    }

    /**
     * Replaces the ID_MACRO with the ID of the Instance of the Drawing in a
     * given String.
     * @param text the String in which the replacement takes place
     * @param instance the NetInstance accessed by a NetInstanceAccessor
     * @return the text where the ID_MACRO is replaced by the ID of the Instance
     */
    public static String expandMacro(String text, NetInstanceAccessor instance) {
        try {
            return StringUtil.replace(text, ID_MACRO, instance.getID());
        } catch (RemoteException e) {
            return e.toString();
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * Creates instance figures in this instance drawing which
     * reflect the net elements of the given net instance.
     * Used by the constructor to build an instance drawing.
     * <p>
     * Also...
     * <ul>
     * <li> builds the instance lookup table
     * (see <code>getInstanceFigure()</code>) </li>
     * <li> registers this instance drawing in the
     * (private) drawingsByInstance map
     * (see <code>getInstanceDrawing()</code>) </li>
     * </ul>
     * May be more, but that is what I could see in the code.
     *
     * @param netInstance the net instance to be displayed
     * @see #getInstanceFigure
     * @see #getInstanceDrawing
     * @throws RemoteException If an RMI problem occurred.
     */
    @Override
    public void connect(NetInstanceAccessor netInstance) throws RemoteException {
        if (netInstance == null) {
            return;
        }

        String key = netInstance.getNet().getName() + ":" + netInstance.getID();
        if (this.netInstance != null) {
            drawingsByInstance.remove(key);
            notifyTransactionStrategyAboutRelease();
        }

        this.netInstance = netInstance;
        drawingsByInstance.put(key, this);
        notifyTransactionStrategyAboutConnect();

        setName(netInstance.asString());
        instanceLookup = new Hashtable<>();
        createInstanceFigures(netInstance);
    }

    /**
     * Creates instance figures in this instance drawing which
     * reflect the net elements of the given net instance.
     * Used by the {@link #connect(NetInstanceAccessor)} method.
     *
     * @param netInstance the net instance to be displayed
     * @throws RemoteException If an RMI problem occurred.
     */
    protected void createInstanceFigures(NetInstanceAccessor netInstance) throws RemoteException {
        NetInstanceElementLookup netElementLookup = new NetInstanceElementLookup(netInstance);
        Enumeration<Figure> figures = netElementLookup.getFigures();

        while (figures.hasMoreElements()) {
            FigureWithID figure = (FigureWithID) figures.nextElement();

            Hashtable<Serializable, ObjectAccessor> netElements =
                netElementLookup.getNetElements(figure);
            if ((netElements != null) && (figure instanceof SimulableFigure simulableFigure)) {
                InstanceFigure instanceFigure =
                    simulableFigure.createInstanceFigure(this, netElements);
                instanceLookup.put(figure, instanceFigure);
                add(instanceFigure);
                sendToBack(instanceFigure);
            }
        }
    }

    /**
     * Registers an InstanceDrawingFactory.
     * @param key to be stored for the InstanceDrawingFactory
     * @param factory the InstanceDrawingFactory
     */
    public static void registerInstanceDrawingFactory(
        Class<?> key, InstanceDrawingFactory factory)
    {
        instanceDrawingFactories.put(key, factory);
    }

    /**
     * Unregisters an InstanceDrawingFactory.
     * @param key to be used for retrieving previously registered instance
     */
    public static void unregisterInstanceDrawingFactory(Class<?> key) {
        instanceDrawingFactories.remove(key);
    }

    @Override
    public void release() {
        cpnDrawing.removeDrawingChangeListener(this);
        if (netInstance != null) {
            try {
                String key = netInstance.getNet().getName() + ":" + netInstance.getID();
                drawingsByInstance.remove(key);
            } catch (RemoteException e) {
                logger.error(e.getMessage(), e);
                JOptionPane.showMessageDialog(
                    null, "A problem occurred: " + e + "\n" + "See the console for details.",
                    "Renew", JOptionPane.ERROR_MESSAGE);
            }

            notifyTransactionStrategyAboutRelease();

            instanceLookup = null;
            netInstance = null;
        }
        super.release();
    }

    /**
     * Notifies the TransactionSource about the connect of
     * the current netInstance.
     */
    protected void notifyTransactionStrategyAboutConnect() {
        try {
            TransactionSource.netInstanceDrawingOpened(netInstance.getID());
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            JOptionPane.showMessageDialog(
                null, "A problem occurred: " + e + "\n" + "See the console for details.", "Renew",
                JOptionPane.ERROR_MESSAGE);
        }
    }

    /**
     * Notifies the TransactionSource about the release of
     * the current netInstance.
     */
    protected void notifyTransactionStrategyAboutRelease() {
        try {
            TransactionSource.netInstanceDrawingClosed(netInstance.getID());
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            JOptionPane.showMessageDialog(
                null, "A problem occurred: " + e + "\n" + "See the console for details.", "Renew",
                JOptionPane.ERROR_MESSAGE);
        }
    }

    /**
     * Sent when an area is invalid
     */
    @Override
    public void drawingInvalidated(DrawingChangeEvent e) {
        DrawingChangeEvent myEvent =
            StorableApi.createDrawingChangeEvent(this, e.getInvalidatedRectangle());
        Enumeration<DrawingChangeListener> listeners = drawingChangeListeners();

        while (listeners.hasMoreElements()) {
            DrawingChangeListener listener = listeners.nextElement();

            listener.drawingInvalidated(myEvent);
            listener.drawingRequestUpdate(myEvent);
        }
    }

    /**
     * Sent when the drawing wants to be refreshed
     */
    @Override
    public void drawingRequestUpdate(DrawingChangeEvent e) {
        // logger.debug("Source requests update: "+e.getInvalidatedRectangle());
    }

    /**
     * {@inheritDoc}
     * <p>
     * In the case of CPNInstanceDrawing, this method prohibits the
     * removal of any figures.
     * </p>
     **/
    @Override
    public void figureRequestRemove(FigureChangeEvent figureChangeEvent) {
        logger.error("Figures from net instance drawings may not be removed.");
    }

    /**
     * Returns whether drawing has been modified since last save.
     */
    @Override
    public boolean isModified() {
        return false; // an InstanceDrawing should not be saved!
    }

    @Override
    public Dimension defaultSize() {
        return cpnDrawing.defaultSize();
    }

    @Override
    public String getWindowCategory() {
        if (isLocal()) {
            return "Net instances";
        } else {
            return "Remote net instances";
        }
    }

    /**
     * Acquires the drawing lock of this drawing and the
     * background drawing. The background drawing is locked first.
     */
    @Override
    public void lock() {
        cpnDrawing.lock();
        super.lock();
    }

    /**
     * Releases the drawing lock of this drawing and the
     * background drawing.
     */
    @Override
    public void unlock() {
        super.unlock();
        cpnDrawing.unlock();
    }

    /**
     * This class is not serializable, although inheriting
     * serializability from its superclass.
     * <p>
     * Reason: Serialization is currently used for Copy&amp;Paste
     * and the Undo machanism only. As such modifications don't
     * make sense for instance drawings, this class doesn't
     * need to be serializable.
     * </p><p>
     * Second reason: Some fields of this class would lead to
     * a serialization of the compiled net, which is not desirable.
     * </p>
     * @param out UNUSED
     * @throws java.io.NotSerializableException always.
     */
    private void writeObject(java.io.ObjectOutputStream out) throws java.io.IOException {
        throw new java.io.NotSerializableException("CPNInstanceDrawing is not serializable!");
    }

    /**
     * Gathers all currently known <code>NetInstances</code>s
     * of the currently running local simulation.
     * The returned information is <b>not</b>
     * guaranteed to describe the complete current simulation
     * state!
     * A <code>NetInstance</code> is known, if it is displayed
     * by a <code>InstanceDrawing</code>.
     * Net instances of remote simulations are ignored.
     * <p>
     * <b>Caution:</b> In order to get consistent data, you have
     * to ensure that there are no concurrent modifications of
     * the simulation state.
     * This method is not able to lock the simulation.
     * </p>
     * Added Feb 29 2000  Michael Duvigneau
     */
    public static NetInstance[] getAllLocalInstances() {
        // Determine which net instances are to be saved.
        // Go through all instance drawings and extract references to
        // local net instances.
        List<NetInstance> saveList = new ArrayList<>(drawingsByInstance.size());
        Enumeration<InstanceDrawing> enumeration = drawingsByInstance.elements();

        while (enumeration.hasMoreElements()) {
            InstanceDrawing drawing = enumeration.nextElement();

            if (drawing.isLocal()) {
                NetInstanceAccessorImpl accessor =
                    (NetInstanceAccessorImpl) drawing.getNetInstance();
                saveList.add((NetInstance) accessor.getObject());
            }
        }

        return saveList.toArray(new NetInstance[0]);
    }

    /**
     * @return Returns the filename of the drawing template.
     */
    @Override
    public File getFilename() {
        return cpnDrawing.getFilename();
    }

    @Override
    public boolean isStorable() {
        return false;
    }

    @Override
    public void write(StorableOutput dw) {
        // do nothing. CPNInstanceDrawing is not storable.
        logger.warn(
            CPNInstanceDrawing.class.getSimpleName() + ": CPNInstanceDrawing is not storable.");
    }
}