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

package CH.ifa.draw.figures;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.RoundRectangle2D;
import java.io.IOException;
import java.io.Serial;
import java.util.Objects;
import java.util.Vector;

import CH.ifa.draw.standard.BoxHandleKit;
import de.renew.draw.storables.ontology.Connector;
import de.renew.draw.storables.ontology.StorableInput;
import de.renew.draw.storables.ontology.StorableOutput;
import de.renew.draw.ui.ontology.FigureHandle;

/**
 * A round rectangle figure.
 * <p>
 * {@link RadiusHandle}
 */
public class RoundRectangleFigure extends AttributeFigure {
    private static final int DEFAULT_ARC = 8;
    private static final double DEFAULT_RATIO = 0.1;

    /**
     * The name of the attribute that contains a boolean
     * value determining if the corner arc's radius
     * should be scaled when the rectangle's size changes.
     **/
    public static final String ARC_SCALE_ATTR = "ArcScale";
    /*
     * Serialization support.
     */
    @Serial
    private static final long serialVersionUID = 7907900248924036885L;

    /**
     * Determines position and size of the rectangle by
     * specifying position and size of its bounding box.
     *
     * @serial
     **/
    private Rectangle _displayBox = null;

    /**
     * The absolute horizontal radius of the corner arcs.
     *
     * @serial
     **/
    private int _arcWidth;

    /**
     * The absolute vertical radius of the corner arcs.
     *
     * @serial
     **/
    private int _arcHeight;

    /**
     * The relative horizontal radius of the corner arcs,
     * given as fraction of the rectangle width.
     * The value must conform to {@link #_arcWidth}.
     * <p>
     * This field is transient because it can be derived
     * from <code>_arcWidth</code> on deserialization.
     * </p>
     **/
    private transient double _xRatio;

    /**
     * The relative vertical radius of the corner arcs,
     * given as fraction of the rectangle width.
     * The value must conform to {@link #_arcHeight}.
     * <p>
     * This field is transient because it can be derived
     * from <code>_arcHeight</code> on deserialization.
     * </p>
     **/
    private transient double _yRatio;

    /**
     * Version number for the serialized data format of RoundRectangleFigures.
     */
    @SuppressWarnings("unused")
    private final int _roundRectangleSerializedDataVersion = 1;

    /**
     * Constructor for the Class RoundRectangleFigure
     * Initializes the round rectangle with default arc sizes.
     */
    public RoundRectangleFigure() {
        this(new Point(0, 0), new Point(0, 0));
        _arcWidth = _arcHeight = DEFAULT_ARC;
        _xRatio = _yRatio = DEFAULT_RATIO;
    }

    /**
     * Constructor for the Class RoundRectangleFigure
     * Initializes the round rectangle with specified origin and corner points.
     *
     * @param origin The starting point of the rectangle.
     * @param corner The opposite corner point of the rectangle.
     */
    public RoundRectangleFigure(Point origin, Point corner) {
        _arcWidth = _arcHeight = DEFAULT_ARC;
        _xRatio = (double) _arcWidth / (double) (corner.x - origin.x);
        _yRatio = (double) _arcHeight / (double) (corner.y - origin.y);
        basicDisplayBox(origin, corner);
    }

    @Override
    public void basicDisplayBox(Point origin, Point corner) {
        willChange();
        _displayBox = new Rectangle(origin);
        _displayBox.add(corner);
        if (scaleArc()) {
            _arcWidth = (int) ((corner.x - origin.x) * _xRatio);
            _arcHeight = (int) ((corner.y - origin.y) * _yRatio);
        } else {
            recalculateRatio();
        }
        handlesChanged();
        changed();
    }

    /**
     * Recalculates {@link #_xRatio} and {@link #_yRatio} so that
     * they conform to {@link #_arcWidth} and {@link #_arcHeight}.
     * Should be called after each modification to fArcWidth or
     * fArcHeight.
     **/
    private void recalculateRatio() {
        _xRatio = (double) getArcWidth() / (double) _displayBox.width;
        _yRatio = (double) getArcHeight() / (double) _displayBox.height;
    }

    /**
     * Tells whether the corner arc's radius's should be scaled
     * when the rectangle's size changes. Evaluates the attribute
     * <code>ARC_SCALE_ATTR</code>, defaults to <code>false</code>.
     **/
    private boolean scaleArc() {
        Boolean attr = (Boolean) getAttribute(ARC_SCALE_ATTR);
        return Objects.requireNonNullElse(attr, false);
    }

    /**
     * Sets the arc's width and height.
     *
     * @param width The value to which the arc's width/horizontal radius will be set
     * @param height The value to which the arc's height/vertical radius will be set
     */
    public void setArc(int width, int height) {
        willChange();
        _arcWidth = width;
        _arcHeight = height;
        recalculateRatio();
        changed();
    }

    /**
     * Returns the absolute horizontal radius of the corner arcs. If this value exceeds the rectangle's width, returns the latter.
     *
     * @return The absolute horizontal radius of the corner arcs or the rectangle's with, in case the arc is too large
     */
    public int getArcWidth() {
        // Make sure that the stored arc dimensions, which
        // might be too big in preparation for a later
        // enlargement, are not passed naively to the environment.
        return Math.min(_arcWidth, _displayBox.width);
    }

    /**
     * Returns the absolute vertical radius of the corner arcs.
     * If this value exceeds the rectangle's height, returns the latter.
     *
     * @return The absolute vertical radius of the corner arcs or the rectangle's height, in case the arc is too large
     */
    public int getArcHeight() {
        return Math.min(_arcHeight, _displayBox.height);
    }

    /**
     * Gets the arc's width and height.
     *
     * @return Point at the associated coordinates
     */
    public Point getArc() {
        return new Point(getArcWidth(), getArcHeight());
    }

    @Override
    public Vector<FigureHandle> handles() {
        Vector<FigureHandle> handles = new Vector<>();
        BoxHandleKit.addHandles(this, handles);

        handles.addElement(new RadiusHandle(this));

        return handles;
    }

    @Override
    public Rectangle displayBox() {
        return new Rectangle(_displayBox.x, _displayBox.y, _displayBox.width, _displayBox.height);
    }

    @Override
    protected void basicMoveBy(int x, int y) {
        _displayBox.translate(x, y);
    }

    @Override
    public void drawBackground(Graphics g) {
        Rectangle r = displayBox();
        Shape s =
            new RoundRectangle2D.Float(r.x, r.y, r.width, r.height, getArcWidth(), getArcHeight());
        ((Graphics2D) g).fill(s);
    }

    @Override
    public void drawFrame(Graphics g) {
        Rectangle r = displayBox();
        Shape s =
            new RoundRectangle2D.Float(r.x, r.y, r.width, r.height, getArcWidth(), getArcHeight());
        ((Graphics2D) g).draw(s);
    }

    @Override
    public Insets connectionInsets() {
        return new Insets(
            getArcWidth() / 2, getArcHeight() / 2, getArcWidth() / 2, getArcHeight() / 2);
    }

    @Override
    public Connector connectorAt(int x, int y) {
        return new ChopRoundRectangleConnector(this); // just for demo purposes
    }

    @Override
    public void write(StorableOutput dw) {
        super.write(dw);
        dw.writeInt(_displayBox.x);
        dw.writeInt(_displayBox.y);
        dw.writeInt(_displayBox.width);
        dw.writeInt(_displayBox.height);
        dw.writeInt(_arcWidth);
        dw.writeInt(_arcHeight);
    }

    @Override
    public void read(StorableInput dr) throws IOException {
        super.read(dr);
        _displayBox = new Rectangle(dr.readInt(), dr.readInt(), dr.readInt(), dr.readInt());
        _arcWidth = dr.readInt();
        _arcHeight = dr.readInt();
        recalculateRatio();
    }

    /**
     * Deserialization method, behaves like default readObject
     * method except recalculating the transient fields
     * <code>fXRatio</code> and <code>fYRatio</code>.
     *
     * @param in The Stream used to deserialize the object
     * @throws IOException if there is an error reading the InputStream
     * @throws ClassNotFoundException if the class is not available on the classpath
     **/
    @Serial
    private void readObject(java.io.ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        in.defaultReadObject();
        recalculateRatio();
    }
}
