/* Copyright (C) 2002-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.texture;

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


// TODO(MB): Mapping data is recorded in texture parameters because mapping
//          must be kept consistent when topology of the underlying mesh
//          changes. Find better solution.

/** UVMapping is a Mapping2D which allows the user to specify the texture
    coordinates of each vertex by hand. */

public class UVMapping extends Mapping2D
{
  int numTextureParams;
  TextureParameter uparam, vparam;

  public UVMapping(Texture theTexture)
  {
    super(theTexture);
  }

  public static String getName()
  {
    return "UV";
  }

  public static boolean legalMapping(Object3D obj, Texture tex)
  {
    while (obj instanceof ObjectWrapper)
      obj = ((ObjectWrapper) obj).getWrappedObject();
    return (tex instanceof Texture2D && (obj instanceof TriangleMesh || obj instanceof SplineMesh));
  }

  /** Create a UV mapped triangle. */

  public RenderingTriangle mapTriangle(int v1, int v2, int v3, int n1, int n2, int n3, Vec3 vert[])
  {
    return new UVMappedTriangle(v1, v2, v3, n1, n2, n3);
  }

  /** This method is called once the texture parameters for the vertices of a triangle
      are known. */
  
  public void setParameters(RenderingTriangle tri, double p1[], double p2[], double p3[], RenderingMesh mesh)
  {
    UVMappedTriangle uv = (UVMappedTriangle) tri;
    uv.setTextureCoordinates((float) p1[numTextureParams], (float) p1[numTextureParams+1],
        (float) p2[numTextureParams], (float) p2[numTextureParams+1],
        (float) p3[numTextureParams], (float) p3[numTextureParams+1],
        mesh.vert[uv.v1], mesh.vert[uv.v2], mesh.vert[uv.v3]);
  }
  
  /** This method should not generally be called.  The mapping is undefined without knowing
      the texture coordinates for a particular triangle. */

  public void getTextureSpec(Vec3 pos, TextureSpec spec, double angle, double size, double time, double param[])
  {
    if (!appliesToFace(angle > 0.0))
      {
        spec.diffuse.setRGB(0.0f, 0.0f, 0.0f);
        spec.specular.setRGB(0.0f, 0.0f, 0.0f);
        spec.transparent.setRGB(1.0f, 1.0f, 1.0f);
        spec.emissive.setRGB(0.0f, 0.0f, 0.0f);
        spec.roughness = spec.cloudiness = 0.0;
        spec.bumpGrad.set(0.0, 0.0, 0.0);
        return;
      }
    texture.getTextureSpec(spec, pos.x, pos.y, size, size, angle, time, param);
    if (texture.hasComponent(Texture.BUMP_COMPONENT))
      {
	double s = spec.bumpGrad.x;
	double t = spec.bumpGrad.y;
	spec.bumpGrad.set(s, t, 0.0);
      }
  }

  /** This method should not generally be called.  The mapping is undefined without knowing
      the texture coordinates for a particular triangle. */

  public void getTransparency(Vec3 pos, RGBColor trans, double angle, double size, double time, double param[])
  {
    if (!appliesToFace(angle > 0.0))
      trans.setRGB(1.0f, 1.0f, 1.0f);
    else
      texture.getTransparency(trans, pos.x, pos.y, size, size, angle, time, param);
  }

  /** This method should not generally be called.  The mapping is undefined without knowing
      the texture coordinates for a particular triangle. */

  public double getDisplacement(Vec3 pos, double size, double time, double param[])
  {
    return texture.getDisplacement(pos.x, pos.y, size, size, time, param);
  }

  /** Given a Mesh to which this mapping has been applied, return the texture coordinates at
      each vertex. */
  
  public Vec2 [] findTextureCoordinates(Mesh mesh)
  {
    TextureParameter param[] = mesh.getParameters();
    ParameterValue values[] = mesh.getParameterValues();
    if (mesh instanceof TriangleMesh && isPerFaceVertex((TriangleMesh) mesh))
    {
      // This is a per-face-vertex mapping, so we need to simplify it down to per-vertex.

      double uval[][] = null, vval[][] = null;
      for (int i = 0; i < param.length; i++)
        {
          if (param[i].equals(uparam))
            uval = ((FaceVertexParameterValue) values[i]).getValue();
          else if (param[i].equals(vparam))
            vval = ((FaceVertexParameterValue) values[i]).getValue();
        }
      TriangleMesh.Face face[] = ((TriangleMesh) mesh).getFaces();
      Vec2 uv[] = new Vec2 [mesh.getVertices().length];
      for (int i = 0; i < face.length; i++)
      {
        uv[face[i].v1] = new Vec2(uval[0][i], vval[0][i]);
        uv[face[i].v2] = new Vec2(uval[1][i], vval[1][i]);
        uv[face[i].v3] = new Vec2(uval[2][i], vval[2][i]);
      }
      return uv;
    }
    double uval[] = null, vval[] = null;
    for (int i = 0; i < param.length; i++)
      {
        if (param[i].equals(uparam))
          uval = ((VertexParameterValue) values[i]).getValue();
        else if (param[i].equals(vparam))
          vval = ((VertexParameterValue) values[i]).getValue();
      }
    Vec2 uv[] = new Vec2 [uval.length];
    for (int i = 0; i < uv.length; i++)
      uv[i] = new Vec2(uval[i], vval[i]);
    return uv;
  }

  /** Given a TriangleMesh to which this mapping has been applied, return the texture coordinates at
      each vertex of each face.  The return value is an array of size [3][# faces]
      containing the face-vertex texture coordinates. */
  
  public Vec2 [][] findFaceTextureCoordinates(TriangleMesh mesh)
  {
    int faces = mesh.getFaces().length;
    TextureParameter param[] = mesh.getParameters();
    ParameterValue values[] = mesh.getParameterValues();
    double uval[][] = null, vval[][] = null;
    for (int i = 0; i < param.length; i++)
      {
        if (param[i].equals(uparam))
          uval = ((FaceVertexParameterValue) values[i]).getValue();
        else if (param[i].equals(vparam))
          vval = ((FaceVertexParameterValue) values[i]).getValue();
      }
    Vec2 uv[][] = new Vec2 [3][faces];
    for (int i = 0; i < 3; i++)
      for (int j = 0; j < faces; j++)
        uv[i][j] = new Vec2(uval[i][j], vval[i][j]);
    return uv;
  }

  /** Given an object to which this mapping has been applied and the desired texture coordinates
      at each vertex, set the texture parameters accordingly. */
  
  public void setTextureCoordinates(Object3D obj, Vec2 uv[])
  {
    double uval[] = new double [uv.length], vval[] = new double [uv.length];
    for (int i = 0; i < uv.length; i++)
    {
      uval[i] = uv[i].x;
      vval[i] = uv[i].y;
    }
    obj.setParameterValue(uparam, new VertexParameterValue(uval));
    obj.setParameterValue(vparam, new VertexParameterValue(vval));
  }

  /** Given a triangle mesh to which this mapping has been applied and the desired texture coordinates
      at each vertex, set the texture parameters accordingly.  uv is an array of size [3][# faces]
      containing the face-vertex texture coordinates. */
  
  public void setFaceTextureCoordinates(Object3D obj, Vec2 uv[][])
  {
    while (obj instanceof ObjectWrapper)
      obj = ((ObjectWrapper) obj).getWrappedObject();
    TriangleMesh mesh = (TriangleMesh) obj;
    int faces = mesh.getFaces().length;
    double uval[][] = new double [3][faces], vval[][] = new double [3][faces];
    for (int i = 0; i < 3; i++)
      for (int j = 0; j < faces; j++)
      {
        uval[i][j] = uv[i][j].x;
        vval[i][j] = uv[i][j].y;
      }
    obj.setParameterValue(uparam, new FaceVertexParameterValue(uval));
    obj.setParameterValue(vparam, new FaceVertexParameterValue(vval));
  }
  
  /** Given a triangle mesh to which this mapping has been applied, determined whether the mapping
      is per-face-vertex. */
  
  public boolean isPerFaceVertex(TriangleMesh mesh)
  {
    TextureParameter param[] = mesh.getParameters();
    for (int i = 0; i < param.length; i++)
      if (param[i] == uparam || param[i] == vparam)
        return (mesh.getParameterValues()[i] instanceof FaceVertexParameterValue);
    return false;
  }

  public TextureMapping duplicate()
  {
    return duplicate(texture);
  }

  public TextureMapping duplicate(Texture tex)
  {
    UVMapping map = new UVMapping(tex);
    
    map.numTextureParams = numTextureParams;
    map.uparam = uparam;
    map.vparam = vparam;
    return map;
  }
  
  public void copy(TextureMapping mapping)
  {
    UVMapping map = (UVMapping) mapping; 
    
    numTextureParams = map.numTextureParams;
    uparam = map.uparam;
    vparam = map.vparam;
  }

  /* Get the list of texture parameters associated with this mapping and its texture.
     That includes the texture's parameters, and parameters for the texture coordinates. */
  
  public TextureParameter [] getParameters()
  {
    TextureParameter tp[] = getTexture().getParameters();
    numTextureParams = tp.length;
    TextureParameter p[] = new TextureParameter [numTextureParams+2];
    System.arraycopy(tp, 0, p, 0, numTextureParams);
    if (uparam == null)
      {
        uparam = new TextureParameter(this, "U", -Double.MAX_VALUE, Double.MAX_VALUE, 0.0);
        vparam = new TextureParameter(this, "V", -Double.MAX_VALUE, Double.MAX_VALUE, 0.0);
        uparam.type = TextureParameter.X_COORDINATE;
        vparam.type = TextureParameter.Y_COORDINATE;
        uparam.assignNewID();
        vparam.assignNewID();
      }
    p[numTextureParams] = uparam;
    p[numTextureParams+1] = vparam;
    return p;
  }

  public Widget getEditingPanel(Object3D obj, MaterialPreviewer preview)
  {
    return new Editor(obj, preview);
  }
  
  public UVMapping(DataInputStream in, Texture theTexture) throws IOException, InvalidObjectException
  {
    super(theTexture);

    short version = in.readShort();
    if (version != 0)
      throw new InvalidObjectException("");
    setAppliesTo(in.readShort());
  }
  
  public void writeToFile(DataOutputStream out) throws IOException
  {
    out.writeShort(0);
    out.writeShort(appliesTo());
  }
  
  /* Editor is an inner class for editing the mapping. */

  class Editor extends FormContainer
  {
    BComboBox applyToChoice;
    Object3D theObject;
    MaterialPreviewer preview;

    public Editor(Object3D obj, MaterialPreviewer preview)
    {
      super(1, 2);
      theObject = obj;
      this.preview = preview;
      
      // Add the various components to the Panel.
      
      add(Translate.button("editUVCoords", this, "doEdit"), 0, 0);
      RowContainer applyRow = new RowContainer();
      applyRow.add(new BLabel(Translate.text("applyTo")+":"));
      applyRow.add(applyToChoice = new BComboBox(new String [] {
        Translate.text("frontAndBackFaces"),
        Translate.text("frontFacesOnly"),
        Translate.text("backFacesOnly")
      }));
      add(applyRow, 0, 1);
      applyToChoice.setSelectedIndex(appliesTo());
      applyToChoice.addEventLink(ValueChangedEvent.class, this, "applyToChanged");
    }

    private void doEdit()
    {
      new UVMappingWindow((BDialog) UIUtilities.findWindow(this), theObject, UVMapping.this);
      Widget parent = getParent();
      while (!(parent instanceof TextureMappingDialog) && parent != null)
        parent = parent.getParent();
      if (parent != null)
        ((TextureMappingDialog) parent).setPreviewMapping(UVMapping.this);
      preview.render();
    }

    private void applyToChanged()
    {
      setAppliesTo((short) applyToChoice.getSelectedIndex());
      preview.setTexture(getTexture(), UVMapping.this);
      preview.render();
    }
  }
}