package CH.ifa.draw.figures;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serial;
import java.util.Enumeration;
import java.util.Vector;

import CH.ifa.draw.DrawPlugin;
import CH.ifa.draw.application.DrawApplication;
import CH.ifa.draw.standard.FigureEnumerator;
import CH.ifa.draw.standard.MergedFigureEnumerator;
import CH.ifa.draw.standard.RelativeLocator;
import CH.ifa.draw.util.ColorMap;
import de.renew.draw.storables.api.StorableApi;
import de.renew.draw.storables.ontology.Figure;
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.storables.ontology.StorableInput;
import de.renew.draw.storables.ontology.StorableOutput;
import de.renew.draw.ui.ontology.DrawingView;
import de.renew.draw.ui.ontology.FigureHandle;

/**
 * The CompositeAttributeFigure aggregates a collection of figures.
 * The set can be conveniently moved and selected (double-click) by mouse.
 * Most of its functionality has been extracted from the NetComponentFigure.
 *
 * @author Lawrence Cabac
 * @author David Mosteller
 */
public class CompositeAttributeFigure extends AttributeFigure implements FigureChangeListener {

    /**
     * Logger for this class
     */
    public static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(CompositeAttributeFigure.class);

    /**
     * Vector of Figures attached to this <code>CompositeAttributeFigure</code>.
     */
    protected Vector<Figure> attached = new Vector<>();

    /**
     * Flag indicating whether this Figure should be automatically removed
     * when the Vector of attached Figures becomes empty.
     */
    protected boolean removeWhenEmpty = true;

    /**
     * Create a new CompositeAttributeFigure.
     */
    public CompositeAttributeFigure() {
        setFillColor(ColorMap.NONE);
        setFrameColor(ColorMap.NONE);
    }

    /**
     * Determines whether the figure is contained in this figure collection.
     */
    @Override
    public boolean includes(Figure figure) {
        return getAttached().contains(figure);
    }

    /**
     * Draws a frame around this figure collection.
     */
    @Override
    public void drawFrame(Graphics g) {
        Rectangle r = displayBox();
        Graphics2D g2 = (Graphics2D) g;
        Shape s = new Rectangle2D.Float(r.x, r.y, r.width, r.height);
        g2.draw(s);
    }

    /**
     * Draw a background behind this figure collection.
     */
    @Override
    public void drawBackground(Graphics g) {
        Rectangle r = displayBox();
        Graphics2D g2 = (Graphics2D) g;
        Shape s = new Rectangle2D.Float(r.x, r.y, r.width, r.height);
        g2.fill(s);
    }

    /**
     * Move this figure collection along the x-axis by dx and along the y-axis by dy.
     */
    @Override
    protected void basicMoveBy(int dx, int dy) {

        // do not trigger invalidation of the net component.
        // do not do this in one loop because of the transitive listener
        // dependencies (moving a transition causes the connected arcs to
        // change)
        for (Figure af : attached) {
            af.removeFigureChangeListener(this);
        }
        for (Figure af : attached) {

            af.moveBy(dx, dy);

        }
        for (Figure af : attached) {
            af.addFigureChangeListener(this);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see CH.ifa.draw.standard.AbstractFigure#basicDisplayBox(java.awt.Point,
     *      java.awt.Point)
     */
    @Override
    public void basicDisplayBox(Point origin, Point corner) {
        // TODO Auto-generated method stub
    }

    /**
     * Gets the display box. The display box is defined as the union of the
     * contained figures.
     */
    @Override
    public Rectangle displayBox() {
        Enumeration<Figure> k = attached.elements();
        Rectangle r = new Rectangle();
        if (k.hasMoreElements()) {
            r = (k.nextElement()).displayBox();
        }
        while (k.hasMoreElements()) {
            r.add((k.nextElement()).displayBox());
        }
        if (r == null) {
            return new Rectangle(0, 0);
        }
        return r;
    }

    /**
     * Gets the handles for the CompositeAttributeFigure.
     * <p>
     * {@link CH.ifa.draw.standard.AbstractFigure#handles()}
     */
    @Override
    public Vector<FigureHandle> handles() {
        Vector<FigureHandle> handles = new Vector<>();
        handles.addElement(new GroupHandle(this, RelativeLocator.northWest()));
        handles.addElement(new GroupHandle(this, RelativeLocator.northEast()));
        handles.addElement(new GroupHandle(this, RelativeLocator.southWest()));
        handles.addElement(new GroupHandle(this, RelativeLocator.southEast()));
        return handles;
    }

    /**
     * Returns whether this figure can be selected.
     */
    @Override
    public boolean isSelectable() {
        return true;
    }

    /**
     * Get the DrawingView of the currently active gui, if present.
     *
     * @return DrawingView or null, if the gui is null
     */
    protected DrawingView getView() {
        DrawApplication gui = DrawPlugin.getGui();
        if (gui == null) {
            return null;
        }
        return gui.getView(gui.drawing());

    }

    /**
     * Stores the connector and its owner to a StorableOutput.
     */
    @Override
    public void write(StorableOutput dw) {
        super.write(dw);
        dw.writeInt(attached.size());
        for (Figure figure : attached) {
            dw.writeStorable(figure);
        }
    }

    /**
     * Reads the connector and its owner from a StorableInput.
     */
    @Override
    public void read(StorableInput dr) throws IOException {
        if (dr.getVersion() > 9) {
            super.read(dr);
        }
        attached = new Vector<>();
        int size = dr.readInt();
        if (size != 0) {
            for (int i = 1; i <= size; i++) {
                Figure figure = (Figure) dr.readStorable();
                attached.add(figure);
                figure.addFigureChangeListener(this);
            }
        }

        // manage unset colors -- default for NetComponentFigure is ColoMap.NONE
        Enumeration<String> attributeKeys = getAttributeKeys();
        boolean isFillColorSet = false;
        boolean isFrameColorSet = false;
        while (attributeKeys.hasMoreElements()) {
            String string = attributeKeys.nextElement();
            if (string.equals("FillColor")) {
                isFillColorSet = true;
            }
            if (string.equals("FrameColor")) {
                isFrameColorSet = true;
            }
        }
        if (!isFillColorSet) {
            setFillColor(ColorMap.NONE);
        }
        if (!isFrameColorSet) {
            setFrameColor(ColorMap.NONE);
            setAttribute("LineStyle", AttributeFigure.LINE_STYLE_DOTTED);
        }
    }

    /**
     * Returns a Vector of the Figures attached to this <code>CompositeAttributeFigure</code>.
     *
     * @return a <code>Vector</code> of Figures that are attached to this
     * <code>CompositeAttributeFigure</code>.
     */
    public Vector<Figure> getAttached() {
        return attached;
    }

    /**
     * Inspect this CompositeAttributeFigure (usually by right-click).
     */
    @Override
    public boolean inspect(DrawingView view, boolean alternate) {
        if (alternate) {
            return super.inspect(view, true);
        } else {
            view.clearSelection();
            view.addToSelectionAll(attached);
            return true;
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see de.renew.draw.storables.ontology.FigureChangeListener#figureInvalidated(de.renew.draw.storables.ontology.FigureChangeEvent)
     */
    @Override
    public void figureInvalidated(FigureChangeEvent e) {
        invalidate();
    }

    /*
     * (non-Javadoc)
     *
     * @see de.renew.draw.storables.ontology.FigureChangeListener#figureChanged(de.renew.draw.storables.ontology.FigureChangeEvent)
     */
    @Override
    public void figureChanged(FigureChangeEvent e) {
        changed();
    }

    /*
     * (non-Javadoc)
     *
     * @see de.renew.draw.storables.ontology.FigureChangeListener#figureRemoved(de.renew.draw.storables.ontology.FigureChangeEvent)
     */
    @Override
    public void figureRemoved(FigureChangeEvent e) {
        willChange();
        Figure removed = e.getFigure();
        attached.remove(removed);
        if (removeWhenEmpty && attached.isEmpty() && listener() != null) {
            listener().figureRequestRemove(StorableApi.createFigureChangeEvent(this));
        }
        changed();
    }

    /*
     * (non-Javadoc)
     *
     * @see de.renew.draw.storables.ontology.FigureChangeListener#figureRequestRemove(de.renew.draw.storables.ontology.FigureChangeEvent)
     */
    @Override
    public void figureRequestRemove(FigureChangeEvent e) {
        willChange();
        if (e.getSource() instanceof Figure f)
            attached.remove(f);
        changed();
    }

    /*
     * (non-Javadoc)
     *
     * @see de.renew.draw.storables.ontology.FigureChangeListener#figureRequestUpdate(de.renew.draw.storables.ontology.FigureChangeEvent)
     */
    @Override
    public void figureRequestUpdate(FigureChangeEvent e) {
        System.out.println("NCF: figureRequestUpdate");
        // nothing to do
    }

    /*
     * (non-Javadoc)
     *
     * @see de.renew.draw.storables.ontology.FigureChangeListener#figureHandlesChanged(de.renew.draw.storables.ontology.FigureChangeEvent)
     */
    @Override
    public void figureHandlesChanged(FigureChangeEvent e) {
        // nothing to do
    }

    /**
     * Deserialization method, behaves like default readObject
     * method, but additionally restores the association from
     * contained figures to this composite figure.
     *
     * @param s ObjectInputStream from which the Object will be deserialized
     * @throws ClassNotFoundException if a class required for deserialization cannot be found
     * @throws IOException if an IO error occurs while reading the stream
     **/
    @Serial
    private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException {
        s.defaultReadObject();

        Enumeration<Figure> k = attached.elements();
        while (k.hasMoreElements()) {
            Figure figure = k.nextElement();
            figure.addToContainer(this);
        }
    }

    /**
     * Get a FigureEnumeration containing all figures that are associated with this collection.
     */
    @Override
    public FigureEnumeration getFiguresWithDependencies() {
        return new MergedFigureEnumerator(super.getFiguresWithDependencies(), getAttachedFigures());
    }

    /**
     * Get a FigureEnumeration containing all figures of this collection.
     *
     * @return FigureEnumerator
     */
    private FigureEnumeration getAttachedFigures() {
        return new FigureEnumerator(attached);
    }

    /**
     * Invoke an update of the listeners of all associated figures.
     */
    public void updateListeners() {
        FigureEnumeration figures = getFiguresWithDependencies();
        while (figures.hasMoreElements()) {
            Figure figure = figures.nextFigure();
            figure.addToContainer(this);
        }
    }
}