/*
 * @(#)CompositeFigure.java 5.1
 *
 */

package CH.ifa.draw.standard;

import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serial;
import java.util.Enumeration;
import java.util.Vector;

import CH.ifa.draw.figures.SmoothPolyLineFigure;
import CH.ifa.draw.framework.ChildFigure;
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.Storable;
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.FigureDrawingContext;

/**
 * A Figure that is composed of several figures. A CompositeFigure doesn't
 * define any layout behavior. It is up to subclasses to arrange the contained
 * figures.
 * <hr>
 * <b>Design Patterns</b>
 * <P>
 * <img src="images/red-ball-small.gif" width=6 height=6 alt=" o "> <b><a
 * href=../pattlets/sld012.htm>Composite</a></b><br>
 * CompositeFigure enables to treat a composition of figures like a single
 * figure.<br>
 *
 * @see Figure
 */
public abstract class CompositeFigure extends AbstractFigure implements FigureChangeListener {
    public static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(CompositeFigure.class);

    /*
     * Serialization support.
     */
    @Serial
    private static final long serialVersionUID = 7408153435700021866L;
    private final int _compositeFigureSerializedDataVersion = 1;

    /**
     * The figures that this figure is composed of.
     *
     * @serial
     * @see #add
     * @see #remove
     */
    protected Vector<Figure> fFigures;

    protected CompositeFigure() {
        fFigures = new Vector<>();
    }

    /**
     * Called whenever the set of figures changes.
     */
    protected void figureSetChanged() {}

    /**
     * Adds a figure to the list of figures. Initializes the figure's
     * container.
     */
    public Figure add(Figure figure) {
        if (!fFigures.contains(figure)) {
            fFigures.addElement(figure);
            figure.addToContainer(this);
        }
        figureSetChanged();
        return figure;
    }

    /**
     * Adds a vector of figures.
     *
     * @see #add
     */
    public void addAll(Vector<? extends Figure> newFigures) {
        Enumeration<? extends Figure> k = newFigures.elements();
        while (k.hasMoreElements()) {
            add(k.nextElement());
        }
    }

    /**
     * Removes a figure from the composite.
     *
     * @see #removeAll
     */
    public Figure remove(Figure figure) {
        if (fFigures.contains(figure)) {
            figure.removeFromContainer(this);
            fFigures.removeElement(figure);
            figure.release();
        }
        figureSetChanged();
        return figure;
    }

    /**
     * Removes a vector of figures.
     *
     * @see #remove
     */
    public void removeAll(Vector<? extends Figure> figures) {
        Enumeration<? extends Figure> k = figures.elements();
        while (k.hasMoreElements()) {
            remove(k.nextElement());
        }
    }

    /**
     * Removes all children.
     *
     * @see #remove
     */
    public void removeAll() {
        removeAll(new Vector<>(fFigures));
    }

    /**
     * Removes a figure from the figure list, but doesn't release it. Use this
     * method to temporarily manipulate a figure outside the drawing.
     */
    public synchronized Figure orphan(Figure figure) {
        fFigures.removeElement(figure);
        figureSetChanged();
        return figure;
    }

    /**
     * Removes a vector of figures from the figure's list without releasing the
     * figures.
     *
     * @see #orphan
     */
    public void orphanAll(Vector<? extends Figure> newFigures) {
        Enumeration<? extends Figure> k = newFigures.elements();
        while (k.hasMoreElements()) {
            orphan(k.nextElement());
        }
    }

    /**
     * Replaces a figure in the drawing without removing it from the drawing.
     */
    public synchronized void replace(Figure figure, Figure replacement) {
        int index = fFigures.indexOf(figure);
        if (index != -1) {
            replacement.addToContainer(this); // will invalidate figure
            figure.changed();
            fFigures.setElementAt(replacement, index);
        }
        figureSetChanged();
    }

    /**
     * Sends a figure to the back of the drawing.
     */
    public synchronized void sendToBack(Figure figure) {
        if (fFigures.contains(figure)) {
            fFigures.removeElement(figure);
            fFigures.insertElementAt(figure, 0);
            figure.changed();
        }
    }

    /**
     * Brings a figure to the front.
     */
    public synchronized void bringToFront(Figure figure) {
        if (fFigures.contains(figure)) {
            fFigures.removeElement(figure);
            fFigures.addElement(figure);
            figure.changed();
        }
    }

    /**
     * Draws all the contained figures
     *
     * @see Figure#draw
     */
    @Override
    public void draw(Graphics g) {
        if (isVisible()) {
            Rectangle bounds = g.getClipBounds();

            // Enlarge the computed rectangle by a tolerance area
            // and use the algorithm described below. If the bounds
            // are given as the null value, no clip area is set.
            if (bounds != null) {
                bounds.grow(5, 5);
            }
            FigureEnumeration k = figures();
            while (k.hasMoreElements()) {
                Figure figure = k.nextFigure();
                Rectangle box = figure.displayBox();
                box.grow(1, 1);

                if (bounds == null || box.intersects(bounds)) {
                    figure.draw(g);
                }
            }
        }
    }

    /**
     * Draws the figure in an appearance according to the DrawingContext.
     *
     * @param g the Graphics to draw into
     * @param dc the FigureDrawingContext to obey
     */
    @Override
    public void draw(Graphics g, final FigureDrawingContext dc) {
        if (isVisible() && dc.isVisible(this)) {
            final boolean hilighted = dc.isHighlighted(this);
            FigureDrawingContext containerDC = new FigureDrawingContext() {
                @Override
                public boolean isHighlighted(Figure figure) {
                    if (hilighted) {
                        return true;
                    } else {
                        return dc.isHighlighted(figure);
                    }
                }

                @Override
                public boolean isVisible(Figure figure) {
                    return true;
                }

                @Override
                public String expandMacro(String text) {
                    return dc.expandMacro(text);
                }
            };
            Rectangle bounds = g.getClipBounds();
            if (bounds != null) {
                bounds.grow(5, 5);
            }
            FigureEnumeration k = figures();

            while (k.hasMoreElements()) {
                Figure figure = k.nextFigure();
                Rectangle box = figure.displayBox();
                box.grow(1, 1);

                if (dc.isVisible(figure) && (bounds == null || box.intersects(bounds))) {
                    figure.draw(g, containerDC);
                }
            }
        }
    }

    /**
     * Gets a figure at the given index.
     */
    public Figure figureAt(int i) {
        return fFigures.elementAt(i);
    }

    /**
     * Returns an Enumeration for accessing the contained figures. The figures
     * are returned in the drawing order.
     */
    @Override
    public final FigureEnumeration figures() {
        return new FigureEnumerator(fFigures);
    }

    /**
     * Gets number of child figures.
     */
    public int figureCount() {
        return fFigures.size();
    }

    /**
     * Returns an Enumeration for accessing the contained figures in the reverse
     * drawing order.
     */
    public final FigureEnumeration figuresReverse() {
        return new FilteredFigureEnumerator(
            new ReverseFigureEnumerator(fFigures), Figure::isVisible);
    }

    /**
     * Finds a top level Figure. Use this call for hit detection that should not
     * descend into the figure's children.
     */
    public Figure findFigure(int x, int y) {
        FigureEnumeration k = figuresReverse();
        while (k.hasMoreElements()) {
            Figure figure = k.nextFigure();
            if (figure.containsPoint(x, y)) {
                return figure;
            }
        }
        return null;
    }

    /**
     * Finds a top level Figure that intersects the given rectangle.
     */
    public Figure findFigure(Rectangle r) {
        FigureEnumeration k = figuresReverse();
        while (k.hasMoreElements()) {
            Figure figure = k.nextFigure();
            Rectangle fr = figure.displayBox();
            fr.grow(1, 1);
            if (r.intersects(fr)) {
                return figure;
            }
        }
        return null;
    }

    /**
     * Finds a top level Figure that intersects the given rectangle. It
     * suppresses the passed in figure. Use this method to ignore a figure that
     * is temporarily inserted into the drawing.
     */
    public Figure findFigure(Rectangle r, Figure without) {
        if (without == null) {
            return findFigure(r);
        }
        FigureEnumeration k = figuresReverse();
        while (k.hasMoreElements()) {
            Figure figure = k.nextFigure();
            Rectangle fr = figure.displayBox();
            fr.grow(1, 1);
            if (r.intersects(fr) && !figure.includes(without)) {
                return figure;
            }
        }
        return null;
    }

    /**
     * Finds a top level Figure, but suppresses the passed in figure. Use this
     * method to ignore a figure that is temporarily inserted into the drawing.
     *
     * @param x
     *            the x coordinate
     * @param y
     *            the y coordinate
     * @param without
     *            the figure to be ignored during the find.
     */
    public Figure findFigureWithout(int x, int y, Figure without) {
        if (without == null) {
            return findFigure(x, y);
        }
        FigureEnumeration k = figuresReverse();
        while (k.hasMoreElements()) {
            Figure figure = k.nextFigure();
            if (figure.containsPoint(x, y) && !figure.includes(without)) {
                return figure;
            }
        }
        return null;
    }

    /**
     * Finds a figure but descends into a figure's children. Use this method to
     * implement <i>click-through</i> hit detection, that is, you want to detect
     * the innermost figure containing the given point.
     */
    @Override
    public Figure findFigureInside(int x, int y) {
        FigureEnumeration k = figuresReverse();
        while (k.hasMoreElements()) {
            Figure figure = k.nextFigure().findFigureInside(x, y);
            if (figure != null) {
                return figure;
            }
        }
        return null;
    }

    /**
     * Finds a figure but descends into a figure's children. It suppresses the
     * passed in figure. Use this method to ignore a figure that is temporarily
     * inserted into the drawing.
     */
    public Figure findFigureInsideWithout(int x, int y, Figure without) {
        FigureEnumeration k = figuresReverse();
        while (k.hasMoreElements()) {
            Figure figure = k.nextFigure();
            if (figure != without) {
                Figure found = figure.findFigureInside(x, y);
                if (found != null) {
                    return found;
                }
            }
        }
        return null;
    }

    /**
     * Checks if the composite figure has the argument as one of its children.
     */
    @Override
    public boolean includes(Figure figure) {
        if (super.includes(figure)) {
            return true;
        }

        FigureEnumeration k = figures();
        while (k.hasMoreElements()) {
            Figure f = k.nextFigure();
            if (f.includes(figure)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Moves all the given figures by x and y. Doesn't announce any changes.
     * Subclasses override basicMoveBy. Clients usually call {@link #moveBy}.
     *
     * @throws IllegalStateException throws IllegalStateException if parent figure is being moved while not existing
     */
    @Override
    protected void basicMoveBy(int x, int y) throws IllegalStateException {
        FigureEnumeration k = figures();
        Figure figure;
        Figure parent;
        while (k.hasMoreElements()) {
            figure = k.nextFigure();
            parent = figure;
            if (figure == null) {
                throw new IllegalStateException();
            }
            while (parent != null) {
                if (parent instanceof ChildFigure) {
                    parent = ((ChildFigure) parent).parent();
                    if (parent != null && includes(parent)) {
                        break;
                    }
                } else {
                    parent = null;
                }
            }
            if (parent == null) {
                figure.moveBy(x, y);
            }
        }
    }

    /**
     * Releases the figure and all its children.
     */
    @Override
    public void release() {
        super.release();
        FigureEnumeration k = figures();
        while (k.hasMoreElements()) {
            Figure figure = k.nextFigure();
            figure.release();
        }
    }

    /**
     * Propagates the figureInvalidated event to my listener.
     *
     * @see FigureChangeListener
     */
    @Override
    public void figureInvalidated(FigureChangeEvent e) {
        if (listener() != null) {
            listener().figureInvalidated(e);
        }
    }

    /**
     * Propagates the removeFromDrawing request up to the container.
     *
     * @see FigureChangeListener
     */
    @Override
    public void figureRequestRemove(FigureChangeEvent e) {
        if (listener() != null) {
            listener().figureRequestRemove(StorableApi.createFigureChangeEvent(this));
        }
    }

    /**
     * Propagates the requestUpdate request up to the container.
     *
     * @see FigureChangeListener
     */
    @Override
    public void figureRequestUpdate(FigureChangeEvent e) {
        if (listener() != null) {
            listener().figureRequestUpdate(e);
        }
    }

    @Override
    public void figureChanged(FigureChangeEvent e) {}

    @Override
    public void figureRemoved(FigureChangeEvent e) {}

    @Override
    public void figureHandlesChanged(FigureChangeEvent e) {}

    /**
     * Writes the contained figures to the StorableOutput.
     */
    @Override
    public void write(StorableOutput dw) {
        super.write(dw);
        dw.writeInt(fFigures.size());
        Enumeration<Figure> k = fFigures.elements();
        while (k.hasMoreElements()) {
            Storable nextElement = k.nextElement();

            // Write SmoothPolyLineFigure in form of an equivalent PolyLineFigure
            if (nextElement instanceof SmoothPolyLineFigure smoothFigure) {
                nextElement = smoothFigure.getEquivalentPolylineFigure();
            }

            dw.writeStorable(nextElement);
        }
    }

    /**
     * Reads the contained figures from StorableInput.
     */
    @Override
    public void read(StorableInput dr) throws IOException {
        super.read(dr);
        int size = dr.readInt();
        fFigures = new Vector<>(size);
        for (int i = 0; i < size; i++) {
            try {
                add((Figure) dr.readStorable());
            } catch (IOException e) {
                final String desc = CompositeFigure.class.getSimpleName() + ": could not read in "
                    + Figure.class.getSimpleName() + " object:";
                if (LOGGER.isDebugEnabled()) {
                    // long version
                    LOGGER.error(desc, e);
                } else {
                    // short version
                    LOGGER.error(desc + " " + e);
                }
                break;
            }
        }
    }

    /**
     * Deserialization method, behaves like default readObject method, but
     * additionally restores the association from contained figures to this
     * composite figure.
     **/
    @Serial
    private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException {
        s.defaultReadObject();

        FigureEnumeration k = figures();
        while (k.hasMoreElements()) {
            Figure figure = k.nextFigure();
            figure.addToContainer(this);
        }
    }

    @Override
    public boolean inspect(DrawingView view, boolean alternate) {
        Point mouse = view.lastClick();
        Figure f = findFigure(mouse.x, mouse.y);
        if (f != null && f.inspect(view, alternate)) {
            return true;
        } else {
            return super.inspect(view, alternate);
        }
    }
}