/*
 * 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.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ByteLookupTable;
import java.awt.image.ColorConvertOp;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.LookupOp;
import java.awt.image.Raster;
import java.awt.image.RasterOp;
import java.awt.image.WritableRaster;

/**
 * An emboss effect that draws a dark grey shadow at the bottom right corner
 * of nontransparent pixels, and a light gray shadow at the top left corner
 * of nonstransparent pixels.
 */
public class HoverEmbossOp implements BufferedImageOp, RasterOp 
{
  private RenderingHints hints;
  private LookupOp lop;
  private Rectangle area;
  private int thickness;

  /**
   * Constructs a hover emboss operation.
   * @param hints The rendering hints
   * @param thickness The thickness of the emboss in [1,4]
   */
  public HoverEmbossOp(RenderingHints hints, int thickness) {
    this.hints  = hints;
    this.thickness = thickness;
    // just to save some code for createCompatibleDestImage, we use this delegate
    lop = new LookupOp(new ByteLookupTable(0,new byte[] { 0, 1, 2 }), hints);
  }

  /**
   * Sets the area of interest.
   * This can be set before filter. Only the area of interest is treated
   * by the operation.
   */
  public void setOperationArea(Rectangle r)
  {
    area = r;
  }

  /**
   * Performs a single-input/single-output operation on a
   * <CODE>BufferedImage</CODE>.
   * If the color models for the two images do not match, a color
   * conversion into the destination color model is performed.
   * If the destination image is null,
   * a <CODE>BufferedImage</CODE> with an appropriate <CODE>ColorModel</CODE>
   * is created.
   * <p>
   * An <CODE>IllegalArgumentException</CODE> may be thrown if the source
   * and/or destination image is incompatible with the types of images
   * allowed by the class implementing this filter.
   *
   * @param src The <CODE>BufferedImage</CODE> to be filtered
   * @param dest The <CODE>BufferedImage</CODE> in which to store the results
   * @return The filtered <CODE>BufferedImage</CODE>.
   *
   * @throws IllegalArgumentException If the source and/or destination
   * image is not compatible with the types of images allowed by the class
   * implementing this filter.
   */
  Override
  public final BufferedImage filter(BufferedImage src, BufferedImage dst)
  {
    ColorModel srcCM = src.getColorModel();
//    int numBands = srcCM.getNumColorComponents();
    ColorModel dstCM;
    if (srcCM instanceof IndexColorModel) {
      throw new IllegalArgumentException("HooverEmbossOp cannot be "+
                                         "performed on an indexed image");
    }

    boolean needToConvert = false;

    int width = src.getWidth();
    int height = src.getHeight();
    BufferedImage origDst = dst;

    if (dst == null) {
      dst = origDst = createCompatibleDestImage(src, null);
      dstCM = srcCM;
    }
    else {
      if (height != dst.getHeight() || width != dst.getWidth()) {
        throw new
            IllegalArgumentException ("Width or height of images do not match");
      }

      dstCM = dst.getColorModel();
      if (srcCM.getColorSpace().getType() != dstCM.getColorSpace().getType())
      {
        needToConvert = true;
        dst = createCompatibleDestImage(src, null);
      }
    }

    filter(src.getRaster(), dst.getRaster());

    if (needToConvert) {
      // ColorModels are not the same
      ColorConvertOp ccop = new ColorConvertOp(hints);
      ccop.filter(dst, origDst);
    }
    return origDst;
  }

  /**
   * Performs a lookup operation on a <code>Raster</code>.
   * If the destination <code>Raster</code> is <code>null</code>,
   * a new <code>Raster</code> will be created.
   * The <code>IllegalArgumentException</code> might be thrown
   * if the source <code>Raster</code> and the destination
   * <code>Raster</code> do not have the same
   * number of bands or if the number of arrays in the
   * <code>LookupTable</code> does not meet the
   * restrictions stated in the class comment above.
   * @param src the source <code>Raster</code> to filter
   * @param dst the destination <code>WritableRaster</code> for the
   *            filtered <code>src</code>
   * @return the filtered <code>WritableRaster</code>.
   * @throws IllegalArgumentException if the source and destinations
   *         rasters do not have the same number of bands, or the
   *         number of arrays in the <code>LookupTable</code> does
   *         not meet the restrictions described in the class comments.
   *
   */
  Override
  public final WritableRaster filter(Raster src, WritableRaster dst)
  {
    int numBands  = src.getNumBands();
    int height    = src.getHeight();
    int width     = src.getWidth();
    int srcPix[]  = new int[numBands];
    int alphaBand = 3;
    int LIGHT=230;
    int DARK=25;
    // Create a new destination Raster, if needed

    if (dst == null) {
        dst = createCompatibleDestRaster(src);
    }
    else if (height != dst.getHeight() || width != dst.getWidth()) {
        throw new
            IllegalArgumentException ("Width or height of Rasters do not match");
    }

    int sminX = src.getMinX();
    int sminY = src.getMinY();
    int dminX = dst.getMinX();
    int dminY = dst.getMinY();

    int minx = 0;
    int miny = 0;
    int maxx = width;
    int maxy = height;
    int t = thickness;
    
    if (t < 1) t = 1;
    if (t > 4) t = 4;

    if (area != null) {
      minx = Math.max(0, area.x);
      miny = Math.max(0, area.y);
      maxx = Math.min(width, area.x + area.width);
      maxy = Math.min(height, area.y + area.height);
    }

    for (int y = miny; y < maxy; y++) {
      for (int x = minx; x < maxx; x++) {
        src.getPixel(sminX + x, sminY + y, srcPix);
        dst.setPixel(dminX + x, dminY + y, srcPix);
      }
    }
    boolean prvTransparent = true;
    boolean actTransparent = true;
    for (int y = miny+1; y < maxy; y++) {
      for (int x = minx+t; x < maxx-t; x++) {
        src.getPixel(sminX + x, sminY + y, srcPix);
        if (srcPix[alphaBand] == 0)
          actTransparent = true;
        else
          actTransparent = false;
        if (actTransparent != prvTransparent) {
          if (prvTransparent) {
            for (int i = 0; i < srcPix.length; i++)
              srcPix[i] = LIGHT;
            srcPix[alphaBand] = 255;
            for (int i = 0; i < t; i++)
              dst.setPixel(dminX + x - 1 - i, dminY + y, srcPix);
          } else {
            for (int i = 0; i < srcPix.length; i++)
              srcPix[i] = DARK;
            srcPix[alphaBand] = 255;
            for (int i = 0; i < t; i++)
              dst.setPixel(dminX + x + i, dminY + y, srcPix);
          }
        }
        prvTransparent = actTransparent;
      }
    }
    prvTransparent = true;
    actTransparent = true;
    for (int x = minx+1; x < maxx; x++) {
      for (int y = miny+t; y < maxy-t; y++) {
        src.getPixel(sminX + x, sminY + y, srcPix);
        if (srcPix[alphaBand] == 0)
          actTransparent = true;
        else
          actTransparent = false;
        if (actTransparent != prvTransparent) {
          if (prvTransparent) {
            for (int i = 0; i < srcPix.length; i++)
              srcPix[i] = LIGHT;
            srcPix[alphaBand] = 255;
            for (int i = 0; i < t; i++)
              dst.setPixel(dminX + x, dminY + y - 1 - i, srcPix);
          } else {
            for (int i = 0; i < srcPix.length; i++)
              srcPix[i] = DARK;
            srcPix[alphaBand] = 255;
            for (int i = 0; i < t; i++)
              dst.setPixel(dminX + x, dminY + y + i, srcPix);
          }
        }
        prvTransparent = actTransparent;
      }
    }

    return dst;
  }

  /**
   * Returns the bounding box of the filtered destination image.  Since
   * this is not a geometric operation, the bounding box does not
   * change.
   * @param src the <code>BufferedImage</code> to be filtered
   * @return the bounds of the filtered definition image.
   */
  Override
  public final Rectangle2D getBounds2D (BufferedImage src) {
    return getBounds2D(src.getRaster());
  }

  /**
   * Returns the bounding box of the filtered destination Raster.  Since
   * this is not a geometric operation, the bounding box does not
   * change.
   * @param src the <code>Raster</code> to be filtered
   * @return the bounds of the filtered definition <code>Raster</code>.
   */
  Override
  public final Rectangle2D getBounds2D (Raster src) {
    return src.getBounds();
  }

  /**
   * Creates a zeroed destination image with the correct size and number of
   * bands.  If destCM is <code>null</code>, an appropriate
   * <code>ColorModel</code> will be used.
   * @param src       Source image for the filter operation.
   * @param destCM    the destination's <code>ColorModel</code>, which
   *                  can be <code>null</code>.
   * @return a filtered destination <code>BufferedImage</code>.
   */
  Override
  public BufferedImage createCompatibleDestImage(BufferedImage src,
                                                 ColorModel destCM)
  {
    return lop.createCompatibleDestImage(src, destCM);
  }

  /**
   * Creates a zeroed-destination <code>Raster</code> with the
   * correct size and number of bands, given this source.
   * @param src the <code>Raster</code> to be transformed
   * @return the zeroed-destination <code>Raster</code>.
   */
  Override
  public WritableRaster createCompatibleDestRaster(Raster src)
  {
    return src.createCompatibleWritableRaster();
  }

  /**
   * Returns the location of the destination point given a
   * point in the source.  If <code>dstPt</code> is not
   * <code>null</code>, it will be used to hold the return value.
   * Since this is not a geometric operation, the <code>srcPt</code>
   * will equal the <code>dstPt</code>.
   * @param srcPt a <code>Point2D</code> that represents a point
   *        in the source image
   * @param dstPt a <code>Point2D</code>that represents the location
   *        in the destination
   * @return the <code>Point2D</code> in the destination that
   *         corresponds to the specified point in the source.
   */
  Override
  public final Point2D getPoint2D(Point2D srcPt, Point2D dstPt)
  {
    return lop.getPoint2D(srcPt, dstPt);
  }

  /**
   * Returns the rendering hints for this op.
   * @return the <code>RenderingHints</code> object associated
   *         with this op.
   */
  Override
  public final RenderingHints getRenderingHints()
  {
    return hints;
  }
}