/*
 * Licensed Materials - Property of Perforce Software, Inc. 
 * © Copyright Perforce Software, Inc. 2014, 2021 
 * © Copyright IBM Corp. 2009, 2014
 * © Copyright ILOG 1996, 2009
 * All Rights Reserved.
 *
 * Note to U.S. Government Users Restricted Rights:
 * The Software and Documentation were developed at private expense and
 * are "Commercial Items" as that term is defined at 48 CFR 2.101,
 * consisting of "Commercial Computer Software" and
 * "Commercial Computer Software Documentation", as such terms are
 * used in 48 CFR 12.212 or 48 CFR 227.7202-1 through 227.7202-4,
 * as applicable.
 */

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.io.IOException;

import ilog.views.IlvGraphic;
import ilog.views.IlvPoint;
import ilog.views.IlvRect;
import ilog.views.IlvSelection;
import ilog.views.IlvTextInterface;
import ilog.views.IlvTransformer;
import ilog.views.graphic.IlvText;
import ilog.views.graphic.IlvTextSelection;
import ilog.views.internal.impl.IlvUtility2D;
import ilog.views.io.IlvInputStream;
import ilog.views.io.IlvOutputStream;
import ilog.views.io.IlvReadFileException;

/**
 * A text object that uses less memory than the usual <code>IlvText</code>.
 * However it is also slower drawn when be at large magnification. The trick
 * is that usually only very few of these objects are visible in large
 * magnification, hence the slow down does not matter. If we draw with
 * demagnification, we draw only the rectangle without the text, which would
 * anyway be unreadable. So, at large demagnification, it is faster than
 * <code>IlvText</code>.
 */
public class LightText extends IlvGraphic
  implements IlvTextInterface
{
  IlvPoint anchor;
  String label;
  byte flags;
  double[] margins;
  Color foreground;
  Paint saveFillPaint;
  Paint fillPaint;
  IlvRect bounds;
  IlvText updatingDelegate;
  int updatingCounter;

  private static final short FILL_FLAG = 1;
  private static final short STROKE_FLAG = 2;
  private static final short ALIAS_FLAG = 4;

  /**
   * Creates a new <code>LightText</code>.
   */
  public LightText()
  {
    this(new IlvPoint(), "");
  }

  /**
   * Creates a new <code>LightText</code>.
   */
  public LightText(IlvPoint anchor, String text)
  {
    super();
    this.margins = new double[4];
    this.label = text;
    this.anchor = new IlvPoint(anchor);
    updateBounds(null);
  }

  /**
   * Creates a new <code>LightText</code> by copying an existing one.
   */
  public LightText(LightText source)
  {
    super(source);
    anchor = new IlvPoint(source.anchor);
    label = source.label;
    flags = source.flags;
    margins = new double[4];
    System.arraycopy(source.margins, 0, margins, 0, 4);
    foreground = source.foreground;
    saveFillPaint = source.saveFillPaint;
    bounds = new IlvRect(source.bounds); 
    updatePaints();
  }

  /**
   * Copies the object.
   * @return a copy of the object.
   * @see #isOwner
   */
  Override
  public IlvGraphic copy()
  {
    return new LightText(this);
  }

  /**
   * Returns <code>true</code> if the object is zoomable.
   * Since <code>IlvText</code> is zoomable, this object is also zoomable.
   */
  Override
  public boolean zoomable()
  {
    return true;
  }

  /**
   * Returns <code>true</code> if the label can be a multi-line label.
   * Since <code>IlvText</code> supports multi-line labels, this object is also
   * supports multi-line labels.
   */
  Override
  public boolean supportMultiline()
  {
    return true;
  }

  /**
   * Returns the area where the label is displayed.
   * It is equivalent to the {@link #boundingBox(IlvTransformer)} without
   * the margins.
   * @param t the transformer used to draw the graphic object.
   */
  Override
  public IlvRect getLabelBBox(IlvTransformer t)
  {
    IlvRect bbox = new IlvRect(bounds);
    bbox.x += getLeftMargin();
    bbox.width -= (getLeftMargin() + getRightMargin());
    bbox.y += getTopMargin();
    bbox.height -= (getTopMargin() + getBottomMargin());
    if (t != null && !t.isIdentity())
      t.apply(bbox);
    if (bbox.height < 0.1)
      bbox.height = 0.1;
    if (bbox.width < 0.1)
      bbox.width = 0.1;
    return bbox;
  }

  /**
   * Reads the object from an <code>IlvInputStream</code>.
   * @param stream the input stream.
   * @exception IlvReadFileException if the format is not correct.
   */
  public LightText(IlvInputStream stream) throws IlvReadFileException
  {
    super(stream);
    label = stream.readString("label");
    flags = (byte)stream.readShort("flags");
    anchor = stream.readPoint("anchorPoint");
    margins = stream.readDoubleOrFloatArray("margins");
    foreground = stream.readColor("foreground");
    saveFillPaint = IlvUtility2D.readPaint(stream);
    bounds = stream.readRect("bounds");
    updatePaints();
  }

  /**
   * Draws the object.
   * Calls <code>draw</code> on the referenced object.
   * @param dst the destination Graphics.
   * @param t the transformation used to draw the object.
   */
  Override
  public void draw(Graphics dst, IlvTransformer t)
  {
    IlvRect bbox = boundingBox(t);

    if (bbox.width <= 2 || bbox.height <= 2) {
      // draw only the border
      dst.setColor(getForeground());
      int w = Math.max(1, (int)bbox.width);
      int h = Math.max(1, (int)bbox.height);
      dst.fillRect((int)bbox.x, (int)bbox.y, w, h);
    } else if (bbox.width <= 4 || bbox.height <= 4) {
      // draw border and inner, but no text
      int w = Math.max(1, (int)bbox.width);
      int h = Math.max(1, (int)bbox.height);
      if (isFillOn()) {
        Graphics2D dst2 = (Graphics2D)dst;
        Paint old = dst2.getPaint();
        dst2.setPaint(fillPaint);
        dst.fillRect((int)bbox.x, (int)bbox.y, w, h);
        dst2.setPaint(old);
      }
      dst.setColor(getForeground());
      if (isStrokeOn()) {
        dst.drawRect((int)bbox.x, (int)bbox.y, w, h);
      } else {
        int x = (int)bbox.x;
        int middle = (int)(bbox.y + 0.5 * bbox.height);
        dst.drawLine(x, middle, x + w, middle);
      }
    } else {
      // draw the text via delegate
      getDelegate(true).draw(dst, t);
    }
  }

  /**
   * Returns the bounding rectangle of the object.
   * Calls <code>boundingBox</code> on the referenced object.
   * @param t the transformer used to draw the object.
   */
  Override
  public IlvRect boundingBox(IlvTransformer t)
  {
    IlvRect bbox = new IlvRect(bounds);
    if (t != null && !t.isIdentity())
      t.apply(bbox);
    if (bbox.height < 0.1)
      bbox.height = 0.1;
    if (bbox.width < 0.1)
      bbox.width = 0.1;
    return bbox;
  }

  /**
   * Applies a transformation to the shape of the object.
   * <p>
   * Note that the method must never be called with a <code>null</code>
   * argument.
   * @param t The transformer to be applied.
   */
  Override
  public void applyTransform(IlvTransformer t)
  {
    if (t == null || t.isIdentity())
      return;

    t.apply(bounds);

    if (bounds.width < 0.1)
      bounds.width = 0.1;
    if (bounds.height < 0.1)
      bounds.height = 0.1;
  }

  /**
   * Writes the object to an <code>IlvOutputStream</code>.
   * @param stream the output stream.
   */
  Override
  public  void write(IlvOutputStream stream)
    throws IOException
  {
    super.write(stream);
    stream.write("label", getLabel());
    stream.write("flags", flags);
    stream.write("anchorPoint", anchor);
    stream.write("margins", margins);
    stream.write("foreground", getForeground());
    IlvUtility2D.writePaint(stream, getFillPaint());
    stream.write("bounds", bounds);
 }

  /**
   * Sets the label that will be displayed by the <code>IlvText</code>. If the
   * label contains line breaking characters (<code>'\n'</code>) it will be
   * displayed on several lines.
   * <p>
   * Note that this may change its bounding rectangle. For this reason,
   * if the object is contained inside a manager, you should modify this flag
   * using the <code>applyToObject</code> method of the manager.
   * <p>
   * @param label the new label.
   */
  Override
  public void setLabel(String label)
  {
    if (label == null) label = "";
    if (!label.equals(this.label)) {
      IlvRect textbbox = getDelegate(false).boundingBox();
      IlvTransformer t = IlvTransformer.computeTransformer(textbbox, bounds, null);
      this.label = label;
      updateBounds(t);
    }
  }

  /**
   * Returns the label that will be displayed by the <code>IlvText</code>.
   * @see #setLabel(String)
   */
  Override
  public String getLabel()
  {
    return label;
  }

  /**
   * Sets the anchor point for the text. This specifies to which point in
   * manager coordinate (before applying {@link #getTransformer()}) the text
   * will be attached to.
   * @param anchor the anchor point.
   */
  public final void setAnchorPoint(IlvPoint anchor)
  {
    if (anchor == null) anchor = new IlvPoint();
    if (!anchor.equals(this.anchor)) {
      IlvRect textbbox = getDelegate(false).boundingBox();
      IlvTransformer t = IlvTransformer.computeTransformer(textbbox, bounds, null);
      this.anchor.setLocation(anchor);
      updateBounds(t);
    }
  }

  /**
   * Returns the anchor point for the text. This is the point in
   * manager coordinate (before applying {@link #getTransformer()}) the text
   * is attached to.
   * @see #getAnchorPosition()
   * @beaninfo
   *   description: The anchor point for the text.
   *   editor: ilog.views.beans.editor.IlvPointEditor.class
   */
  public final IlvPoint getAnchorPoint()
  {
    return new IlvPoint(anchor);
  }


  /**
   * Changes the visibility of the background rectangle
   * of the text.
   * @param value <code>true</code> if fill is on.
   */
  Override
  public final void setFillOn(boolean value)
  {
    if (value) {
      flags |=  FILL_FLAG;
      updatePaints();
    } else
      flags &= ~FILL_FLAG;
  }

  /**
   * Returns <code>true</code> if the background rectangle
   * of the text is drawn.
   * @see #setFillPaint
   * @see #setFillOn
   */
  public final boolean isFillOn()
  {
    return (flags & FILL_FLAG) != 0;
  }

  /**
   * Changes the visibility of the stroke of the background rectangle.
   * @param value <code>true</code> if stroke is on.
   */
  Override
  public final void setStrokeOn(boolean value)
  {
    if (value)
      flags |=  STROKE_FLAG;
    else
      flags &= ~STROKE_FLAG;
  }

  /**
   * Returns <code>true</code> if the frame of the background rectangle
   * of the text is drawn.
   */
  public final boolean isStrokeOn()
  {
    return (flags & STROKE_FLAG) != 0;
  }

  /**
   * Returns <code>true</code> if the anti-aliasing mode
   * of the label is on. The default value is <code>false</code>.
   */
  public final boolean isAntialiasing()
  {
    return (flags & ALIAS_FLAG) != 0;
  }

  /**
   * Changes the anti-aliasing mode of the text.
   * The default value is <code>false</code>.
   * Note that this may change its bounding rectangle. For this reason,
   * if the object is contained inside a manager, you should modify this flag
   * using the <code>applyToObject</code> method of the manager.
   */
  public final void setAntialiasing(boolean set)
  {
    if (set != isAntialiasing()) {
      IlvRect textbbox = getDelegate(false).boundingBox();
      IlvTransformer t = IlvTransformer.computeTransformer(textbbox, bounds, null);
      if (set)
        flags |=  ALIAS_FLAG;
      else
        flags &= ~ALIAS_FLAG;
      updateBounds(t);
    }
  }

  /**
   * Sets the left margin of the box around the text.
   * Note that this may change its bounding rectangle.
   * For this reason, if the object is contained inside a manager
   * you should modify the parameter using the <code>applyToObject</code>
   * method of the manager.
   * The default value is <code>0</code>.
   */
  public final void setLeftMargin(double margin)
  {
    if (margin < 0)
      throw new IllegalArgumentException("Illegal margin " + margin);
    if (margins[0] != margin) {
      IlvRect textbbox = getDelegate(false).boundingBox();
      IlvTransformer t = IlvTransformer.computeTransformer(textbbox, bounds, null);
      margins[0] = margin;
      updateBounds(t);
    }
  }

  /**
   * Returns the left margin of the box around the text.
   * The default value is <code>0</code>.
   * @see #setLeftMargin
   */
  public final double getLeftMargin()
  {
    return margins[0];
  }

  /**
   * Sets the right margin of the box around the text.
   * Note that this may change its bounding rectangle.
   * For this reason, if the object is contained inside a manager
   * you should modify the parameter using the <code>applyToObject</code>
   * method of the manager.
   * The default value is <code>0</code>.
   */
  public final void setRightMargin(double margin)
  {
    if (margin < 0)
      throw new IllegalArgumentException("Illegal margin " + margin);
    if (margins[1] != margin) {
      IlvRect textbbox = getDelegate(false).boundingBox();
      IlvTransformer t = IlvTransformer.computeTransformer(textbbox, bounds, null);
      margins[1] = margin;
      updateBounds(t);
    }
  }

  /**
   * Returns the right margin of the box around the text.
   * The default value is <code>0</code>.
   * @see #setRightMargin
   */
  public final double getRightMargin()
  {
    return margins[1];
  }

  /**
   * Sets the top margin of the box around the text.
   * Note that this may change its bounding rectangle.
   * For this reason, if the object is contained inside a manager
   * you should modify the parameter using the <code>applyToObject</code>
   * method of the manager.
   * The default value is <code>0</code>.
   */
  public final void setTopMargin(double margin)
  {
    if (margin < 0)
      throw new IllegalArgumentException("Illegal margin " + margin);
    if (margins[2] != margin) {
      IlvRect textbbox = getDelegate(false).boundingBox();
      IlvTransformer t = IlvTransformer.computeTransformer(textbbox, bounds, null);
      margins[2] = margin;
      updateBounds(t);
    }
  }

  /**
   * Returns the top margin of the box around the text.
   * The default value is <code>0</code>.
   * @see #setTopMargin
   */
  public final double getTopMargin()
  {
    return margins[2];
  }

  /**
   * Sets the bottom margin of the box around the text.
   * Note that this may change its bounding rectangle.
   * For this reason, if the object is contained inside a manager
   * you should modify the parameter using the <code>applyToObject</code>
   * method of the manager.
   * The default value is <code>0</code>.
   */
  public final void setBottomMargin(double margin)
  {
    if (margin < 0)
      throw new IllegalArgumentException("Illegal margin " + margin);
    if (margins[3] != margin) {
      IlvRect textbbox = getDelegate(false).boundingBox();
      IlvTransformer t = IlvTransformer.computeTransformer(textbbox, bounds, null);
      margins[3] = margin;
      updateBounds(t);
    }
  }

  /**
   * Returns the bottom margin of the box around the text.
   * The default value is <code>0</code>.
   * @see #setBottomMargin
   */
  public final double getBottomMargin()
  {
    return margins[3];
  }

  /**
   * Changes the foreground color of the object, i.e the color used
   * to draw the text itself.
   * @param c The new color.
   */
  Override
  public void setForeground(Color c)
  {
    foreground = c;
  }

  /**
   * Returns the foreground color of object, i.e the color used
   * to draw the text itself. The default value is <code>Color.BLACK</code>.
   */
  public Color getForeground()
  {
    return (foreground!=null)?foreground:Color.black;
  }

  /**
   * Changes the background <code>Paint</code> of the object, i.e the
   * paint server used to fill the background rectangle.
   * @param paint the new <code>Paint</code> object.
   */
  public void setFillPaint(Paint paint)
  {
    saveFillPaint = paint;
    updatePaints();
  }

  /**
   * Returns the <code>Paint</code> object used to fill the background
   * rectangle. The default value is <code>Color.GRAY</code>.
   * @see #setFillPaint
   */
  public Paint getFillPaint()
  {
    if (saveFillPaint == null)
      return Color.gray;
    return saveFillPaint;
  }

  private void updateBounds(IlvTransformer t)
  {
    bounds = getDelegate(false).boundingBox();
    if (t != null)
      t.apply(bounds);
    updatePaints();
  }

  private void updatePaints()
  {
    // this means we are here always in paint-relative mode
    if (isFillOn())
      fillPaint = IlvUtility2D.AdaptPaint(boundingBox(), getFillPaint());
  }

  /**
   * This method returns a <code>Shape</code> object enveloping the
   * characters specified by <code>range</code>.
   * This is part of the <code>IlvTextInterface</code>.
   * @param range A <code>Range</code> indicating the number and position of
   * selected characters.
   * @param t The transformer used while drawing the graphic object being
   * edited.
   * @return A <code>Shape</code> used to highlight the selected characters.
   * If <code>null</code> is returned, the text is not editable and the caret
   * shape should not be displayed.
   */
  Override
  public Shape getCaretShape(IlvTextSelection.Range range, IlvTransformer t)
  {
    return getDelegate(true).getCaretShape(range, t);
  }

  /**
   * Returns the index of the character clicked by the user.
   * This is part of the <code>IlvTextInterface</code>.
   * @param click The position clicked on with the mouse. The coordinates
   * have already been transformed by the <code>IlvTransformer</code>,
   * <code>t</code>.
   * @param t The transformer used to draw the label.
   * @return The index of the character clicked by the mouse. When no
   * character has been selected, -1 is returned.
   */
  Override
  public int pickCharacter(IlvPoint click, IlvTransformer t)
  {
    return getDelegate(true).pickCharacter(click, t);
  }

  /**
   * Returns the line count for multi-line text.
   * This is part of the <code>IlvTextInterface</code>.
   * @return The line count for multi-line text. For a single line
   * text, the value <code>1</code> is returned.
   */
  Override
  public int lineCount()
  {
    return getDelegate(true).lineCount();
  }

  /**
   * Returns the line offset for a given line.
   * This is part of the <code>IlvTextInterface</code>.
   * @param index The line index. The first line in the label has index 0.
   * @return The line offset for the selected character in a label.
   * Returns 0 for single line label or -1 if the specified index is out of
   * range.
   */
  Override
  public int lineOffset(int index)
  {
    return getDelegate(true).lineOffset(index);
  }

  /**
   * Returns the delegate object that actually does the drawing.
   * Note we don't cache the delegate, therefore the LightText uses less
   * memory.
   */
  private IlvText getDelegate(boolean includeBounds)
  {
    IlvText text = (updatingDelegate != null ? updatingDelegate 
                                             : new IlvText(anchor, label));
    text.setAntialiasing(isAntialiasing());
    text.setLeftMargin(getLeftMargin());
    text.setRightMargin(getRightMargin());
    text.setTopMargin(getTopMargin());
    text.setBottomMargin(getBottomMargin());
    text.setFillOn(isFillOn());
    text.setStrokeOn(isStrokeOn());
    text.setForeground(getForeground());
    text.setFillPaint(getFillPaint());
    if (includeBounds)
      text.moveResize(bounds);
    else if (bounds != null) {
      text.move(bounds.x, bounds.y);
    }
    return text;
  }

  /**
   * Starts or stops an updating session.
   * This can be used to improve performance when many properties
   * are changed at once.
   */
  public void setUpdating(boolean updating)
  {
    if (updating) {
      if (updatingCounter == 0)
        updatingDelegate = new IlvText(anchor, label);
      updatingCounter++;
    } else {
      updatingCounter--;
      if (updatingCounter == 0)
        updatingDelegate = null;
    }
  }

  /**
   * Creates a selection object for this graphic object.
   */
  Override
  public IlvSelection makeSelection()
  {
    return new IlvTextSelection(this, true);
  }
}