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

package CH.ifa.draw.figures;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.io.IOException;
import java.io.Serial;
import java.util.Enumeration;
import java.util.Objects;

import org.freehep.graphicsio.HyperrefGraphics;

import CH.ifa.draw.framework.FigureWithID;
import CH.ifa.draw.standard.AbstractFigure;
import CH.ifa.draw.util.BSpline;
import CH.ifa.draw.util.ColorMap;
import de.renew.draw.storables.ontology.Figure;
import de.renew.draw.storables.ontology.StorableInput;
import de.renew.draw.storables.ontology.StorableOutput;
import de.renew.draw.ui.ontology.FigureDrawingContext;
import de.renew.draw.ui.ontology.FigureHandle;

/**
 * A figure that can keep track of an open-ended set of attributes.
 * The attributes are stored in a dictionary implemented by
 * FigureAttributes.
 * <p>
 * {@link Figure}
 * {@link FigureHandle}
 * {@link FigureAttributes}
 */

// It is assumed by PolyLineFigure that AttributeFigure
// inherits directly from AbstractFigure.
public abstract class AttributeFigure extends AbstractFigure implements FigureWithID {

    /**
     * Name of the attribute holding the ID required by
     * interface FigureWithID.
     * The ID is stored as an attribute to avoid changing
     * the data format written to streams.
     * <p>
     * {@link FigureWithID}
     **/
    private static final String ID_ATTR = "FigureWithID";
    /*
     * Serialization support.
     */
    @Serial
    private static final long serialVersionUID = -10857585979273442L;
    /**
     * The default attributes associated with a figure.
     * If a figure doesn't have an attribute set, a default
     * value from this shared attribute set is returned.
     * <p>
     * {@link #getAttribute}
     * {@link #setAttribute}
     */
    private static FigureAttributes _defaultAttributes = null;

    /**
     * Default value for line style / solid line.
     */
    public static final String LINE_STYLE_NORMAL = "";

    /**
     * Draw 1 unit, skip 2.
     */
    public static final String LINE_STYLE_DOTTED = "1 2";

    /**
     * Dash for 10 units, skip 10.
     */
    public static final String LINE_STYLE_DASHED = "10";

    /**
     * Dash for 15 units, skip 10.
     */
    public static final String LINE_STYLE_MEDIUM_DASHED = "15 10";

    /**
     * Dash for 20 units, skip 20.
     */
    public static final String LINE_STYLE_LONG_DASHED = "20";

    /**
     * Dash for 7 units, skip 3, dot for 1, skip 3.
     */
    public static final String LINE_STYLE_DASH_DOTTED = "7 3 1 3";

    /**
     * Key for the line width.
     */
    public static final String LINE_WIDTH_KEY = "LineWidth";

    /**
     * Default line width.
     */
    public static final Integer LINE_WIDTH_DEFAULT = 1;
    /**
     * The attributes of a figure. Each figure can have
     * an open-ended set of attributes. Attributes are
     * identified by name.
     * <p>
     * {@link #getAttribute}
     * {@link #setAttribute}
     *
     * @serial
     */
    private FigureAttributes _attributes;

    /**
     * Serial number for the serialized data format of AttributeFigures.
     */
    @SuppressWarnings("unused")
    private final int _attributeFigureSerializedDataVersion = 1;

    /**
     * Constructor of the class AttributeFigure. Creates a new AttributeFigure.
     */
    protected AttributeFigure() {}

    /**
     * Gets the default value for a named attribute.
     *
     * @param name attribute name
     * @return default attribute for given name
     * {@link #getAttribute}
     */
    public static Object getDefaultAttribute(String name) {
        if (_defaultAttributes == null) {
            initializeAttributes();
        }
        return _defaultAttributes.get(name);
    }

    /**
     * Draws the figure's background and frame using the current fill color, frame color, line width, and line style.
     *
     * @param g graphics used to draw
     */
    public void internalDraw(Graphics g) {
        Color fill = getFillColor();
        if (!ColorMap.isTransparent(fill)) {
            g.setColor(fill);
            drawBackground(g);
        }
        Color frame = getFrameColor();
        if (!ColorMap.isTransparent(frame)) {
            g.setColor(frame);
            Graphics2D g2 = (Graphics2D) g;
            String lineStyle = getLineStyle();
            BasicStroke stroke = (BasicStroke) g2.getStroke();
            BasicStroke bs = new BasicStroke(
                getLineWidth(), stroke.getEndCap(), stroke.getLineJoin(), stroke.getMiterLimit(),
                lineStyle2ArrayOfFloat(lineStyle), stroke.getDashPhase());
            g2.setStroke(bs);
            drawFrame(g2);
            g2.setStroke(stroke);
        }
    }

    /**
     * Draws the figure in the given graphics. Draw is a template
     * method calling drawBackground followed by drawFrame.
     * <p>
     * If it is sometimes required to override this method,
     * make internalDraw() more public and override that method.
     */
    @Override
    public void draw(Graphics g) {
        if (isVisible()) {
            if (g instanceof HyperrefGraphics && hasAttribute("targetLocation")) {
                ((HyperrefGraphics) g)
                    .drawLink(displayBox(), (String) getAttribute("targetLocation"));
            }
            internalDraw(g);
            if (g instanceof HyperrefGraphics && hasAttribute("targetLocation")) {
                ((HyperrefGraphics) g).drawLinkEnd();
            }
        }
    }

    /**
     * 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, FigureDrawingContext dc) {
        if (!dc.isVisible(this)) {
            return;
        }
        if (dc.isHighlighted(this)) {
            Color fill = getFillColor();
            Color frame = getFrameColor();
            Color text = null;
            if (ColorMap.isTransparent(fill) || ColorMap.isBackground(fill)
                || this instanceof PolyLineFigure) {
                if (this instanceof TextFigure) {
                    text = (Color) getAttribute("TextColor");
                    setTextColor(ColorMap.hilight(text));
                } else if (!ColorMap.isTransparent(frame) && !ColorMap.isBackground(fill)) {
                    setFrameColor(ColorMap.hilight(frame));
                }
            } else {
                setFillColor(ColorMap.hilight(fill));
            }
            internalDraw(g);
            setFillColor(fill);
            setFrameColor(frame);
            if (text != null) {
                setTextColor(text);
            }
        } else {
            internalDraw(g);
        }
    }

    /**
     * Draws the background of the figure.
     *
     * @param g UNUSED
     * {@link #draw}
     */
    protected void drawBackground(Graphics g) {}

    /**
     * Draws the frame of the figure.
     *
     * @param g UNUSED
     * {@link #draw}
     */
    protected void drawFrame(Graphics g) {}

    /**
     * Gets the fill color of a figure. This is a convenience
     * method.
     *
     * @return fill color
     * {@link #getAttribute}
     */
    public Color getFillColor() {
        return (Color) getAttribute("FillColor");
    }

    /**
     * Sets the fill color of a figure <b>without</b> issuing
     * a {@code changed} Event.
     *
     * @param color fill color
     */
    public void setFillColor(Color color) {
        if (_attributes == null) {
            _attributes = new FigureAttributes();
        }
        _attributes.set("FillColor", color);
    }

    /**
     * Sets the frame color of a figure <b>without</b> issuing
     * a {@code changed} Event.
     *
     * @param color frame color
     */
    public void setFrameColor(Color color) {
        if (_attributes == null) {
            _attributes = new FigureAttributes();
        }
        _attributes.set("FrameColor", color);
    }

    /**
     * Sets the text color of a figure <b>without</b> issuing
     * a {@code changed} Event.
     *
     * @param color text color
     */
    public void setTextColor(Color color) {
        if (_attributes == null) {
            _attributes = new FigureAttributes();
        }
        _attributes.set("TextColor", color);
    }

    //---- figure attributes ----------------------------------
    private static void initializeAttributes() {
        _defaultAttributes = new FigureAttributes();
        _defaultAttributes.set("FrameColor", Color.black);
        _defaultAttributes.set("FillColor", new Color(0x70DB93));
        _defaultAttributes.set("TextColor", Color.black);
        _defaultAttributes.set(TextFigure.ALIGN_ATTR, TextFigure.LEFT);
        _defaultAttributes.set("ArrowMode", 0);
        _defaultAttributes.set("FontName", "Helvetica");
        _defaultAttributes.set("FontSize", 12);
        _defaultAttributes.set("FontStyle", Font.PLAIN);
        _defaultAttributes.set("LineShape", PolyLineFigure.LINE_SHAPE);
        _defaultAttributes.set("BSplineSegments", BSpline.DEFSEGMENTS);
        _defaultAttributes.set("BSplineDegree", BSpline.DEFDEGREE);

        // Initialize the ID of this Figure to NOID.
        // (See comments to getID(), setID() and _idAttr below.)
        _defaultAttributes.set(ID_ATTR, FigureWithID.NOID);
    }

    /**
     * Gets the frame color of a figure. This is a convenience
     * method.
     *
     * @return frame color
     * {@link #getAttribute}
     */
    public Color getFrameColor() {
        return (Color) getAttribute("FrameColor");
    }

    /**
     * Returns the named attribute or null if a
     * figure doesn't have an attribute.
     * All figures support the attribute names.
     * <code>FillColor</code> and <code>FrameColor</code>.
     */
    @Override
    public Object getAttribute(String name) {
        if (_attributes != null) {
            if (_attributes.hasDefined(name)) {
                return _attributes.get(name);
            }
        }
        Object supAttr = super.getAttribute(name);
        if (supAttr == null) {
            return getDefaultAttribute(name);
        } else {
            return supAttr;
        }
    }

    /**
     * Checks if this figure has defined attribute name.
     *
     * @param name Name of the attribute
     * @return true if the figure has defined attribute name
     */
    public Boolean hasAttribute(String name) {
        if (_attributes != null && _attributes.hasDefined(name)) {
            return true;
        }
        return super.getAttribute(name) != null;
    }

    /**
     * Returns the list of defined attribute keys for this figure.
     *
     * @return {@code Enumeration<String>} the attribute names
     */
    public Enumeration<String> getAttributeKeys() {
        Enumeration<String> attr = new Enumeration<>() {
            @Override
            public boolean hasMoreElements() {
                return false;
            }

            @Override
            public String nextElement() {
                return null;
            }
        };
        if (_attributes != null) {
            attr = _attributes.definedAttributes();
        }
        return attr;
    }

    /**
     * Sets the named attribute to the new value.
     */
    @Override
    public void setAttribute(String name, Object value) {
        super.setAttribute(name, value);
        if (_attributes == null) {
            _attributes = new FigureAttributes();
        }
        _attributes.set(name, value);
        changed();
    }

    /**
     * Stores the Figure to a StorableOutput.
     */
    @Override
    public void write(StorableOutput dw) {
        super.write(dw);

        // The write() method of PolyLineFigure assumes that
        // the first thing that this method writes is a string
        // and not a number.
        if (_attributes == null) {
            dw.writeString("no_attributes");
        } else {
            dw.writeString("attributes");
            _attributes.write(dw);
        }
    }

    /**
     * Reads the Figure from a StorableInput.
     */
    @Override
    public void read(StorableInput dr) throws IOException {
        super.read(dr);
        String s = dr.readString();
        if ("attributes".equalsIgnoreCase(s)) {
            if (_attributes == null) {
                _attributes = new FigureAttributes();
            }
            _attributes.read(dr);
            Enumeration<String> attrenumeration = _attributes.definedAttributes();
            while (attrenumeration.hasMoreElements()) {
                String attr = attrenumeration.nextElement();
                Object val = _attributes.get(attr);
                super.setAttribute(attr, val);
            }
        }

        // The line style attribute has been changed
        // from Integer to String, but I didn't want to
        // increase the storable data version for this.
        // (Current stream version is 8.)
        Object lineStyle = getAttribute("LineStyle");
        if (lineStyle instanceof Integer value) {
            if (value <= 0) {
                setAttribute("LineStyle", null);

            } else {
                setAttribute("LineStyle", lineStyle.toString());
            }
        }
    }

    // An additional line concerning this attribute has
    // been added to initializeDefaultAttributes.

    /**
     * Get the ID as required by interface FigureWithID.
     * <p>
     * {@link FigureWithID}
     **/
    @Override
    public int getID() {
        return (Integer) getAttribute(ID_ATTR);
    }

    /**
     * Set the ID as required by interface FigureWithID.
     * <p>
     * {@link FigureWithID}
     **/
    @Override
    public void setID(int id) {
        setAttribute(ID_ATTR, id);
    }

    /**
     * Returns the line style. If the line style is not currently set, the default line style is returned.
     *
     * @return the line style as a String or the default value if not set
     */
    protected String getLineStyle() {
        String lineStyle = (String) getAttribute("LineStyle");
        return Objects.requireNonNullElse(lineStyle, LINE_STYLE_NORMAL);
    }

    /**
     * Returns the line width. If the line width is not currently set, the default line width is returned.
     *
     * @return the line width as an Integer or the default value if not set
     */
    protected Integer getLineWidth() {
        Integer lineWidth = (Integer) getAttribute(LINE_WIDTH_KEY);
        if (lineWidth == null) {
            return LINE_WIDTH_DEFAULT;
        } else {
            return lineWidth;
        }
    }

    /**
     * Sets the line width to the given value.
     *
     * @param width the value to which the line width will be set
     */
    protected void setLineWidth(Integer width) {
        setAttribute(LINE_WIDTH_KEY, width);
    }

    /**
     * Sets the line style.
     *
     * @param lineStyle The style to which the line style will be set
     */
    protected void setLineStyle(String lineStyle) {
        setAttribute("LineStyle", lineStyle);
    }

    /**
     * Returns a BasicStroke object for a given line style.
     *
     * @param lineStyle line style
     * @return corresponding BasicStroke object
     */
    protected BasicStroke getBasicStroke(String lineStyle) {
        float[] f = lineStyle2ArrayOfFloat(lineStyle);
        return new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, f, 0);
    }

    /**
     * @param lineStyle line style
     * @return corresponding float array
     */
    private float[] lineStyle2ArrayOfFloat(String lineStyle) {
        float[] f;

        // the guard seems redundant
        if (!AttributeFigure.LINE_STYLE_NORMAL.equals(lineStyle) && !lineStyle.isEmpty()) {
            String[] split = lineStyle.split(" ");
            f = new float[split.length];
            for (int i = 0; i < split.length; i++) {
                f[i] = Float.parseFloat(split[i]);
            }
        } else {
            f = null;
            // Apparently, { 1 } is the same as { 1, 1 } (i.e. very fine  dots)
            // This shows in Renew (screen), PNG and SVG as solid line.
            // In contrast, PS, PDF and EPS show the dotted line.
            // Both solutions (null and { 1, 0}) work.
            // The values ({ 1 } and {1 1}) can now only be forced through the 
            // Line Style >  other ... dialogue.
            // f = new float[] { 1, 0 };
        }
        return f;
    }
}