/// Evolver Copyright 2000, Morgan McGuire
/// matrix@alum.mit.edu   http://graphics3d.com/matrix
import java.text.*;
import java.io.*;

/**
 * All of the state for an animation.  An animation is a sequence of
 * AnimationChunks.
 */
public class Animation 
{
  /**
   * Must be alternating Behavior and Morph chunks, must begin and end
   * with a behavior.
   */
  private AnimationChunk chunk[];

  String filename;

  private boolean dirty = true;

  private GeneticParameters geneticParameters;

  public Animation(GeneticParameters gp)
  {
    geneticParameters = gp;
  }

  public Animation(int numChunks, GeneticParameters gp) 
  {
    geneticParameters = gp;
    chunk = new AnimationChunk[numChunks];

    for(int i = 0; i < chunk.length; i++) {      
      if ((i % 2) == 0) {
        chunk[i] = new BehaviorChunk();
      } else {
        chunk[i] = new MorphChunk();
      }
    }
  }

  public ChunkDisplay getDisplay(int index, AnimationEditor editor)
  {
    clean();
    return chunk[index].getDisplay(editor);
  }

  public int numChunks()
  {
    return chunk.length;
  }

  public void setDuration(int index, double duration)
  {
    chunk[index].duration = duration;
    setDirty();
  }

  public void setExpression(int index, ELExpression expression)
  {
    if (chunk[index] instanceof BehaviorChunk) {
      ((BehaviorChunk)chunk[index]).setExpression(expression);

      // Invalidate the neighboring chunks
      if (index > 0) {
        ((MorphChunk)chunk[index - 1]).invalidateExpression();
      }
      if (index < chunk.length - 1) {
        ((MorphChunk)chunk[index + 1]).invalidateExpression();
      }
    } else {
      throw new Error("Internal error: " + index + " is not a behavior chunk");
    }
  }

  public ELExpression getExpression(int index)
  {
    if (chunk[index] instanceof BehaviorChunk) {
      return ((BehaviorChunk)chunk[index]).getExpression();
    } else {
      throw new Error("Internal error: " + index + " is not a behavior chunk");
    }
  }

  /**
   * Produces a frame of animation.  Returns the number of the chunk rendered.
   * 
   * @result Where you want the frame rendered.  This matrix will be
   * dirty at the end.
   * 
   * @environment You should set all fields of this except
   * the morph interpolation parameter.  
   *
   * @hint This is the value renderFrame returned to you last time a
   * frame was rendered.  Pass in 0 the first time.  This hint indicates
   * which chunk was rendered. */
  public int renderFrame(ELEnvironment environment, ELMatrix result, int hint)
  {
    clean();
    double time = environment.getTime();
    
    // Figure out which expression to evaluate.  Find a chunk s.t. 
    // chunk[c].startTime <= time and chunk[c].startTime + chunk[c].duration > time.
    
    int c = hint;
    int deltaC;
    if (chunk[c].startTime > time) {
      deltaC = -1;
    } else {
      deltaC = 1;
    }
     
    // Loop until we find the right one
    while (! ((chunk[c].startTime <= time) && (chunk[c].startTime + chunk[c].duration > time))) {
      c += deltaC;
    }

    // Compute the morph interpolation parameter
    double m = (time - chunk[c].startTime) / chunk[c].duration;
    environment.setMorphInterpolationParameter(m);

    // Extract the expression
    ELExpression expression = chunk[c].getExpression();
    
    //System.out.println("  chunk " + c + " expression = " + expression);

    // Render
    expression.evaluate(result, environment, null);
    result.setDirty();

    return c;
  }

  public double getDuration(int index)
  {
    return chunk[index].duration;
  }

  public double getStartTime(int index)
  {
    clean();
    return chunk[index].startTime;
  }

  /**
   * You must call this whenever you change the state of something
   * in the animation.  It will trigger a clean when some value
   * is later accessed.
   */
  public void setDirty()
  {
    dirty = true;
  }

  /**
   * Recomputes start times and indices of each chunk.
   */
  public void clean()
  {
    if (dirty) {
      double time = 0;
      for(int i = 0; i < chunk.length; i++) {
        chunk[i].index = i;
        chunk[i].startTime = time;
        time += chunk[i].duration;
        

        // recompute morph expressions as needed
        if (chunk[i] instanceof MorphChunk) {
          MorphChunk mchunk = (MorphChunk)chunk[i];
          try {
            mchunk.computeExpression(chunk[i - 1].getExpression(), chunk[i + 1].getExpression(), geneticParameters);
          } catch (NullPointerException e) {
            System.out.println("Error in chunk " + i + ": " + e.toString());
            throw new Error("");
          }
        }
      }
      
    }
    dirty = false;
  }

  /**
   * Saves this animation to file, overwriting any pre-existing
   * file.  
   */
  public void save(File file)
  {
    try {
      FileWriter out = new FileWriter(file);
      
      System.out.print("Writing " + file.getAbsolutePath() + "...");
      
      out.write("// Evolver 1.0 Animation\n");
      out.write("NumChunks = " + chunk.length + "\n");
      
      for(int i = 0; i < chunk.length; i++) {
        out.write(chunk[i].unparse());
      }
      
      out.close();
      this.filename = file.getAbsolutePath();
      System.out.println("done");
    } catch (IOException e) {
      throw new Error(e.toString());
    }
  }

  /**
   * Loads an animation from file.
   * @param gp The genetic parameters that should be used to parse this file.
   */    
  public void load(File file, GeneticParameters gp)
  {
    try {
      StreamTokenizer st = new StreamTokenizer(new BufferedReader(new FileReader(file)));
      st.quoteChar((int)'\"');
      st.slashStarComments(false);
      st.slashSlashComments(true);
      st.eolIsSignificant(false);
      st.parseNumbers();

      st.nextToken();

      // Read header
      readIdentifier(st, "NumChunks");
      readSymbol(st, '=');
    
      int numChunks = (int)readNumber(st);
      if ((numChunks > 0) && (numChunks < 4096)) {
        chunk = new AnimationChunk[numChunks];
      } else {
        throw new Error("Back num chunks " + numChunks);
      }

      int i = 0;
      while (st.ttype != StreamTokenizer.TT_EOF) {
        String chunkType = readString(st);
        readSymbol(st, ':');
        readIdentifier(st, "duration");
        readSymbol(st, '=');
        double duration = readNumber(st);
        if (chunkType.equals("BehaviorChunk")) {
          readSymbol(st, ',');
          readIdentifier(st, "expression");
          readSymbol(st, '=');
          String expressionString = readQuotedString(st);

          BehaviorChunk b = new BehaviorChunk();
          b.setExpression(gp.parseELExpression(expressionString));
          chunk[i] = b;
          //System.out.println("" + i + ": behavior"); 
        
        } else if (chunkType.equals("MorphChunk")) {
          chunk[i] = new MorphChunk();

        } else {
          throw new Error("Unknown chunk type: " + chunkType);
        }

        chunk[i].duration = duration;
        i++;
      }
      
      if (i < numChunks) {
        System.out.println("Warning: " + numChunks + " declared, " + i + " chunks found!");
        while(i < numChunks) {
          chunk[i] = chunk[i - 1];
          i++;
        }
      }
    } catch (IOException e) {
      throw new Error(e.toString());
    }
    setDirty();
  }

  private void readSymbol(StreamTokenizer st, int symbol) throws IOException
  {
    if (st.ttype != symbol) {
      throw new Error("Line " + st.lineno() + ": Expected " + new Character((char)symbol) + ", got " + tokenToString(st));
    }
    st.nextToken();
  }
  
  // For error messages.  Does not consume the token.
  private String tokenToString(StreamTokenizer st)
  {
    if (st.ttype == StreamTokenizer.TT_WORD) {
      return st.sval;
    } else if (st.ttype == StreamTokenizer.TT_NUMBER) {
      return "" + st.nval;
    } else {
      return "ttype = " + st.ttype + " ('" + new Character((char)(st.ttype)) + "')";
    }
  }

  private void readIdentifier(StreamTokenizer st, String identifier) throws IOException
  {
    if ((st.ttype != StreamTokenizer.TT_WORD) || !(st.sval.equals(identifier))) {
      throw new Error("Line " + st.lineno() + ": Expected " + identifier + ", got " + tokenToString(st));
    }
    st.nextToken();
  }

  private double readNumber(StreamTokenizer st) throws IOException
  {
    if (st.ttype != StreamTokenizer.TT_NUMBER) {
      throw new Error("Line " + st.lineno() + ": Expected number, got " + tokenToString(st));
    }
    double num = st.nval;
    st.nextToken();
    return num;
  }

  private String readString(StreamTokenizer st) throws IOException
  {
    if (st.ttype != StreamTokenizer.TT_WORD) {
      throw new Error("Line " + st.lineno() + ": Expected string, got " + tokenToString(st));
    }
    String s = st.sval;
    st.nextToken();
    return s;
  }

  private String readQuotedString(StreamTokenizer st) throws IOException
  {
    if (st.ttype != '\"') {
      throw new Error("Line " + st.lineno() + ": Expected quotedstring, got " + tokenToString(st));
    }
    String s = st.sval;
    st.nextToken();
    return s;
  }


  static NumberFormat secondFormat;

  static public String formatTime(double time)
  {
    if (secondFormat == null) {
      secondFormat = NumberFormat.getInstance();
      secondFormat.setMinimumIntegerDigits(2);
      secondFormat.setMaximumFractionDigits(2);
      secondFormat.setMinimumFractionDigits(2);
    }
    
    int minutes = (int)Math.floor(time / 60);
    double seconds = time - minutes * 60;
    return "" + minutes + ":" + secondFormat.format(seconds);
  }
}

