package de.renew.gui;

import java.awt.Dimension;
import java.io.IOException;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.NoSuchElementException;
import java.util.Vector;

import CH.ifa.draw.framework.FigureChangeAdapter;
import CH.ifa.draw.framework.FilterContainer;
import CH.ifa.draw.standard.AbstractFigure;
import CH.ifa.draw.standard.CompositeFigure;
import CH.ifa.draw.standard.StandardDrawing;
import de.renew.draw.storables.ontology.ConnectionFigure;
import de.renew.draw.storables.ontology.Figure;
import de.renew.draw.storables.ontology.FigureChangeEvent;
import de.renew.draw.storables.ontology.FigureEnumeration;
import de.renew.draw.storables.ontology.StorableInput;
import de.renew.draw.storables.ontology.StorableOutput;
import de.renew.io.FileFilterCreator;
import de.renew.ioontology.ExtensionFileFilter;
import de.renew.simulatorontology.shadow.ShadowNet;
import de.renew.simulatorontology.shadow.ShadowNetElement;
import de.renew.simulatorontology.shadow.ShadowNetSystem;


public class CPNDrawing extends StandardDrawing implements LayoutableDrawing {
    private static final org.apache.log4j.Logger logger =
        org.apache.log4j.Logger.getLogger(CPNDrawing.class);

    /**
    * The shadow net of this drawing.
    * <p>
    * Transient, can be recalculated via
    * <code>buildShadow()</code>.
    * </p>
    */
    protected transient ShadowNet shadowNet = null;

    /**
     * The figure which should be displayed as representation
     * for instances of this net.
     * @serial
     */
    private AbstractFigure iconFigure = null;

    /**
     * Cache for all associations to
     * {@link FigureWithHighlight}s from their
     * highlight figures.
     * To point it out: the figure to be highlighted is
     * the key, and the figure with hilight is the value
     * of a pair in the hashtable.
     * <p>
     * Transient, will be rebuilt on deserialization.
     * </p>
     */
    private transient Hashtable<Figure, Figure> hilightMap = new Hashtable<>();

    /**
     * Listeners for all Figures added to or removed from a CPNDrawing
     */
    private transient Vector<CPNFigureListener> _figureListeners = new Vector<>();

    public CPNDrawing() {
        super();
    }

    @Override
    public void release() {
        super.release();
        discardShadow();
    }

    public ShadowNet getShadow() {
        return shadowNet;
    }

    static CPNDrawing findDrawing(Object errorObject) {
        // Determine the drawing containing the errorObject.
        if (errorObject instanceof ShadowNetElement) {
            return (CPNDrawing) ((ShadowNetElement) errorObject).getNet().getContext();
        }
        return null;
    }

    public void discardShadow() {
        if (shadowNet != null) {
            shadowNet.discard();
            shadowNet = null;
        }
    }

    /**
     * Calls the {@link ShadowHolder#buildShadow} method on all net
     * element figures of the given type.
     *
     * @param clazz  the <code>ShadowHolder</code> subclass to use
     *               as filter criterium
     */
    private void buildShadow(Class<?> clazz) {
        FigureEnumeration k = figures();

        while (k.hasMoreElements()) {
            Figure fig = k.nextFigure();

            if (fig instanceof ShadowHolder && clazz.isInstance(fig)) {
                ((ShadowHolder) fig).buildShadow(shadowNet);
            }
        }
    }

    public ShadowNet buildShadow(ShadowNetSystem netSystem) {
        discardShadow();
        shadowNet = new ShadowNet(getName(), netSystem);
        shadowNet.setContext(this);


        // Build shadows for declaration nodes (java-nets: only one per net!)
        buildShadow(DeclarationFigure.class);


        // Build shadows for nodes:
        buildShadow(NodeFigure.class);


        // Build shadows for connections:
        buildShadow(CH.ifa.draw.figures.LineConnection.class);


        // Build shadows for inscriptions:
        buildShadow(CPNTextFigure.class);

        return shadowNet;
    }

    public void setIconFigure(AbstractFigure iconFigure) {
        this.iconFigure = iconFigure;
    }

    public Figure getIconFigure() {
        return iconFigure;
    }

    /**
     * Removes a figure from the composite.
     * Additionally checks if the icon figure is removed.
     * Also checks if the highlight map has to be updated.
     * Also notifies the figureListeners that a figure was removed.
     */
    @Override
    public Figure remove(Figure figure) {
        if (figure == iconFigure) {
            iconFigure = null;
        }
        if (figure instanceof FigureWithHighlight) {
            Figure hilight = ((FigureWithHighlight) figure).getHighlightFigure();

            if (hilight != null) {
                hilightMap.remove(hilight);
            }
        }

        Figure result = super.remove(figure);

        if (figure instanceof CompositeFigure) {
            recomputeHilightMap();
        }

        notifyListeners(figure, false);

        return result;
    }

    /**
     * Writes the contained figures to the StorableOutput.
     */
    @Override
    public void write(StorableOutput dw) {
        super.write(dw);
        dw.writeStorable(iconFigure);
    }

    /**
     * Reads the contained figures from StorableInput.
     */
    @Override
    public void read(StorableInput dr) throws IOException {
        super.read(dr);
        if (dr.getVersion() >= 2) {
            try {
                iconFigure = (AbstractFigure) dr.readStorable();
            } catch (IOException e) {
                logger.error("Icon expected.");
                logger.debug("Icon expected.", e);
            }
            if (dr.getVersion() >= 3) {
                recomputeHilightMap();
            }
        }
    }

    @Override
    public synchronized void fillInGraph(GraphLayout layout) {
        FigureEnumeration k = figures();

        while (k.hasMoreElements()) {
            Figure f = k.nextFigure();

            if (f instanceof NodeFigure) {
                layout.addNode(f);
            } else if (f instanceof ConnectionFigure connectionFigure) {
                layout.addEdge(connectionFigure, 20);
            }
        }
    }

    void setHighlightFigure(final FigureWithHighlight node, final Figure fig) {
        Figure oldHighlight = node.getHighlightFigure();

        if (oldHighlight != null) {
            hilightMap.remove(oldHighlight);
        }
        node.setHighlightFigure(fig);
        if (fig != null) {
            hilightMap.put(fig, node);
            fig.addFigureChangeListener(new FigureChangeAdapter() {
                @Override
                public void figureRemoved(FigureChangeEvent e) {
                    setHighlightFigure(node, null);
                }
            });
            node.addFigureChangeListener(new FigureChangeAdapter() {
                @Override
                public void figureRemoved(FigureChangeEvent e) {
                    hilightMap.remove(fig);
                }
            });
        }
    }

    FigureWithHighlight getFigureForHighlight(Figure hilightFig) {
        try {
            return (FigureWithHighlight) hilightMap.get(hilightFig);
        } catch (NoSuchElementException e) {
            return null;
        }
    }

    @Override
    public Dimension defaultSize() {
        return new Dimension(535, 788);
    }

    /**
     * Deserialization method, behaves like default readObject
     * method except restoring default values for transient
     * fields.
     */
    private void readObject(java.io.ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        in.defaultReadObject();


        // The serialization mechanism constructs the object
        // with a less-than-no-arg-constructor, so that the
        // default values for transient fields have to be
        // reinitialized manually.
        hilightMap = new Hashtable<>();
        _figureListeners = new Vector<>();


        // For full functionality we need to recompute some
        // tables.
        recomputeHilightMap();
    }

    /**
     * Recomputes the <code>hilightMap</code> by examining
     * all figures.
     */
    private void recomputeHilightMap() {
        hilightMap.clear();
        addToHilightMap(this);
    }

    /**
     * Adds the highlight figures of all figures contained in the
     * given <code>CompositeFigure</code> to the <code>hilightMap</code>.
     * Used by {@link #recomputeHilightMap}.
     *
     * @param container  CompositeFigure to scan for figures
     * with hilights.
     */
    private void addToHilightMap(CompositeFigure container) {
        FigureEnumeration figenumeration = container.figures();

        while (figenumeration.hasMoreElements()) {
            Figure fig = figenumeration.nextFigure();

            if (fig instanceof FigureWithHighlight) {
                Figure hilight = ((FigureWithHighlight) fig).getHighlightFigure();

                if (hilight != null) {
                    hilightMap.put(hilight, fig);
                }
            }
            if (fig instanceof CompositeFigure compositeFigure) {
                addToHilightMap(compositeFigure);
            }
        }
    }

    @Override
    public String getWindowCategory() {
        return "Nets";
    }


    /**
     * Adds a figure to the list of figures
     * (as the superclass does).
     *
     * If the figure implements the interface<code>
     * CH.ifa.draw.framework.FigureWithID</code>, checks
     * its ID and assigns a new one if needed.
     *
     * Also notifies the figureListeners that a figure was added.
     */
    @Override
    public Figure add(Figure figure) {
        Figure result = super.add(figure);

        // If the figure can hilight other figures, put
        // its highlight figure into the map (if there
        // is any).
        if (figure instanceof FigureWithHighlight) {
            Figure hilight = ((FigureWithHighlight) figure).getHighlightFigure();

            if (hilight != null) {
                hilightMap.put(hilight, figure);
            }
        }


        // If a CompositeFigure is added, it may
        // contain a lot of other figures.
        if (figure instanceof CompositeFigure) {
            recomputeHilightMap();
        }

        notifyListeners(figure, true);

        return result;
    }

    //------------------------------------------------------------------------------   
    public static FilterContainer getFilterContainer() {
        // previously there was an unassigned field which may be returned instead
        // but as its never assigned, just always return a new instance
        return new FilterContainer(FileFilterCreator.createRNWFileFilter());
    }

    @Override
    public ExtensionFileFilter getDefaultFileFilter() {
        return getFilterContainer().getDefaultFileFilter();
    }

    @Override
    public HashSet<ExtensionFileFilter> getImportFileFilters() {
        return getFilterContainer().getImportFileFilters();
    }

    @Override
    public HashSet<ExtensionFileFilter> getExportFileFilters() {
        return getFilterContainer().getExportFileFilters();
    }

    /* (non-Javadoc)
     * @see de.renew.draw.storables.ontology.Drawing#getDefaultExtension()
     */
    @Override
    public String getDefaultExtension() {
        return getDefaultFileFilter().getExtension();
    }

    /**
     * Adds CPNFigureListener
     *
     * @param listener the added listener
     */
    public void addFigureListener(CPNFigureListener listener) {
        _figureListeners.add(listener);
    }

    /**
     * Removes a CPNFigureListener
     *
     * @param listener the removed listener
     */
    public void removeFigureListener(CPNFigureListener listener) {
        _figureListeners.remove(listener);
    }

    private void notifyListeners(Figure figure, boolean add) {
        for (int i = 0; i < _figureListeners.size(); i++) {
            if (add) {
                _figureListeners.elementAt(i).cpnFigureAdded(new CPNFigureEvent(this, figure));
            } else {
                _figureListeners.elementAt(i).cpnFigureRemoved(new CPNFigureEvent(this, figure));
            }
        }
    }
}
