import java.awt.*;
import java.util.*;

/** StarMarker extends Point,
 * rather than Canvas / Component,
 * to be drawn directly on a parentStarField,
 * which provides Graphics g and coordinate space for the drawing.
 * Parent also provides event handling and interpretation.
 *
 * StarMarker should be as close as possible to ImageButton,
 * except for being lightweight rather than Canvas / Component,
 * but single inheritance means we must implement in both places.  */

public class StarMarker
extends Point
implements ButtonLike {

/* Need this for unique hashCode,
 * because all Point (0, 0) are created equal.
 * Or we could extend Object instead of Point,
 * declare x and y and a few methods. */

    protected Object myObject;

    public int width = 24, height = 20;

    protected float fX = 0, fY = 0;

    protected String text = null;

    protected Color fgColor = null, bgColor = null;

    private boolean visible = true;
    private boolean stale = true;
    protected boolean Enabled = true;
    protected boolean Pressed = false;
    protected boolean Highlighted = false;
    protected boolean Expanded = false;

    protected StarField parentStarField = null;
    protected ButtonLikeListener listener = null;
    protected RadioGroup RG = null;

/* Appearance details that belong in an extension,
 * or at least that extensions may do differently */

    protected Color fgHLColor = null, bgHLColor = null;

    protected int borderThickness = 0;

    protected int borderHLThickness = 0;

/* Iconic display options --
 * String, Image, Simple Drawn Shapes.
 * And an Expansion option String */

    protected String abbrev = null;
    protected Image image = null;
    protected int shape = 0;
    protected int radius = 0;

/* Numbers chosen to allow some bit-masking:
 * UP & DOWN = DIAMOND
 * VERT & HORIZ = PLUS
 * Anything undefined non-zero SQUARE */

    public static final int
	CIRCLE = 1,
	UP_POINT = 2,
	DOWN_POINT = 4,
	DIAMOND = 6,
	VERT_BAR = 8,
	HORIZ_BAR = 16,
	PLUS = 24,
	SQUARE = 99;

/** Constructor */

    StarMarker (String text, String abbrev) {
        super (0, 0);
        myObject = new Object ();
        this.text = text;
        this.abbrev = abbrev;
    }

/** Constructor */

    StarMarker (String text) {
        this (text, null);
    }

/** Constructor */

    StarMarker () {
        this (null);
    }

/* Changed X and/or Y needs reScale to pixels,
 * so that repaint can be limited in area. */

    public void setXY (float fX, float fY) {
        if ((this.fX == fX) && (this.fY == fY)) return;

        this.fX = fX;
        this.fY = fY;
	parentStarField.reScale (this);
        repaint (50);
    }

    public void setX (float fX) {
        if (this.fX == fX) return;

        this.fX = fX;
	parentStarField.reScale (this);
        repaint (50);
    }

    public void setY (float fY) {
        if (this.fY == fY) return;

        this.fY = fY;
	parentStarField.reScale (this);
        repaint (50);
    }

/** Just change the value(s) if change is needed,
 * let caller take care of the rest. */

    public boolean changeXY (float fX, float fY) {
        if ((this.fX == fX) && (this.fY == fY)) return false;

        this.fX = fX;
        this.fY = fY;
	return true;
    }

    public boolean changeX (float fX) {
        if (this.fX == fX) return false;

        this.fX = fX;
	return true;
    }

    public boolean changeY (float fY) {
        if (this.fY == fY) return false;

        this.fY = fY;
	return true;
    }

    public float getX () { return fX; }

    public float getY () { return fY; }

    public void setParent (StarField parentStarField) {
        this.parentStarField = parentStarField;
    }

    public StarField getParent () { return parentStarField; }

    public int hashCode () { return myObject.hashCode (); }

    public void setVisible (boolean bv) { visible = bv; repaint (50); }

    public boolean isVisible () {
        if (null == parentStarField)
            return false;

        if ((x < 0) || (y < 0))
            return false;

        Dimension ps = parentStarField.getSize ();
        if ((x > ps.width) || (y > ps.height))
            return false;

        return visible;
    }

/** Set the Listener to provide some button behavior.
 * Really all happens in the parent not here. */

    public void setListener (ButtonLikeListener ibl) { listener = ibl; }

/** Get the current Listener. */

    public ButtonLikeListener getListener () { return listener; }

/** Have parentStarField do the rest for us... */

    public void processStateChange (boolean s) {
         parentStarField.processStateChange (this, s);
    }

    public void processAction () {
        parentStarField.processAction (this);
    }

/** Set the state,
 * have RadioGroup (if any) turn off any prior selection,
 * update the "state machine" in the event handler,
 * also update the appearance of the button.
 *
 * Here for internal call,
 * if this is the RG current selection,
 * and it is being turned off,
 * let it happen,
 * and null out the RG current selection. */

    public void setState (boolean state) {
        ((StickyButtonLikeListener)listener).setState (state);

        if (null != RG) {
            if (state)
                RG.setSelected (this);
            else if (this == RG.getSelected ())
                RG.setSelected (null);
        }

        setPressed (state);
    }

/** Get the current state.
 * Only called when Sticky listener is attached. */

    public boolean getState () { return ((StickyButtonLikeListener)listener).getState (); }

/** Set the RadioGroup.
 * @param RG -- the RadioGroup to manage this control. */

    public void setRadioGroup (RadioGroup RG) {
        this.RG = RG;
    }

    public boolean contains (Point cp) { return contains (cp.x, cp.y); }

    public boolean contains (int cx, int cy) {
        return ((x - width/2 < cx)
            && (x + width/2 > cx)
            && (y - height/2 < cy)
            && (y + height/2 > cy));
    }

/** Set appearance Pressed or not */

    public void setPressed (boolean Pressed) {
        if (this.Pressed == Pressed) return;

        this.Pressed = Pressed;

        if (isVisible ()) repaint ();
    }

    public boolean isPressed () { return Pressed; }

/** Set appearance Highlighted or not */

    public void setHighlighted (boolean Highlighted) {
        if (this.Highlighted == Highlighted) return;

        this.Highlighted = Highlighted;
	stale = true;

        if (isVisible ()) repaint ();
    }

    public boolean isHighlighted () { return Highlighted; }

/** Set appearance Expanded or not */

    public void setExpanded (boolean Expanded) {
        if (this.Expanded == Expanded) return;

        this.Expanded = Expanded;
	stale = true;

        if ((null == abbrev) && (null == image) && (0 == shape))
            return;

        if (isVisible ()) repaint ();
    }

    public boolean isExpanded () { return Expanded; }

    public void enable () { setEnabled (true); }
    public void disable () { setEnabled (false); }

    public void setEnabled (boolean enable) {
        if (Enabled == enable) return;

        Enabled = enable;
	stale = true;

        if (isVisible ()) repaint ();
    }

    public boolean isEnabled () { return Enabled; }

    public void setForeground (Color set_fgColor) {
        if (fgColor == set_fgColor) return;

        fgColor = set_fgColor;
        repaint (50);
    }

    public void setBackground (Color set_bgColor) {
        if (bgColor == set_bgColor) return;

        bgColor = set_bgColor;
        repaint (50);
    }

    public void setText (String text) {
        this.text = text;
	stale = true;
        repaint (50);
    }

    public String getText () { return text; }

/** Appearance methods */

    public void setBorderHLThickness (int borderHLThickness) {
        if (this.borderHLThickness == borderHLThickness) return;

        this.borderHLThickness =  borderHLThickness;
	stale = true;
    }

    public int getBorderHLThickness () {
        if (0 < borderHLThickness)
	    return borderHLThickness;
	else
	    return parentStarField.getBorderHLThickness ();
    }

    public void setBorderThickness (int borderThickness) {
        if (this.borderThickness == borderThickness) return;

        this.borderThickness =  borderThickness;
	stale = true;
    }

    public int getBorderThickness () {
        if (0 < borderThickness)
	    return borderThickness;
	else
	    return parentStarField.getBorderThickness ();
    }

    public void setHLForeground (Color set_fgHLColor) {
        if (fgHLColor == set_fgHLColor) return;

        fgHLColor = set_fgHLColor;
        if (Highlighted)
            repaint (50);
    }

    public void setHLBackground (Color set_bgHLColor) {
        if (bgHLColor == set_bgHLColor) return;

        bgHLColor = set_bgHLColor;
        if (Highlighted)
            repaint (50);
    }

    public Color getHLBackground () {
        if (null != bgHLColor)
	    return bgHLColor;
	else
	    return parentStarField.getHLBackground ();
    }

    public Color getHLForeground () {
        if (null != fgHLColor)
	    return fgHLColor;
	else
	    return parentStarField.getHLForeground ();
    }

    public void setAbbrev (String abbrev) {
        this.abbrev = abbrev;
	stale = true;
        repaint (50);
    }

    public void setImage (Image image) {
        this.image = image;
	stale = true;
        repaint (50);
    }

/* Choose one of the standard shapes,
 * at current radius size if set,
 * or default radius size. */

    public void setShape (int shape) {
        if (this.shape == shape) return;

        this.shape = shape;
	stale = true;
        if (0 == radius)
            radius = 8;
        repaint (50);
    }

/* Choose one of the standard shapes and radius size */

    public void setShape (int shape, int radius) {
        if ((this.shape == shape) && (this.radius == radius)) return;

        this.shape = shape;
        this.radius = radius;
	stale = true;
        repaint (50);
    }

  /// public String toString () { return super.toString () + text; }

/** To repaint this StarMarker after some change,
   schedule repaint for parentStarField,
   allowing enough delay to batch more of these requests. */

/* Use repaint (long mSec, int x y w h) with --VALID-- x y w h ***/

    public void repaint (int msec) {
        if (!isVisible ()) return;

        if (stale)
            parentStarField.repaint (msec);
	else
            parentStarField.repaint (msec, x-width/2, y-height/2, width, height);
    }

/** To repaint this StarMarker as soon as possible */

/* Use repaint (int x y w h) with --VALID-- x y w h ***/

    public void repaint () {
        if (!isVisible ()) return;

        if (stale)
            parentStarField.repaint ();
	else
            parentStarField.repaint (x-width/2, y-height/2, width, height);
    }

/** Because StarMarker extends Point,
 * rather than Canvas / Component,
 * and StarField extends Canvas / Component,
 * rather than Container,
 * the parentStarField calls this explicitly for each StarMarker. */

    public void paint (Graphics g) {

        if (!isVisible ()) return;

        if (null == fgColor)
            fgColor = parentStarField.getForeground ();

        if (null == bgColor)
            bgColor = parentStarField.getBackground ();

/* How to show not Enabled? Faded colors...
 * Could it still be Expanded? Pressed? No it can't.  */

/* Use HLColors / HLThickness when Highlighted */

/* Show the Expansion text if requested,
 * or if we have no abbrev or image or shape,
 * otherwise the abbrev,
 * or image,
 * or shape */

        if (Expanded)
            gString (g, text);

        else if (null != abbrev)
            gString (g, abbrev);

        else if (null != image)
            gImage (g, image);

        else if (0 < shape)
            gShape (g, shape);

        else
            gString (g, text);

        stale = false;
    }

/* Draw one of the standard shapes at current radius size
 * and equivalent of bold / normal font for Highlighted
 * since that is how we are doing those now,
 * although fonts are managed by parentStarField. */

    protected void gShape (Graphics g, int shape) {
	if (stale) {
	    width = radius * 2;
	    height = radius * 2;
	}

        g.setColor (fgColor);

/* Draw solid instead of outline when Pressed. */

        int rinner;

        if (Pressed) {
	    if (Highlighted)
		rinner = getBorderHLThickness ();
            else
		rinner = getBorderThickness ();
        }

        else {
	    if (Highlighted)
		rinner = radius - getBorderHLThickness ();
            else
		rinner = radius - getBorderThickness ();
        }

/** Works but not good (fuzzy) for circle and rectangle,
 * should just use a couple of fillXX outer and inner */

        for (int ee = rinner; ee < radius; ee++)
	    doShape (g, shape, x, y, ee);
    }

    protected void doShape (Graphics g, int shape, int x, int y, int ee) {
        if (CIRCLE == shape) {
            g.drawArc (x-ee, y-ee, 2*ee, 2*ee, 0, 360);
        }

        else if (UP_POINT == shape) {
	    g.drawLine (x+ee, y+ee, x-ee, y+ee);
	    g.drawLine (x-ee, y+ee, x, y-ee);
	    g.drawLine (x, y-ee, x+ee, y+ee);
	}

        else if (DOWN_POINT == shape) {
	    g.drawLine (x+ee, y-ee, x-ee, y-ee);
	    g.drawLine (x-ee, y-ee, x, y+ee);
	    g.drawLine (x, y+ee, x+ee, y-ee);
	}

	else if (DIAMOND == shape) {
	    g.drawLine (x, y+ee, x-ee, y);
	    g.drawLine (x-ee, y, x, y-ee);
	    g.drawLine (x, y-ee, x+ee, y);
	    g.drawLine (x+ee, y, x, y+ee);
	}

	else if (VERT_BAR == shape) {
	    g.drawRect (x-ee/2, y-ee, ee, 2*ee);
        }

	else if (HORIZ_BAR == shape) {
	    g.drawRect (x-ee, y-ee/2, 2*ee, ee);
        }

	else if (PLUS == shape) {
	    g.drawRect (x-ee, y-ee/2, 2*ee, ee);
	    g.drawRect (x-ee/2, y-ee, ee, 2*ee);
        }

	else {
	    g.drawRect (x-ee, y-ee, 2*ee, 2*ee);
        }
    }

    protected void gImage (Graphics g, Image image) {
        if (stale) {
            parentStarField.waitForImage (image);
            width  = image.getWidth (parentStarField);
            height = image.getHeight (parentStarField);
        }

/* Offset when Pressed. */

        int dd = (Pressed) ? 3:0;

        g.drawImage (image,
          dd + x - width/2,
          dd + y - height/2,
          parentStarField);
    }

    protected void gString (Graphics g, String str) {
        if (stale) {
            FontMetrics fm = g.getFontMetrics ();
            width = fm.stringWidth (str);
            height = fm.getHeight ();
        }

/* Reverse fg and bg Colors when Pressed. */

        if (Pressed) {
            g.setColor (fgColor);
            g.fillRect (x-width/2, y-height/2, width, height);
            g.setColor (bgColor);
        }

        else {
            g.setColor (bgColor);
            g.fillRect (x-width/2, y-height/2, width, height);
            g.setColor (fgColor);
        }

        g.drawString (str, x-width/2, y+height/2);
    }

    protected boolean dragEnabled = true;

/** Set policy to allow this Marker to be dragged by user,
 * if the StarField also allows it. */

    public void setDragEnabled (boolean bv) {
	dragEnabled = bv;
    }

    public boolean getDragEnabled () { return dragEnabled; }

/** Formerly a separate extension class,
 * combined as a convenience.
 * QueryMarker extends StarMarker with linkage and flag info. */

  protected Vector MV = new Vector (10);
  protected StarMarker[] links = null;

  protected String title = null;

  protected int interest = 0;

    public String toString () { return super.toString ()+" "+text+" "+title; }

/** Attribute flags as convenience for use by applications */

    private long flags = 0;

/** Set the given bit(s) true leaving others alone */

    public void setFlags (long bits) { flags = flags | bits; }

/** Get the current set of flags */

    public long getFlags () { return flags; }

/** Set the given bit(s) false leaving others alone */

    public void unsetFlags (long bits) { flags = flags & (~bits); }

/** Set all flag bits false */

    public void clearAllFlags () { flags = 0; }

/** Are any of the given bit(s) true */

    public boolean testFlags (long bits) { return (0 != (flags & bits)); }

/** Add a Linked StarMarker */

    public StarMarker addLink (StarMarker marker) {
        MV.addElement (marker);
	links = null;
        return marker;
    }

/** Get array of current Linked StarMarkers */

    public StarMarker[] getLinks () {
        if (MV.isEmpty())
            return null;

        if (null == links) {
            links = new StarMarker[MV.size ()];
	    MV.copyInto (links);
        }

        return links;
    }

    public int getNumLinks () { return MV.size (); }

    public void setTitle (String title) { this.title = title; }

    public String getTitle () { return title; }

    public void incInterest () {
        interest += 1;
        setHighlighted (true);
    }

    public void decInterest () {
	if (interest > 0)
            interest -= 1;
        setHighlighted (interest > 0);
    }
}
