/* Copyright (C) 2004 by Peter Eastman

   This program is free software; you can redistribute it and/or modify it under the
   terms of the GNU General Public License as published by the Free Software
   Foundation; either version 2 of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful, but WITHOUT ANY 
   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 
   PARTICULAR PURPOSE.  See the GNU General Public License for more details. */

package artofillusion.image.filter;

import artofillusion.*;
import artofillusion.image.*;
import artofillusion.math.*;
import artofillusion.object.*;
import artofillusion.ui.*;
import buoy.event.*;
import buoy.widget.*;
import java.awt.*;
import java.io.*;

/** This is an image filter which blurs an image. */

public class OutlineFilter extends ImageFilter
{
  private static final byte NONE = 0;
  private static final byte CENTER = 1;
  private static final byte UP = 2;
  private static final byte DOWN = 3;
  private static final byte LEFT = 4;
  private static final byte RIGHT = 5;
  
  public OutlineFilter()
  {
  }

  /** Get the name of this filter.*/

  public String getName()
  {
    return Translate.text("Outline");
  }
  
  /** Get a list of all the image components required by this filter. */
  
  public int getDesiredComponents()
  {
    return ComplexImage.RED+ComplexImage.GREEN+ComplexImage.BLUE+ComplexImage.DEPTH;
  }
  
  /** Apply the filter to an image.
      @param image      the image to filter
      @param scene      the Scene which was rendered to create the image
      @param camera     the camera from which the Scene was rendered
      @param coords     the position of the camera in the scene
  */
  
  public void filterImage(ComplexImage image, Scene scene, SceneCamera camera, CoordinateSystem cameraPos)
  {
    if (!image.hasFloatData(ComplexImage.DEPTH))
      return;
    double thickness = paramValue[0];
    if (thickness <= 0.0)
      return;
    float masks[][] = new float [][] {null, createMask(thickness, 0.0, 0.0),
        createMask(thickness, 0.0, -0.5), createMask(thickness, 0.0, 0.5),
        createMask(thickness, -0.5, 0.0), createMask(thickness, 0.5, 0.0)};
    float outline[] = findOutline(image, masks);
    applyOutline(image, ComplexImage.RED, outline, (float) paramValue[3]);
    applyOutline(image, ComplexImage.GREEN, outline, (float) paramValue[4]);
    applyOutline(image, ComplexImage.BLUE, outline, (float) paramValue[5]);
  }
  
  /** Create a map of the outlines. */
  
  private float [] findOutline(ComplexImage image, float masks[][])
  {
    int width = image.getWidth(), height = image.getHeight();
    byte edgeType[] = new byte [width*height];
    
    // First find the points that lie on edges.
    
    for (int i = 0; i < width; i++)
      for (int j = 0; j < height; j++)
      {
        float depth = image.getPixelComponent(i, j, ComplexImage.DEPTH);
        if (i > 0 && i < width-1 && isOutline(image.getPixelComponent(i-1, j, ComplexImage.DEPTH), depth, image.getPixelComponent(i+1, j, ComplexImage.DEPTH)))
          edgeType[i+j*width] = CENTER;
        else if (j > 0 && j < height-1 && isOutline(image.getPixelComponent(i, j-1, ComplexImage.DEPTH), depth, image.getPixelComponent(i, j+1, ComplexImage.DEPTH)))
          edgeType[i+j*width] = CENTER;
      }
    
    // Now smooth the outline by finding points on the edge that can be moved a half-pixel
    // in one direction.
    
    for (int i = 1; i < width-1; i++)
      for (int j = 1; j < height-1; j++)
      {
        if (edgeType[i+j*width] == NONE)
          continue;
        int count = 0;
        for (int k = -1; k < 2; k++)
          for (int m = -1; m < 2; m++)
            if (edgeType[i+k+(j+m)*width] != NONE)
              count++;
        if (count != 3)
          continue;
        if (edgeType[i-1+j*width] != NONE)
        {
          if (edgeType[i+1+(j-1)*width] != NONE)
            edgeType[i+j*width] = UP;
          else if (edgeType[i+1+(j+1)*width] != NONE)
            edgeType[i+j*width] = DOWN;
        }
        else if (edgeType[i+1+j*width] != NONE)
        {
          if (edgeType[i-1+(j-1)*width] != NONE)
            edgeType[i+j*width] = UP;
          else if (edgeType[i-1+(j+1)*width] != NONE)
            edgeType[i+j*width] = DOWN;
        }
        else if (edgeType[i+(j-1)*width] != NONE)
        {
          if (edgeType[i-1+(j+1)*width] != NONE)
            edgeType[i+j*width] = LEFT;
          else if (edgeType[i+1+(j+1)*width] != NONE)
            edgeType[i+j*width] = RIGHT;
        }
        else if (edgeType[i+(j+1)*width] != NONE)
        {
          if (edgeType[i-1+(j-1)*width] != NONE)
            edgeType[i+j*width] = LEFT;
          else if (edgeType[i+1+(j-1)*width] != NONE)
            edgeType[i+j*width] = RIGHT;
        }
      }
    
    // Now build the image of the outline.
    
    float outline[] = new float [width*height];
    int maskWidth[] = new int [masks.length];
    for (int i = 1; i < masks.length; i++)
      maskWidth[i] = (int) Math.sqrt(masks[i].length);
    for (int i = 0; i < width; i++)
      for (int j = 0; j < height; j++)
      {
        byte type = (edgeType[i+j*width]);
        if (type != NONE)
          drawOutlineSpot(i, j, outline, width, height, masks[type], maskWidth[type], 1.0f);
      }
    return outline;
  }
  
  /** Given the depths at three adjacent pixels, decide whether this is an outline. */
  
  private boolean isOutline(float d1, float d2, float d3)
  {
    double changeCutoff = paramValue[1];
    double relCutoff = paramValue[2];
    if (d1 > 1.0e6f)
      d1 = 1.0e6f;
    if (d2 > 1.0e6f)
      d2 = 1.0e6f;
    if (d3 > 1.0e6f)
      d3 = 1.0e6f;
    if (d2-d1 < d2*relCutoff && d2-d3 < d2*relCutoff)
      return false;
    if ((2.0*d2-d1-d3)/d2 > changeCutoff)
      return true;
    return false;
  }
  
  /** Draw a single dot into the image of the outline. */
  
  private void drawOutlineSpot(int i, int j, float outline[], int width, int height, float mask[], int maskWidth, float fraction)
  {
    int radius = (maskWidth-1)/2;
    int basex = i-radius, basey = j-radius;
    int xstart = (basex < 0 ? -basex : 0);
    int ystart = (basey < 0 ? -basey : 0);
    int xend = (basex+maskWidth >= width ? width-basex : maskWidth);
    int yend = (basey+maskWidth >= height ? height-basey : maskWidth);
    for (int y = ystart; y < yend; y++)
      {
        int maskBase = y*maskWidth;
        int imageBase = basex+(basey+y)*width;
        for (int x = xstart; x < xend; x++)
        {
          float val = mask[maskBase+x]*fraction;
          if (outline[imageBase+x] < val)
            outline[imageBase+x] = val;
        }
      }
  }
  
  /** Add the outline to one component of the image. */
  
  private void applyOutline(ComplexImage image, int component, float outline[], float color)
  {
    int width = image.getWidth(), height = image.getHeight();
    float pixel[] = new float [width*height];
    for (int i = 0; i < width; i++)
      for (int j = 0; j < height; j++)
      {
        float fract = outline[j*width+i];
        pixel[j*width+i] = fract*color + (1.0f-fract)*image.getPixelComponent(i, j, component);
      }
    image.setComponentValues(component, pixel);
  }

  /** Build the mask. */
  
  private float [] createMask(double thickness, double xoffset, double yoffset)
  {
    int size = (int) Math.ceil(thickness-0.001);
    if (size%2 == 0)
      size++;
    double radius = 0.5*thickness;
    int last = size-1, center = last/2;
    float mask[] = new float [size*size];

    for (int i = 0; i < size; i++)
      for (int j = 0; j < size; j++)
      {
        double dx1, dx2, dy1, dy2;
        if (i < center)
        {
          dx1 = center+xoffset-i-0.5;
          dx2 = center+xoffset-i+0.5;
        }
        else if (i == center)
          dx1 = dx2 = Math.abs(xoffset);
        else
        {
          dx1 = i-center-xoffset-0.5;
          dx2 = i-center-xoffset+0.5;
        }
        if (j < center)
        {
          dy1 = center+yoffset-j-0.5;
          dy2 = center+yoffset-j+0.5;
        }
        else if (j == center)
          dy1 = dy2 = Math.abs(yoffset);
        else
        {
          dy1 = j-center-yoffset-0.5;
          dy2 = j-center-yoffset+0.5;
        }
        double dist1 = Math.sqrt(dx1*dx1+dy1*dy1);
        if (dist1 > radius)
          continue;
        double dist2 = Math.sqrt(dx2*dx2+dy2*dy2);
        float val;
        if (dist2 < radius)
          val = 1.0f;
        else
          val = (float) ((radius-dist1)/(dist2-dist1));
        mask[i+j*size] = val;
      }
    return mask;
  }
  
  /** Get a list of parameters which affect the behavior of the filter. */
  
  public TextureParameter [] getParameters()
  {
    return new TextureParameter [] {
        new TextureParameter(this, Translate.text("Thickness"), 0.0, Double.MAX_VALUE, 3.0),
        new TextureParameter(this, Translate.text("Change Cutoff"), 0.0, Double.MAX_VALUE, 0.01),
        new TextureParameter(this, Translate.text("Distance Cutoff"), 0.0, Double.MAX_VALUE, 0.01),
        new TextureParameter(this, Translate.text("Red"), 0.0, Double.MAX_VALUE, 0.0),
        new TextureParameter(this, Translate.text("Green"), 0.0, Double.MAX_VALUE, 0.0),
        new TextureParameter(this, Translate.text("Blue"), 0.0, Double.MAX_VALUE, 0.0)
    };
  }

  /** Write a serialized description of this filter to a stream. */
  
  public void writeToStream(DataOutputStream out, Scene theScene) throws IOException
  {
    out.writeShort(0);
    for (int i = 0; i < paramValue.length; i++)
      out.writeDouble(paramValue[i]);
  }

  /** Reconstruct this filter from its serialized representation. */
  
  public void initFromStream(DataInputStream in, Scene theScene) throws IOException
  {
    int version = in.readShort();
    if (version != 0)
      throw new IOException("Unknown version "+version);
    for (int i = 0; i < paramValue.length; i++)
      paramValue[i] = in.readDouble();
  }

  /** Get a Widget with which the user can specify options for the filter. */ 

  public Widget getConfigPanel()
  {
    TextureParameter param[] = getParameters();
    FormContainer form = new FormContainer(2, 4);
    form.setColumnWeight(0, 0.0);
    
    // Create the labels.
    
    LayoutInfo leftLayout = new LayoutInfo(LayoutInfo.EAST, LayoutInfo.NONE, new Insets(0, 0, 0, 5), null);
    for (int i = 0; i < 3; i++)
      form.add(new BLabel(param[i].name+": "), 0, i, leftLayout);
    form.add(new BLabel(Translate.text("Color")+": "), 0, 3, leftLayout);
    
    // Define an inner class to act as a listener on a ValueField.
    
    class FieldListener
    {
      private int which;
      private ValueField field;
      
      public FieldListener(int which, ValueField field)
      {
        this.which = which;
        this.field = field;
      }
      
      void processEvent()
      {
        setParameterValue(which, field.getValue());
      }
    }
    
    // Create the editing components.
    
    LayoutInfo rightLayout = new LayoutInfo(LayoutInfo.WEST, LayoutInfo.NONE, null, null);
    for (int i = 0; i < 3; i++)
    {
      Widget w = param[i].getEditingWidget(paramValue[i]);
      w.addEventLink(ValueChangedEvent.class, new FieldListener(i, (ValueField) w));
      form.add(w, 1, i, rightLayout);
    }
    final RGBColor color = new RGBColor(paramValue[3], paramValue[4], paramValue[5]);
    final Widget sample = color.getSample(40, 30);
    form.add(sample, 1, 3, rightLayout);
    sample.addEventLink(MouseClickedEvent.class, new Object() {
      void processEvent()
      {
        new ColorChooser(UIUtilities.findFrame(sample), Translate.text("chooseOutlineColor"), color);
//        while (!(c instanceof Frame))
//          c = c.getParent();
//        new ColorChooser((Frame) c, "Choose Outline Color", color);
        paramValue[3] = color.getRed();
        paramValue[4] = color.getGreen();
        paramValue[5] = color.getBlue();
        sample.setBackground(color.getColor());
        sample.repaint();
      }
    });
    UIUtilities.applyBackground(form, null);
    sample.setBackground(color.getColor());
    return form;
  }
}
