/* Copyright (C) 2000,2003,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.procedural;

import artofillusion.*;
import artofillusion.image.*;
import artofillusion.math.*;
import artofillusion.ui.*;
import buoy.event.*;
import buoy.widget.*;
import java.awt.*;
import java.io.*;
import java.util.*;
import javax.swing.*;

/** This is a Module which outputs an image. */

public class ImageModule extends Module
{
  private ImageMap map;
  private boolean tilex, tiley, mirrorx, mirrory, wrapx, wrapy;
  private boolean pointOk, colorOk, valueOk[], gradOk[], outside;
  private double xscale, yscale, xinv, yinv;
  private double x, y, xsize, ysize, lastBlur;
  private int maxComponent;
  private PointInfo point;
  private RGBColor color;
  private float mask;
  private Vec2 tempGrad;
  private Vec3 gradient[];

  public ImageModule(Point position)
  {
    super("(Image)", new IOPort [] {new IOPort(IOPort.NUMBER, IOPort.INPUT, IOPort.LEFT, new String [] {"X", "(X)"}), 
      new IOPort(IOPort.NUMBER, IOPort.INPUT, IOPort.LEFT, new String [] {"Y", "(Y)"})}, 
      new IOPort [] {new IOPort(IOPort.COLOR, IOPort.OUTPUT, IOPort.RIGHT, new String [] {Translate.text("Color")}),
      new IOPort(IOPort.NUMBER, IOPort.OUTPUT, IOPort.RIGHT, new String [] {Translate.text("Red")}),
      new IOPort(IOPort.NUMBER, IOPort.OUTPUT, IOPort.RIGHT, new String [] {Translate.text("Green")}),
      new IOPort(IOPort.NUMBER, IOPort.OUTPUT, IOPort.RIGHT, new String [] {Translate.text("Blue")}), 
      new IOPort(IOPort.NUMBER, IOPort.OUTPUT, IOPort.RIGHT, new String [] {Translate.text("Mask")})}, 
      position);
    xscale = yscale = xinv = yinv = 1.0;
    tilex = tiley = true;
    color = new RGBColor(0.0f, 0.0f, 0.0f);
    tempGrad = new Vec2();
    gradient = new Vec3 [] {new Vec3(), new Vec3(), new Vec3(), new Vec3()};
    valueOk = new boolean [4];
    gradOk = new boolean [4];
  }
  
  /** Get the image map used by this module. */
  
  public ImageMap getMap()
  {
    return map;
  }
  
  /** Set the image map used by this module. */
  
  public void setMap(ImageMap map)
  {
    this.map = map;
    maxComponent = (map == null ? 0 : map.getComponentCount()-1);
  }
  
  /** Get the X scale. */
  
  public double getXScale()
  {
    return xscale;
  }
  
  /** Set the X scale. */
  
  public void setXScale(double scale)
  {
    xscale = scale;
    xinv = 1.0/scale;
  }
  
  /** Get the Y scale. */
  
  public double getYScale()
  {
    return yscale;
  }
  
  /** Set the Y scale. */
  
  public void setYScale(double scale)
  {
    yscale = scale;
    yinv = 1.0/scale;
  }
  
  /** Get whether the image is tiled in the X direction. */
  
  public boolean getTileX()
  {
    return tilex;
  }
  
  /** Set whether the image is tiled in the X direction. */
  
  public void setTileX(boolean b)
  {
    tilex = b;
  }
  
  /** Get whether the image is tiled in the Y direction. */
  
  public boolean getTileY()
  {
    return tiley;
  }
  
  /** Set whether the image is tiled in the Y direction. */
  
  public void setTileY(boolean b)
  {
    tiley = b;
  }

  /** Get whether the image is mirrored in the X direction. */
  
  public boolean getMirrorX()
  {
    return mirrorx;
  }
  
  /** Set whether the image is mirrored in the X direction. */
  
  public void setMirrorX(boolean b)
  {
    mirrorx = b;
  }
  
  /** Get whether the image is mirrored in the Y direction. */
  
  public boolean getMirrorY()
  {
    return mirrory;
  }
  
  /** Set whether the image is mirrored in the Y direction. */
  
  public void setMirrorY(boolean b)
  {
    mirrory = b;
  }

  /** New point, so the color will need to be recalculated. */

  public void init(PointInfo p)
  {
    point = p;
    pointOk = colorOk = false;
    valueOk[0] = valueOk[1] = valueOk[2] = valueOk[3] = false;
    gradOk[0] = gradOk[1] = gradOk[2] = gradOk[3] = false;
  }

  /** Find the point at which the image is being evaluated. */
  
  private void findPoint(double blur)
  {
    pointOk = true;
    colorOk = valueOk[0] = valueOk[1] = valueOk[2] = valueOk[3] = false;
    x = (linkFrom[0] == null) ? point.x : linkFrom[0].getAverageValue(linkFromIndex[0], blur);
    y = (linkFrom[1] == null) ? point.y : linkFrom[1].getAverageValue(linkFromIndex[1], blur);
    x *= xinv;
    y *= yinv;
    outside = (!tilex && (x < 0.0 || x > 1.0)) || (!tiley && (y < 0.0 || y > 1.0));
    if (outside)
      return;
    if (mirrorx)
      {
	double f = Math.floor(x);
	if ((((int) f)&1) == 0)
	  x = 1.0+f-x;
	else
	  x = x-f;
      }
    else
      x = x-Math.floor(x);
    if (mirrory)
      {
	double f = Math.floor(y);
	if ((((int) f)&1) == 0)
	  y = 1.0+f-y;
	else
	  y = y-f;
      }
    else
      y = y-Math.floor(y);
    xsize = (linkFrom[0] == null) ? 0.5*point.xsize+blur : linkFrom[0].getValueError(linkFromIndex[0], blur);
    ysize = (linkFrom[1] == null) ? 0.5*point.ysize+blur : linkFrom[1].getValueError(linkFromIndex[1], blur);
    xsize *= xinv;
    ysize *= yinv;
    wrapx = tilex && !mirrorx;
    wrapy = tiley && !mirrory;
  }

  /** Calculate the color. */
  
  public void getColor(int which, RGBColor c, double blur)
  {
    if (colorOk && blur == lastBlur)
      {
        c.copy(color);
        return;
      }
    if (map == null)
      {
        color.setRGB(0.0f, 0.0f, 0.0f);
        c.copy(color);
        return;
      }
    if (!pointOk || blur != lastBlur)
      findPoint(blur);
    colorOk = valueOk[0] = valueOk[1] = valueOk[2] = true;
    lastBlur = blur;
    if (outside)
      {
        // The point is outside the map.
        
        color.setRGB(0.0f, 0.0f, 0.0f);
        c.copy(color);
        return;
      }
    map.getColor(color, wrapx, wrapy, x, y, xsize, ysize);
    c.copy(color);
  }
  
  /** Get the value of one of the components. */
  
  public double getAverageValue(int which, double blur)
  {
    int component = which-1;
    if (component > maxComponent)
    {
      if (component == 3)
        return 0.0;
      component = 0;
    }
    if (valueOk[component] && blur == lastBlur)
      {
	if (which == 1)
	  return color.getRed();
	if (which == 2)
	  return color.getGreen();
        if (which == 3)
          return color.getBlue();
        return mask;
      }
    if (map == null)
      return 0.0;
    if (!pointOk || blur != lastBlur)
      findPoint(blur);
    if (outside)
      return 0.0;
    valueOk[component] = true;
    float value = map.getComponent(which-1, wrapx, wrapy, x, y, xsize, ysize);
    if (which == 1)
      color.setRGB(value, color.getGreen(), color.getBlue());
    else if (which == 2)
      color.setRGB(color.getRed(), value, color.getBlue());
    else if (which == 3)
      color.setRGB(color.getRed(), color.getGreen(), value);
    else
      mask = value;
    return value;
  }

  /** Calculate the gradient of one of the components. */

  public void getValueGradient(int which, Vec3 grad, double blur)
  {
    int component = which-1;
    if (component > maxComponent)
    {
      if (component == 3)
      {
	grad.set(0.0, 0.0, 0.0);
	return;
      }
      component = 0;
    }
    if (gradOk[component] && blur == lastBlur)
      {
	grad.set(gradient[component]);
	return;
      }
    if (map == null)
      {
	grad.set(0.0, 0.0, 0.0);
	return;
      }
    if (!pointOk || blur != lastBlur)
      findPoint(blur);
    if (outside)
      {
	grad.set(0.0, 0.0, 0.0);
	return;
      }
    map.getGradient(tempGrad, component, wrapx, wrapy, x, y, xsize, ysize);
    double dx = tempGrad.x*xinv, dy = tempGrad.y*yinv;
    Vec3 g = gradient[component];
    if (dx != 0.0)
      {
        if (linkFrom[0] == null)
          g.set(dx, 0.0, 0.0);
        else
          {
            linkFrom[0].getValueGradient(linkFromIndex[0], grad, blur);
            g.x = dx*grad.x;
            g.y = dx*grad.y;
            g.z = dx*grad.z;
          }
      }
    else
      g.set(0.0, 0.0, 0.0);
    if (dy != 0.0)
      {
        if (linkFrom[1] == null)
          g.y += dy;
        else
          {
            linkFrom[1].getValueGradient(linkFromIndex[1], grad, blur);
            g.x += dy*grad.x;
            g.y += dy*grad.y;
            g.z += dy*grad.z;
          }
      }
    gradOk[component] = true;
    grad.set(g);
  }

  public void calcSize()
  {
    bounds.width = ImageMap.PREVIEW_WIDTH+IOPort.SIZE*2;
    bounds.height = ImageMap.PREVIEW_HEIGHT+IOPort.SIZE*2;
    if (output.length*IOPort.SIZE*3 > bounds.height)
      bounds.height = output.length*IOPort.SIZE*3;
  }

  protected void drawContents(Graphics2D g)
  {
    if (map == null)
      {
        super.drawContents(g);
        return;
      }
    g.drawImage(map.getPreview(), bounds.x+bounds.width/2-ImageMap.PREVIEW_WIDTH/2, bounds.y+bounds.height/2-ImageMap.PREVIEW_HEIGHT/2, null);
  }
  
  /** Create a duplicate of this module. */
  
  public Module duplicate()
  {
    ImageModule mod = new ImageModule(new Point(bounds.x, bounds.y));
    
    mod.map = map;
    mod.xscale = xscale;
    mod.yscale = yscale;
    mod.xinv = xinv;
    mod.yinv = yinv;
    mod.color.copy(color);
    mod.tilex = tilex;
    mod.tiley = tiley;
    mod.mirrorx = mirrorx;
    mod.mirrory = mirrory;
    mod.wrapx = wrapx;
    mod.wrapy = wrapy;
    mod.maxComponent = maxComponent;
    return mod;
  }
  
  /** Allow the user to set a new value. */
  
  public boolean edit(final BFrame fr, final Scene theScene)
  {
    ImageMap oldMap = map;
    final ValueField xField = new ValueField(xscale, ValueField.NONE, 10);
    final ValueField yField = new ValueField(yscale, ValueField.NONE, 10);
    BCheckBox tilexBox = new BCheckBox("X", tilex);
    BCheckBox tileyBox = new BCheckBox("Y", tiley);
    BCheckBox mirrorxBox = new BCheckBox("X", mirrorx);
    BCheckBox mirroryBox = new BCheckBox("Y", mirrory);
    final BLabel preview = new BLabel() {
      public Dimension getPreferredSize()
      {
        return new Dimension(ImageMap.PREVIEW_WIDTH, ImageMap.PREVIEW_HEIGHT);
      }
    };
    if (map != null)
      preview.setIcon(new ImageIcon(map.getPreview()));
    preview.setAlignment(BLabel.CENTER);
    BOutline outline = new BOutline(preview, BorderFactory.createLineBorder(Color.black)) {
      public Dimension getMaximumSize()
      {
        return new Dimension(ImageMap.PREVIEW_WIDTH+2, ImageMap.PREVIEW_HEIGHT+2);
      }
    };
    preview.addEventLink(MouseClickedEvent.class, new Object() {
      void processEvent()
      {
        ImagesDialog dlg = new ImagesDialog(fr, theScene, map);
        if (dlg.getSelection() != map && dlg.getSelection() != null)
          {
            int w = dlg.getSelection().getWidth();
            int h = dlg.getSelection().getHeight();
            if (w > h)
              {
                xField.setValue(1.0);
                yField.setValue(((double) h)/w);
              }
            else
              {
                xField.setValue(((double) w)/h);
                yField.setValue(1.0);
              }
          }
        map = dlg.getSelection();
        preview.setIcon(map == null ? null : new ImageIcon(map.getPreview()));
      }
    });
    ComponentsDialog dlg = new ComponentsDialog(fr, "Click to Set Image:", 
      new Widget [] {outline, xField, yField, tilexBox, tileyBox, mirrorxBox, mirroryBox},
      new String [] {null, "X Size", "Y Size", "Tile", "", "Mirror", ""});
    if (!dlg.clickedOk())
      {
	map = oldMap;
	return false;
      }
    xscale = xField.getValue();
    yscale = yField.getValue();
    xinv = 1.0/xscale;
    yinv = 1.0/yscale;
    tilex = tilexBox.getState();
    tiley = tileyBox.getState();
    mirrorx = mirrorxBox.getState();
    mirrory = mirroryBox.getState();
    maxComponent = (map == null ? 0 : map.getComponentCount()-1);
    return true;
  }

  /** Write out the parameters. */

  public void writeToStream(DataOutputStream out, Scene theScene) throws IOException
  {
    if (map == null)
      out.writeInt(-1);
    else
      out.writeInt(theScene.indexOf(map));
    out.writeDouble(xscale);
    out.writeDouble(yscale);
    out.writeBoolean(tilex);
    out.writeBoolean(tiley);
    out.writeBoolean(mirrorx);
    out.writeBoolean(mirrory);
  }
  
  /** Read in the parameters. */
  
  public void readFromStream(DataInputStream in, Scene theScene) throws IOException
  {
    int index = in.readInt();
    
    if (index > -1)
      map = theScene.getImage(index);
    else
      map = null;
    xscale = in.readDouble();
    yscale = in.readDouble();
    xinv = 1.0/xscale;
    yinv = 1.0/yscale;
    tilex = in.readBoolean();
    tiley = in.readBoolean();
    mirrorx = in.readBoolean();
    mirrory = in.readBoolean();
    maxComponent = (map == null ? 0 : map.getComponentCount()-1);
  }
}
