/*
 * Open Source Physics software is free software as described near the bottom of this code file.
 *
 * For additional information and documentation on Open Source Physics please see:
 * <http://www.opensourcephysics.org/>
 */

package org.opensourcephysics.ejs;
import java.lang.reflect.Array;
import java.lang.reflect.Field;

/**
 * A base interface for a simulation
 */
public abstract class Simulation implements java.lang.Runnable {
  static public final int MAXIMUM_FPS = 25;
  static public final int MINIMUM_FPS = 1;
  private Model model = null;
  private View view = null;
  private java.lang.Thread thread = null;
  private boolean autoplay = false, isPlaying = false;
  private long delay = 0;
  private java.net.URL codebase = null;

  private void errorMessage(String _text) {
    System.err.println(this.getClass().getName()+": "+_text); //$NON-NLS-1$
  }

  private void errorMessage(Exception _exc) {
    System.err.println(this.getClass().getName()+": Exception caught! Text follows:"); //$NON-NLS-1$
    _exc.printStackTrace(System.err);
  }

  // -----------------------------
  // Relationship with its parts
  // -----------------------------
  public Model getModel() {
    return model;
  }

  public void setModel(Model _aModel) {
    model = _aModel;
  }

  public View getView() {
    return view;
  }

  public void setView(View _aView) {
    view = _aView;
  }

  /**
    * Sets the codebase
    */
  public void setCodebase(java.net.URL _codebase) {
    codebase = _codebase;
  }

  /**
    * Returns the codebase
    */
  public java.net.URL getCodebase() {
    return codebase;
  }

  // -----------------------------
  // Controlling the execution
  // -----------------------------

  /**
    * Sets the simulation in play mode
    */
  public void play() {
    if(thread!=null) {
      return;
    }
    thread = new Thread(this);
    thread.setPriority(Thread.MIN_PRIORITY);
    thread.start();
    isPlaying = true;
  }

  /**
    * Stops the simulation
    */
  public void pause() {
    thread = null;
    isPlaying = false;
  }

  /**
    * Implementation of the Runnable interface
    */
  public void run() {
    while(thread==Thread.currentThread()) {
      step();
      try {
        Thread.sleep(delay);
      } catch(InterruptedException ie) {}
    }
  }

  /**
   * Sets the (approximate) number of frames per second for the simulation
   * @param _fps the number of frames per second
   */
  public void setFPS(int _fps) {
    if(_fps<=MINIMUM_FPS) {
      delay = 1000;
    } else if(_fps>=MAXIMUM_FPS) {
      delay = 0;
    } else {
      delay = (long) (1000.0/_fps);
    }
  }

  /**
   * Sets the delay between two steps of the simulation
   * @param _aDelay the number of milliseconds for the delay
   */
  public void setDelay(int _aDelay) {
    if(_aDelay<0) {
      delay = 0;
    } else {
      delay = _aDelay;
    }
  }

  /**
   * Sets whether the simulation should be set to play mode when it is reset.
   * Default is false.
   * @param _play Whether it should play
   */
  public void setAutoplay(boolean _play) {
    autoplay = _play;
  }

  /**
   * Returns whether the simulation is running or not
   */
  public boolean isPlaying() {
    return isPlaying;
  }

  /**
   * Returns whether the simulation is running or not
   */
  public boolean isPaused() {
    return !isPlaying;
  }

  // ------------------------------------
  // Simulation logic based on the model
  // ------------------------------------

  /**
   * Resets the simulation to a complete initial state
   */
  public void reset() {
    pause();
    if(model!=null) {
      model.reset();
      model.initialize();
      model.update();
    }
    if(view!=null) {
      view.reset();
      view.initialize();
      view.update();
    }
    System.gc();
    if(autoplay) {
      play();
    }
  }

  /**
   * Initialize model using user interface changes
   */
  public void initialize() {
    if(view!=null) {
      view.read();
    }
    if(model!=null) {
      model.initialize();
      model.update();
    }
    if(view!=null) {
      // view.reset();
      view.initialize();
      view.update();
    }
  }

  /**
   * apply user interface changes
   */
  public void apply() {
    if(view!=null) {
      view.read();
    }
    update();
  }

  /**
   * apply user interface changes. Yes, exactly the same as apply() (I need it somewhere else :-)
   */
  public void applyAll() {
    // if (view!=null) Commented for optimization
    view.read();
    update();
  }

  /**
   * apply a single change in the user interface
   */
  public void apply(String _variable) {
    if(view!=null) {
      view.read(_variable);
    }
    // update(); // Should be called by the user
  }

  /**
   * update
   */
  public void update() {
    if(model!=null) {
      model.update();
    }
    if(view!=null) {
      view.update(); // View could be null
    }
  }

  /**
   * step
   */
  public void step() {
    // if (model!=null) Commented for optimization
    model.step();
    update();
  }

  // --------------------------------------------------------
  // Accesing model methods
  // --------------------------------------------------------
  public void updateAfterModelAction() {
    // if (view!=null) view.initialize(); // If initialize sends all the data, this is redundant
    update();
  }

  // --------------------------------------------------------
  // Accesing model variables
  // --------------------------------------------------------
  static private final String dummy = ""; //$NON-NLS-1$
  static private final Class<?> strClass = dummy.getClass();

  /**
     * This method returns a String with the value of a public variable of the
     * model. If the variable is an array, individual element values are
     * separated by a comma. Only public variables of primitive or String
     * type can be accessed.
     * @param _name The name of a public variable of the model
     * @return The value of the variable as a String.
     */
  public String getVariable(String _name) {
    return getVariable(_name, ","); //$NON-NLS-1$
  }

  /**
   * This method returns a String with the value of a public variable of the
   * model. If the variable is an array, individual element values are
   * separated by the specified separator string. Only public variables
   * of primitive or String type can be accessed.
   * @param _name The name of a public variable of the model
   * @param _sep A separator string to use for array variables
   * @return The value of the variable
   */
  public String getVariable(String _name, String _sep) {
    if(model==null) {
      return null;
    }
    try {
      Field field = model.getClass().getField(_name);
      if(field.getType().isArray()) {
        String txt = ""; //$NON-NLS-1$
        Object array = field.get(model);
        int l = Array.getLength(array);
        for(int i = 0; i<l; i++) {
          if(i>0) {
            txt += _sep+Array.get(array, i).toString();
          } else {
            txt += Array.get(array, i).toString();
          }
        }
        return txt;
      }
      return field.get(model).toString();
    } catch(Exception exc) {
      errorMessage(exc);
      return null;
    }
  }

  /**
   * This method sets the value of a public variable of the model. If the
   * variable is an array, individual element values must separated by a
   * comma.
   * In this case, if the number of values specifies differs with the
   * length of the array (a warning may be issued and) either the extra values
   * are ignored (if there are more) or the last elements of the array will be
   * left unmodified (if there are less).
   * If the values provided cannot be parsed to the variable type (an error
   * message may be issued and) the method returns false.
   * Only public variables of primitive or String type can be accessed.
   * @param _name the name of a public variable of the model
   * @param _value the value to be given to the variable
   * @return true if the process was completed sucessfully, false otherwise
   */
  public boolean setVariable(String _name, String _value) {
    return setVariable(_name, _value, ","); //$NON-NLS-1$
  }

  /**
   * This method sets the value of a public variable of the model.
   * If the variable is an array, individual element values must separated by
   * the specified separator string.
   * In this case, if the number of values specifies differs with the
   * length of the array (a warning may be issued and) either the extra values
   * are ignored (if there are more) or the last elements of the array will be
   * left unmodified (if there are less).
   * Only public variables of primitive or String type can be accessed.
   * If the values provided cannot be parsed to the variable type (an error
   * message may be issued and) the method returns false.
   * @param _variable the name of a public variable of the model
   * @param _value the value to be given to the variable
   * @param _sep the separator string for arrays
   * @return true if the process was completed sucessfully, false otherwise
   */
  public boolean setVariable(String _variable, String _value, String _sep) {
    if(model==null) {
      return false;
    }
    try {
      Field field = model.getClass().getField(_variable);
      if(field.getType().isArray()) {
        boolean result = true;
        Object array = field.get(model);
        int i = 0, l = Array.getLength(array);
        Class<?> type = field.getType().getComponentType();
        java.util.StringTokenizer line = new java.util.StringTokenizer(_value, _sep);
        if(l<line.countTokens()) {
          errorMessage("Warning: there are less elements in the array than values provided!"); //$NON-NLS-1$
        } else if(l>line.countTokens()) {
          errorMessage("Warning: there are more elements in the array than values provided!"); //$NON-NLS-1$
        }
        while(line.hasMoreTokens()&&(i<l)) {
          String token = line.nextToken();
          if(type.equals(Double.TYPE)) {
            Array.setDouble(array, i, Double.parseDouble(token));
          } else if(type.equals(Float.TYPE)) {
            Array.setFloat(array, i, Float.parseFloat(token));
          } else if(type.equals(Byte.TYPE)) {
            Array.setByte(array, i, Byte.parseByte(token));
          } else if(type.equals(Short.TYPE)) {
            Array.setShort(array, i, Short.parseShort(token));
          } else if(type.equals(Integer.TYPE)) {
            Array.setInt(array, i, Integer.parseInt(token));
          } else if(type.equals(Long.TYPE)) {
            Array.setLong(array, i, Long.parseLong(token));
          } else if(type.equals(Boolean.TYPE)) {
            if(token.trim().toLowerCase().equals("true")) {                                    //$NON-NLS-1$
              Array.setBoolean(array, i, true);
            } else {
              Array.setBoolean(array, i, false);
            }
          } else if(type.equals(Character.TYPE)) {
            Array.setChar(array, i, token.charAt(0));
          } else if(type.equals(strClass)) {
            Array.set(array, i, token);
          } else {
            result = false;
          }
          i++;
        }
        return result;
      }
      Class<?> type = field.getType();
      if(type.equals(Double.TYPE)) {
        field.setDouble(model, Double.parseDouble(_value));
      } else if(type.equals(Float.TYPE)) {
        field.setFloat(model, Float.parseFloat(_value));
      } else if(type.equals(Byte.TYPE)) {
        field.setByte(model, Byte.parseByte(_value));
      } else if(type.equals(Short.TYPE)) {
        field.setShort(model, Short.parseShort(_value));
      } else if(type.equals(Integer.TYPE)) {
        field.setInt(model, Integer.parseInt(_value));
      } else if(type.equals(Long.TYPE)) {
        field.setLong(model, Long.parseLong(_value));
      } else if(type.equals(Boolean.TYPE)) {
        if(_value.trim().toLowerCase().equals("true")) {                                       //$NON-NLS-1$
          field.setBoolean(model, true);
        } else {
          field.setBoolean(model, false);
        }
      } else if(type.equals(Character.TYPE)) {
        field.setChar(model, _value.charAt(0));
      } else if(type.equals(strClass)) {
        field.set(model, _value);
      } else {
        return false;
      }
      return true;
    } catch(Exception exc) {
      errorMessage(exc);
      return false;
    }
  }

  /**
   * This method is used to set more than one variables of the model
   * at once. Pairs of the type 'variable=value' must be separated
   * by semicolons. Then they will be tokenized and sent to setVariable().
   * @param _valueList the string containing the pairs 'variable=value'
   * @return true if all the variables are correctly set by setVariable()
   * @see setVariable(String,String)
   *
   */
  public boolean setVariables(String _valueList) {
    return setVariables(_valueList, ";", ","); //$NON-NLS-1$ //$NON-NLS-2$
  }

  /**
   * This method is used to set more than one variables of the model
   * at once. Pairs of the type 'variable=value' must be separated
   * by the separator string _sep. Then they will be tokenized and
   * sent to setVariable(), using _arraySep as separator string for
   * values of array variables.
   * @param _valueList the string containing the pairs 'variable=value'
   * @param _sep the separator string between pairs
   * @param _arraySep the separator string for values of array variables
   * @return true if all the variables are correctly set by setVariable()
   * @see setVariable(String,String)
   *
   */
  public boolean setVariables(String _valueList, String _sep, String _arraySep) {
    boolean result = true;
    String name = "", value = ""; //$NON-NLS-1$ //$NON-NLS-2$
    java.util.StringTokenizer line = new java.util.StringTokenizer(_valueList, _sep);
    while(line.hasMoreTokens()) {
      String token = line.nextToken();
      int index = token.indexOf('=');
      if(index<0) {
        result = false;
        continue;
      }
      name = token.substring(0, index).trim();
      value = token.substring(index+1).trim();
      boolean partial = setVariable(name, value, _arraySep);
      if(partial==false) {
        result = false;
      }
    }
    update(); // Should this be called by the user explicitly?
    return result;
  }

  // --------------------------------------------------------
  // Input /Output
  // --------------------------------------------------------
  static private java.util.Hashtable<String, Object> memory = new java.util.Hashtable<String, Object>();

  /**
   * Saves the state of the model either to a file on the disk or to memory.
   * If the name of the file starts with the prefix "ejs:", then the
   * state of the model will be saved to memory, otherwise it will be
   * dumped to disk.
   * Security considerations apply when running the simulation as
   * an applet.
   * <p>
   * The state of the model is saved by writing to disk all its public
   * fields which implement the java.io.Serializable interface. This
   * includes primitives and arrays.
   * @param _filename the name of a file (either in disk or in memory)
   * @return true if the file was correctly saved
   */
  public boolean saveState(String _filename) {
    if(model==null) {
      return false;
    }
    try {
      java.io.OutputStream out;
      if(_filename.startsWith("ejs:")) {                   //$NON-NLS-1$
        out = new java.io.ByteArrayOutputStream();
      } else {
        out = new java.io.FileOutputStream(_filename);
      }
      java.io.BufferedOutputStream bout = new java.io.BufferedOutputStream(out);
      java.io.ObjectOutputStream dout = new java.io.ObjectOutputStream(bout);
      java.lang.reflect.Field[] fields = model.getClass().getFields();
      for(int i = 0; i<fields.length; i++) {
        if(fields[i].get(model) instanceof java.io.Serializable) {
          dout.writeObject(fields[i].get(model));
        }
      }
      dout.close();
      if(_filename.startsWith("ejs:")) {                   //$NON-NLS-1$
        memory.put(_filename, ((java.io.ByteArrayOutputStream) out).toByteArray());
      }
      return true;
    } catch(java.lang.Exception ioe) {
      errorMessage("Error when trying to save"+_filename); //$NON-NLS-1$
      ioe.printStackTrace(System.err);
      return false;
    }
  }

  public boolean readState(String _filename) {
    return readState(_filename, null);
  }

  /**
   * Reads the state of the model either from a file on the disk, from memory
   * or from a url location.
   * If the name of the file starts with the prefix "ejs:", then the
   * state of the model will be read from a memory file that must have been
   * created previously with the corresponding call to saveState().
   * If the name of the file starts with "url:" it will be considered
   * a url location and the method will attempt to read the file from this
   * url (either locally or through the network).
   * This file must have been previously created with a call to saveState()
   * with destination a disk file, and then this file must have been
   * copied at the right url location.
   * dumped to disk.
   * If the name of the file does not start with any of those prefixes,
   * then it will be considered to be a file.
   * Security considerations apply when running the simulation as
   * an applet.
   * <p>
   * The state of the model is read by reading from disk all its public
   * fields which implement the java.io.Serializable interface. This
   * includes primitives and arrays.
   * @param _filename the name of a file (either in disk , in memory or a url)
   * @return true if the file was correctly read
   */
  public boolean readState(String _filename, java.net.URL _codebase) {
    if(model==null) {
      return false;
    }
    try {
      java.io.InputStream in;
      // System.out.println ("filename = "+_filename);
      // System.out.println ("codebase = "+_codebase);
      if(_filename.startsWith("ejs:")) {                    //$NON-NLS-1$
        in = new java.io.ByteArrayInputStream((byte[]) memory.get(_filename));
      } else if(_filename.startsWith("url:")) {             //$NON-NLS-1$
        String url = _filename.substring(4);
        // System.out.println ("url = "+url);
        // System.out.println ("codebase = "+_codebase);
        if((_codebase==null)||url.startsWith("http:")) {    //$NON-NLS-1$
          // Do nothing
        } else {
          url = _codebase+url;
        }
        // System.out.println ("URL = "+url.toString());
        in = (new java.net.URL(url)).openStream();
      } else {
        in = new java.io.FileInputStream(_filename);
      }
      java.io.BufferedInputStream bin = new java.io.BufferedInputStream(in);
      java.io.ObjectInputStream din = new java.io.ObjectInputStream(bin);
      java.lang.reflect.Field[] fields = model.getClass().getFields();
      for(int i = 0; i<fields.length; i++) {
        if(fields[i].get(model) instanceof java.io.Serializable) {
          fields[i].set(model, din.readObject());
        }
      }
      din.close();
      if(view!=null) {
        view.initialize();
      }
      update();
      return true;
    } catch(java.lang.Exception ioe) {
      errorMessage("Error when trying to read "+_filename); //$NON-NLS-1$
      ioe.printStackTrace(System.err);
      return false;
    }
  }

} // End of class

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

 * Code that uses any portion of the code in the org.opensourcephysics package
 * or any subpackage (subdirectory) of this package must must also be be released
 * under the GNU GPL license.
 *
 * This software 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
 * or view the license online at http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2007  The Open Source Physics project
 *                     http://www.opensourcephysics.org
 */
