/* Copyright (C) 2000-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.math.*;
import java.awt.*;
import java.io.*;
import java.util.*;
import java.lang.reflect.*;

/** This represents a procedure for calculating a set of values (typically, the parameters
    for a texture or material). */

public class Procedure
{
  protected OutputModule output[];
  protected Module module[];
  protected Link link[];

  public Procedure(OutputModule output[])
  {
    this.output = output;
    module = new Module [0];
    link = new Link [0];
  }
  
  /** Get the list of output modules. */
  
  public OutputModule [] getOutputModules()
  {
    return output;
  }
  
  /** Get the list of all other modules. */
  
  public Module [] getModules()
  {
    return module;
  }
  
  /** Get the index of a particular module. */
  
  public int getModuleIndex(Module mod)
  {
    for (int i = 0; i < module.length; i++)
      if (module[i] == mod)
        return i;
    return -1;
  }
  
  /** Get the index of a particular output module. */
  
  public int getOutputIndex(Module mod)
  {
    for (int i = 0; i < output.length; i++)
      if (output[i] == mod)
        return i;
    return -1;
  }

  /** Add a module to the procedure. */
  
  public void addModule(Module mod)
  {
    Module newmod[] = new Module [module.length+1];
    for (int i = 0; i < module.length; i++)
      newmod[i] = module[i];
    newmod[module.length] = mod;
    module = newmod;
  }
  
  /** Delete a module from the procedure.  Any links involving this module should be deleted
      *before* calling this method. */

  public void deleteModule(int which)
  {
    Module newmod[] = new Module [module.length-1];
    int i, j;
    for (i = 0, j = 0; i < module.length; i++)
      if (i != which)
        newmod[j++] = module[i];
    module = newmod;
  }

  /** Delete modules <b>and its links</b> from the procedure. */

  public void deleteModulesAndLinks(boolean moduleselection[], boolean linkselection[])
  {
    boolean modsel[] = moduleselection;
    boolean linksel[];

    if (linkselection == null)
      linksel = new boolean[link.length];
    else
      linksel = (boolean[]) linkselection.clone();

    int num = 0;
    int i, j;
    for (i = 0; i < linksel.length; i++)
      for (j = 0; j < modsel.length; j++)
        if ((module[j] == link[i].from.getModule() && modsel[j]) ||
            (module[j] == link[i].to.getModule() && modsel[j]))
        linksel[i] = true;

    // Delete any selected links.

    for (i = linksel.length-1; i >= 0; i--)
      if (linksel[i])
        deleteLink(i);

    // Now delete any selected modules.

    for (i = modsel.length-1; i >= 0; i--)
      if (modsel[i])
        deleteModule(i);
  }

  /** Returns the first object toobj in toarr for which <br>
      &nbsp;&nbsp ((fromarr[i] == fromobj) && (toarr[i] == toobj)) <br>
      is true or null if not possible.<br>
      The length of toarr must not be less than length of fromarr.*/

  public static Object mapArrayIdent(Object[] fromarr, Object fromobj, Object[] toarr)   // TODO(MB) Move to Utilities class?
  {
    int i = Utilities.findIndexIdent(fromarr, fromobj);

    if (i == -1)
      return null;

    return toarr[i];
  }

  /** Create module group. A module group is a set of modules and links without output modules.
      At the moment, it is represented as a procedure.<br>
      It is used for clipboard operations.

    @param which indices of modules the group will consist of.*/

  public Procedure createModuleGroup(int[] which)
  {
    Procedure result = new Procedure(new OutputModule[0]);   // TODO(MB) Change parameter to null or create new class.
    int m, l;
    Vector linkdst = new Vector();   // Vector of Link objects

    Module[] modulesrc = new Module [which.length];
    Module[] moduledst = new Module [which.length];
    for (m = 0; m < which.length; ++m)
    {
      modulesrc[m] = module[which[m]];
      moduledst[m] = modulesrc[m].duplicate();
      moduledst[m].clearLinks();
    }

    for (int i = 0; i < link.length; i++)
    {
      Module fromModuleSrc = link[i].from.getModule();
      Module toModuleSrc = link[i].to.getModule();
      Module fromModuleDst = (Module)mapArrayIdent(modulesrc, fromModuleSrc, moduledst);
      if (fromModuleDst == null)  // Link goes out of module group
        continue;

      Module toModuleDst = (Module)mapArrayIdent(modulesrc, toModuleSrc, moduledst);
      if (toModuleDst == null)  // Link goes out of module group
        continue;

      IOPort from = (IOPort)mapArrayIdent(fromModuleSrc.getOutputPorts(), link[i].from, fromModuleDst.getOutputPorts());
      IOPort to = (IOPort)mapArrayIdent(toModuleSrc.getInputPorts(), link[i].to, toModuleDst.getInputPorts());

      linkdst.add(new Link(from, to));
      toModuleDst.setInput(to, from);
    }

    result.module = moduledst;
    result.link = (Link[])linkdst.toArray(new Link[0]);

    return result;
  }

  /** Removes all modules (and their links) which are forbidden.
    @param allowParameters ParameterModule allowed here?
    */
  public void removeForbidden(boolean allowParameters)
  {
    boolean modsel[] = new boolean[module.length];

    for(int i = 0; i < module.length; ++i)
    {
      if (module[i] instanceof ParameterModule)
        if (!allowParameters)
          modsel[i] = true;
    }

    deleteModulesAndLinks(modsel, null);
  }


  /** Insert <b>a copy of the module group group</b> into this procedure.

    @param group the group to insert
    @param allowParameters allows the ProcedureOwner to add a ParameterModule
    @return number of added modules
  */
  public int insertGroup(Procedure group, boolean allowParameters)
  {
    Procedure insert = new Procedure(new OutputModule[0]);
    insert.copy(group);
    int i;

    insert.removeForbidden(allowParameters);

    for(i = 0; i < insert.module.length; ++i)
    {
      addModule(insert.module[i]);
    }

    for(i = 0; i < insert.link.length; ++i)
      addLink(insert.link[i]);

    return insert.module.length;
  }

  /** Get the list of links between modules. */
  
  public Link [] getLinks()
  {
    return link;
  }
  
  /** Add a link to the procedure. */
  
  public void addLink(Link ln)
  {
    Link newlink[] = new Link [link.length+1];
    for (int i = 0; i < link.length; i++)
      newlink[i] = link[i];
    newlink[link.length] = ln;
    link = newlink;
  }
  
  /** Delete a link from the procedure. */
  
  public void deleteLink(int which)
  {
    Link newlink[] = new Link [link.length-1];
    int i, j;

    if (link[which].to.getType() == IOPort.INPUT)
      link[which].to.getModule().setInput(link[which].to, null);
    else
      link[which].from.getModule().setInput(link[which].from, null);
    for (i = 0, j = 0; i < link.length; i++)
      if (i != which)
        newlink[j++] = link[i];
    link = newlink;
  }
  
  /** Check for feedback loops in this procedure. */
  
  public boolean checkFeedback()
  {
    for (int i = 0; i < output.length; i++)
      {
        for (int j = 0; j < output.length ; j++)
          output[j].checked = false;
        for (int j = 0; j < module.length ; j++)
          module[j].checked = false;
        if (output[i].checkFeedback())
          return true;
      }
    return false;
  }
  
  /** This routine is called before the procedure is evaluated.  The PointInfo object 
      describes the point for which it is to be evaluated. */
  
  public void initForPoint(PointInfo p)
  {
    for (int i = 0; i < module.length; i++)
      module[i].init(p);
  }
  
  /** This routine returns the value of the specified output module.  If that output does
      not have value type NUMBER, the results are undefined. */
  
  public double getOutputValue(int which)
  {
    return output[which].getAverageValue(0, 0.0);
  }
  
  /** This routine returns the gradient of the specified output module.  If that output does
      not have value type NUMBER, the results are undefined. */
  
  public void getOutputGradient(int which, Vec3 grad)
  {
    output[which].getValueGradient(0, grad, 0.0);
  }
  
  /** This routine returns the color of the specified output module.  If that output does
      not have value type COLOR, the results are undefined. */
  
  public void getOutputColor(int which, RGBColor color)
  {
    output[which].getColor(0, color, 0.0);
  }
  
  /** Make this procedure identical to another one.  The output modules must already
      be set up before calling this method. */
  
  public void copy(Procedure proc)
  {
    module = new Module [proc.module.length];
    for (int i = 0; i < module.length; i++)
    {
      module[i] = proc.module[i].duplicate();
      module[i].clearLinks();
    }
    link = new Link [proc.link.length];
    for (int i = 0; i < link.length; i++)
      {
        Module fromModule = proc.link[i].from.getModule();
        Module toModule = proc.link[i].to.getModule();
        int fromIndex = proc.getModuleIndex(fromModule);
        int toIndex = toModule instanceof OutputModule ? proc.getOutputIndex(toModule) : proc.getModuleIndex(toModule);
        IOPort from = module[fromIndex].getOutputPorts()[proc.module[fromIndex].getOutputIndex(proc.link[i].from)];
        IOPort to = toModule instanceof OutputModule ?
                output[toIndex].getInputPorts()[proc.output[toIndex].getInputIndex(proc.link[i].to)] :
                module[toIndex].getInputPorts()[proc.module[toIndex].getInputIndex(proc.link[i].to)];
        link[i] = new Link(from, to);
        to.getModule().setInput(to, from);
      }
  }
  
  /** Write this procedure to an output stream. */
  
  public void writeToStream(DataOutputStream out, Scene theScene) throws IOException
  {
    out.writeShort(0);
    out.writeInt(module.length);
    for (int i = 0; i < module.length; i++)
      {
	out.writeUTF(module[i].getClass().getName());
	out.writeInt(module[i].getBounds().x);
	out.writeInt(module[i].getBounds().y);
	module[i].writeToStream(out, theScene);
      }
    out.writeInt(link.length);
    for (int i = 0; i < link.length; i++)
      {
	out.writeInt(getModuleIndex(link[i].from.getModule()));
	out.writeInt(link[i].from.getModule().getOutputIndex(link[i].from));
	if (link[i].to.getModule() instanceof OutputModule)
	  out.writeInt(-getOutputIndex(link[i].to.getModule())-1);
	else
	  {
	    out.writeInt(getModuleIndex(link[i].to.getModule()));
	    out.writeInt(link[i].to.getModule().getInputIndex(link[i].to));
	  }
      }
  }
  
  /** Reconstruct this procedure from an input stream.  The output modules must already
      be set up before calling this method. */

  public void readFromStream(DataInputStream in, Scene theScene) throws IOException, InvalidObjectException
  {
    short version = in.readShort();

    if (version != 0)
      throw new InvalidObjectException("");
    for (int i = 0; i < output.length; i++)
      output[i].setInput(output[i].getInputPorts()[0], null);
    module = new Module [in.readInt()];
    try
      {
        for (int i = 0; i < module.length; i++)
          {
	    String classname = in.readUTF();
	    Point p = new Point(in.readInt(), in.readInt());
	    Class cls = ModellingApp.getClass(classname);
	    Constructor con = cls.getConstructor(new Class [] {Point.class});
	    module[i] = (Module) con.newInstance(new Object [] {p});
	    module[i].readFromStream(in, theScene);
          }
      }
    catch (InvocationTargetException ex)
      {
	ex.getTargetException().printStackTrace();
	throw new IOException();
      }
    catch (Exception ex)
      {
	ex.printStackTrace();
	throw new IOException();
      }
    link = new Link [in.readInt()];
    for (int i = 0; i < link.length; i++)
      {
        IOPort to, from = module[in.readInt()].getOutputPorts()[in.readInt()];
        int j = in.readInt();
        if (j < 0)
          to = output[-j-1].getInputPorts()[0];
        else
          to = module[j].getInputPorts()[in.readInt()];
        link[i] = new Link(from, to);
        to.getModule().setInput(to, from);
      }
  }
}
