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

package CH.ifa.draw.standard;

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Point;
import java.awt.Rectangle;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serial;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Objects;
import java.util.Vector;

import bibliothek.gui.dock.common.MultipleCDockableLayout;
import bibliothek.util.xml.XElement;

import CH.ifa.draw.framework.DrawingChangeEvent;
import CH.ifa.draw.framework.FigureWithID;
import CH.ifa.draw.framework.FilterContainer;
import de.renew.draw.storables.api.StorableApi;
import de.renew.draw.storables.ontology.Drawing;
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.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.FigureHandle;
import de.renew.io.api.FileApi;
import de.renew.ioontology.ExtensionFileFilter;
import de.renew.ioontology.FileType;
import de.renew.util.Lock;


/**
 * The standard implementation of the Drawing interface.
 *
 * @see Drawing
 */
public class StandardDrawing extends CompositeFigure implements Drawing, MultipleCDockableLayout {
    public static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(StandardDrawing.class);
    private static FilterContainer _filterContainer;
    static final String UNTITLED = "untitled";
    /*
     * Serialization support
     */
    @Serial
    private static final long serialVersionUID = -2602151437447962046L;
    private final int _drawingSerializedDataVersion = 1;

    /**
     * The registered listeners.
     * <p>
     * Transient, listeners must re-register themselves
     * after deserialization.
     * </p>
     */
    private transient Vector<DrawingChangeListener> _listeners;

    /**
     * Lock that blocks concurrent accesses to the drawing.
     * Unlike the previous solution, this lock counts the number of locks
     * and unlocks and will not prematurely regrant the lock
     * in the case of multiple locks from the same thread.
     */
    private transient Lock _lock;

    /**
     * The name of the drawing, as it gets displayed and
     * can be used for references.
     * @serial
     **/
    private String _drawingName = UNTITLED;

    /**
     * The named of the drawing, augmented by path and extension.
     * This information is used to save the drawing. Therefore,
     * it doesn't make sense to keep it on serialization.
     **/
    private transient File _drawingFileName = null;

    /**
     * Tells whether the drawing was modified since the
     * last save. Therefore, it doesn't make sense to keep
     * the information on serialization.
     **/
    private transient boolean _modified = false;

    /**
     * Tells whether a backup was created on the last save.
     * If <code>false</code>, a backup copy of an old file
     * with the same name will be made before the current
     * version gets written on the next save. Therefore,
     * it doesn't make sense to keep the information on
     * serialization.
     **/
    private transient boolean _backupStatus = false;

    // ---- ID-Management -----------------------------------    

    /**
     * Caches the greatest used ID by any known figure.
     * Updated by <code>recomputeIDCache()</code>.
     * This field is transient because it is only a cache.
     */
    protected transient int _maxUsedId = FigureWithID.NOID;
    /**
     * Caches used IDs in this and all subfigures.
     * Updated by <code>recomputeIDCache()</code>.
     * This field is transient because it is only a cache.
     */
    protected transient Hashtable<Integer, Figure> _usedIds = new Hashtable<>();

    /**
     * Constructs the Drawing.
     */
    public StandardDrawing() {
        super();
        _listeners = new Vector<>(2);
        _lock = new Lock();
    }

    @Override
    public String getName() {
        return _drawingName;
    }

    @Override
    public void setName(String name) {
        _drawingName = name;
    }

    public Dimension getSize() {
        return new Dimension(getBounds().width, getBounds().height);
    }

    @Override
    public File getFilename() {
        return _drawingFileName;
    }

    @Override
    public void setFilename(File filename) {
        _drawingFileName = filename;
    }

    /**
     * Return true, if a backup has been made since the
     * drawing has been generated.
     */
    @Override
    public boolean getBackupStatus() {
        return _backupStatus;
    }

    /**
     * Inform the drawing that a backup has been generated.
     */
    @Override
    public void setBackupStatus(boolean status) {
        _backupStatus = status;
    }

    /**
     * Adds a listener for this drawing.
     */
    @Override
    public void addDrawingChangeListener(DrawingChangeListener listener) {
        _listeners.addElement(listener);
    }

    /**
     * Removes a listener from this drawing.
     */
    @Override
    public void removeDrawingChangeListener(DrawingChangeListener listener) {
        _listeners.removeElement(listener);
    }

    /**
     * Adds a listener for this drawing.
     */
    @Override
    public Enumeration<DrawingChangeListener> drawingChangeListeners() {
        return _listeners.elements();
    }

    /**
     * Removes the figure from the drawing and releases it.
     * Also checks if the ID cache has to be updated.
     */
    @Override
    public Figure remove(Figure figure) {
        if (figure instanceof FigureWithID) {
            freeID((FigureWithID) figure);
        }

        // ensure that we remove the top level figure in a drawing
        Figure result = null;
        if (figure.listener() != null) {
            figure.listener()
                .figureRequestRemove(StorableApi.createFigureChangeEvent(figure, null));
            result = figure;
        }

        if (figure instanceof CompositeFigure) {
            recomputeIDCache();
        }
        return result;
    }

    /**
     * Handles a removeFromDrawing request that
     * is passed up the figure container hierarchy.
     * @see FigureChangeListener
     */
    @Override
    public void figureRequestRemove(FigureChangeEvent e) {
        Figure figure = e.getFigure();
        if (fFigures.contains(figure)) {
            _modified = true;
            fFigures.removeElement(figure);
            figure.removeFromContainer(this); // will invalidate figure
            figure.release();
        } else {
            LOGGER.error("Attempt to remove non-existing figure");
        }
    }

    /**
     * Invalidates a rectangle and merges it with the
     * existing damaged area.
     * @see FigureChangeListener
     */
    @Override
    public void figureInvalidated(FigureChangeEvent e) {
        _modified = true;
        if (_listeners != null) {
            for (int i = 0; i < _listeners.size(); i++) {
                DrawingChangeListener l = _listeners.elementAt(i);
                l.drawingInvalidated(new DrawingChangeEvent(this, e.getInvalidatedRectangle()));
            }
        }
    }

    /**
     * Forces an update
     */
    @Override
    public void figureRequestUpdate(FigureChangeEvent e) {
        if (_listeners != null) {
            for (int i = 0; i < _listeners.size(); i++) {
                DrawingChangeListener l = _listeners.elementAt(i);
                l.drawingRequestUpdate(new DrawingChangeEvent(this, null));
            }
        }
    }

    /**
     * Checks whether the drawing has some accumulated damage
     * and informs all views about the required update,
     * if necessary.
     */
    @Override
    public synchronized void checkDamage() {
        final StandardDrawing object = this;
        EventQueue.invokeLater(() -> {
            Enumeration<DrawingChangeListener> each = _listeners.elements();
            while (each.hasMoreElements()) {
                DrawingChangeListener l = each.nextElement();
                l.drawingRequestUpdate(new DrawingChangeEvent(object, null));
            }
        });
    }

    /**
     * Return's the figure's handles. This is only used when a drawing
     * is nested inside another drawing.
     */
    @Override
    public Vector<FigureHandle> handles() {
        Vector<FigureHandle> handles = new Vector<>();
        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;
    }

    /**
     * Gets the display box. This is the union of all figures.
     */
    @Override
    public Rectangle displayBox() {
        Rectangle box = null;
        FigureEnumeration k = figures();
        while (k.hasMoreElements()) {
            Figure f = k.nextFigure();
            if (f.isVisible()) {
                Rectangle r = f.displayBox();
                if (box == null) {
                    box = r;
                } else {
                    box.add(r);
                }
            }
        }
        if (box == null) {
            return new Rectangle(0, 0, 100, 100);
        } else {
            return new Rectangle(box.x - 10, box.y - 10, box.width + 20, box.height + 20);
        }
    }

    @Override
    public void basicDisplayBox(Point p1, Point p2) {}

    /**
     * Acquires the drawing lock.
     */
    @Override
    public void lock() {
        _lock.lock();
    }

    /**
     * Releases the drawing lock.
     */
    @Override
    public void unlock() {
        _lock.unlock();
    }

    @Serial
    private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException {
        s.defaultReadObject();

        _listeners = new Vector<>(2);
        _lock = new Lock();


        // 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.
        _maxUsedId = FigureWithID.NOID;
        _usedIds = new Hashtable<>();


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

    /**
     * Returns whether drawing has been modified since last save.
     */
    @Override
    public boolean isModified() {
        return _modified;
    }

    @Override
    public void clearModified() {
        _modified = false;
    }

    @Override
    public Rectangle getBounds() {
        return displayBox();
    }

    @Override
    public Dimension defaultSize() {
        return new Dimension(430, 406);
    }

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

    /**
     * Reads the contained figures from StorableInput.
     */
    @Override
    public void read(StorableInput dr) throws IOException {
        super.read(dr);
        _modified = false;
        recomputeIDCache();
    }

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

    //------------------------------------------------------------------------------
    static public FilterContainer getFilterContainer() {
        return Objects.requireNonNullElseGet(
            _filterContainer,
            () -> new FilterContainer(FileApi.createExtensionFileFilter(FileType.DRAW)));
    }

    @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();
    }

    @Override
    public void init() {}

    @Override
    public Drawing add(Drawing drawing) {
        FigureEnumeration figures = drawing.figures();
        while (figures.hasMoreElements()) {
            Figure figure = figures.nextElement();
            add(figure);
        }
        return drawing;
    }

    @Override
    public Drawing add(Drawing drawing, int x, int y) {
        FigureEnumeration figures = drawing.figures();
        while (figures.hasMoreElements()) {
            Figure figure = figures.nextElement();
            add(figure);
            figure.moveBy(x, y);
        }
        return drawing;
    }

    /**
     * Adds a figure to the list of figures
     * (as the superclass does).
     * <br>
     * If the figure implements the interface
     * <code>CH.ifa.draw.framework.FigureWithID</code>,
     * checks its ID and assigns a new one if needed.
     */
    @Override
    public Figure add(Figure figure) {
        // The ID check has to be done before the
        // figure is added to the list to avoid
        // collision of the figure with itself.
        // If figure is capable of holding an ID,
        // check its ID and assign a new one, if
        // needed.
        if (figure instanceof FigureWithID) {
            // If the cache is uninitialized,
            // recompute its value.
            if (_maxUsedId == FigureWithID.NOID) {
                recomputeIDCache();
            }
            checkAndAssignID((FigureWithID) figure);

            // Now the figure can be added to the list.
        }
        Figure result = super.add(figure);

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

        return result;
    }

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

    @Override
    public void writeStream(DataOutputStream dataOutputStream) {

    }

    @Override
    public void readStream(DataInputStream dataInputStream) {

    }

    @Override
    public void writeXML(XElement xElement) {

    }

    @Override
    public void readXML(XElement xElement) {

    }

    /**
     * Checks the figure if it has already a
     * legal ID. If necessary, generates new
     * unique ID and assigns it.
     */
    private void checkAndAssignID(FigureWithID figure) {
        int oldID = figure.getID();
        int newID = oldID;
        FigureWithID inCache = (FigureWithID) _usedIds.get(oldID);

        if ((inCache != null) && (inCache != figure)) {
            // The old ID is already used by another figure,
            // so reset it temporarily to NOID.
            newID = FigureWithID.NOID;
        }
        if (newID == FigureWithID.NOID) {
            newID = newUniqueID();
            figure.setID(newID);
        }
        _usedIds.put(newID, figure);
    }

    /**
     * Generates a new ID not used in the list of figures.
     * @see CH.ifa.draw.framework.FigureWithID
     */
    private int newUniqueID() {
        _maxUsedId++;

        if (_usedIds.containsKey(_maxUsedId)) {
            boolean resetOnce = false;

            while (_usedIds.containsKey(_maxUsedId)) {
                _maxUsedId++;

                if (_maxUsedId == Integer.MIN_VALUE) {
                    _maxUsedId = 1;

                    if (resetOnce) {
                        throw new RuntimeException("Maximum number of figures exceeded.");
                    } else {
                        resetOnce = true;
                    }
                }
            }
        }

        return _maxUsedId;
    }

    /**
     * Recomputes the ID cache (maxUsedID and usedIDs)
     * and eliminates IDs used more than once.
     */
    protected void recomputeIDCache() {
        // To ensure that no ID will be reassigned
        // even after it has been freed, do never
        // reset the greatest ID cache.
        // However, after closing the drawing, this
        // value will be reset anyway...
        // maxUsedID = FigureWithID.NOID;
        Vector<Figure> offendingFigures = new Vector<>();

        _usedIds.clear();

        addToIDCache(this, offendingFigures);

        Enumeration<Figure> figureList = offendingFigures.elements();

        while (figureList.hasMoreElements()) {
            checkAndAssignID((FigureWithID) figureList.nextElement());
        }
    }

    /**
     * Do not call this method directly, use
     * <code>recomputeIDCache()</code> instead.
     * @param container CompositeFigure to scan for figures with ID.
     * @param offendingFigures Collects figures with illegal IDs.
     */
    private void addToIDCache(CompositeFigure container, Vector<Figure> offendingFigures) {
        FigureEnumeration knownFigures = container.figures();
        Figure figure;
        int usedID;
        FigureWithID inCache;


        // Iterate through all known Figures and update the
        // greatest seen ID.
        // Also update the Hashtable of used IDs and check if
        // some ID is already used by some other figure.
        // If there are CompositeFigures contained in the
        // drawing (like GroupFigures), recurse into them.
        while (knownFigures.hasMoreElements()) {
            figure = knownFigures.nextFigure();
            if (figure instanceof FigureWithID) {
                usedID = ((FigureWithID) figure).getID();
                if (usedID == FigureWithID.NOID) {
                    offendingFigures.addElement(figure);
                } else {
                    inCache = (FigureWithID) _usedIds.get(usedID);
                    if ((inCache == null) || (inCache == figure)) {
                        _usedIds.put(usedID, figure);
                        if (usedID > _maxUsedId) {
                            _maxUsedId = usedID;
                        }
                    } else {
                        // An ID is used twice.This will be silently corrected
                        // by the caller of this method.
                        // logger.debug("ID used more than once: " + usedID);
                        offendingFigures.addElement(figure);
                    }
                }
            } else if (figure instanceof CompositeFigure) {
                addToIDCache((CompositeFigure) figure, offendingFigures);
            }
        }
    }

    /**
     * Tries to assign a given id to the figure.
     */
    public void assignID(FigureWithID figure, int id) {
        // check if the new id can be given to the figure:        
        FigureWithID inCache = (FigureWithID) _usedIds.get(id);
        if ((inCache != null) && (inCache != figure)) {
            throw new IllegalArgumentException("The id is already in use!");
        }

        // remove old mapping from cache:
        _usedIds.remove(figure.getID());

        // set new id and add to cache:
        figure.setID(id);
        _usedIds.put(id, figure);
    }

    /**
     * Frees up the ID used by the given figure.
     */
    private void freeID(FigureWithID figure) {
        // Check if the ID was really cached
        // before freeing it up.
        Integer usedID = figure.getID();

        if (_usedIds.get(usedID) == figure) {
            _usedIds.remove(usedID);
        }
    }

    /**
     * Returns the FigureWithID currently assigned to
     * the given ID. If no figure is assigned to that
     * ID, returns <code>null</code>.
     * <p>
     * It is possible that the returned figure is not
     * contained in the <code>figures()</code> enumeration.
     * Then it was found in some CompositeFigure contained
     * in the enumeration.
     * </p>
     * @see CH.ifa.draw.framework.FigureWithID
     */
    public FigureWithID getFigureWithID(int id) {
        return (FigureWithID) _usedIds.get(id);
    }
}
