package nxm.sys.lib;

import java.io.FilenameFilter;

/**
  Resource for interacting with a NeXtMidas RamFile. <br>
  NOTE: The max RamFile size is limited to a signed 32-bit integer (2^31 - 1 =~ 2 GiB) in Java.

  @author Jeff Schoen / Neon Ngo
*/
public class RamResource extends IOResource {

  /** name of Ram Resource */
  public String name;
  /** NeXtMidas Ram File to be used */
  public RamFile ramfile = new RamFile();

  /** Default Constructor */
  public RamResource () { }

  @Override
  public boolean isFile () { return true; } //  since 2.8.0: fix for Bug 2129 (Ram Resource is a File)

  /** @since NeXtMidas 3.1.2 */
  @Override
  public void setURL(String urlstr) {
    super.setURL(urlstr);
    name = urlstr.substring(4); // strip off "ram:"
  }

  /** test existence of the resource */
  public boolean exists () {
    // name = urlstr.substring(4); // strip off "ram:" (Since 3.1.2 this was moved into overridden setURL(String) method)
    return M.pipes.containsKey(name);
  }

  /** open the resource */
  public boolean open () {
    isOpen = false;
    if (dir > DIR_INOUT) { // open for write
      RamFile prevRamfile = (RamFile) M.pipes.get(name);
      if (prevRamfile != null) { // since 2.9.1: if have existing RamFile use it, matches FileResource behavior (BUG 2335)
        prevRamfile.setLength(ramfile.length);
        ramfile = prevRamfile;
      }
      else {
        M.pipes.put(name, ramfile);
      }
    }
    else if (dir <= DIR_INOUT) { // open for read and write
      ramfile = (RamFile) M.pipes.get(name);
    }
    isOpen = (ramfile != null);

    return isOpen;
  }

  /** close the resource */
  public boolean close () { return true; }

  /** delete the resource */
  public boolean delete () { M.pipes.remove(name); return true; }

  /** get length of resource */
  public long getLength () { return (long)ramfile.length; }

  /** set length of resource */
  public void setLength (long bytes) { ramfile.setLength(bytes); }

  /** seek to the specified offset in file */
  public boolean seek (long offset) { this.offset = offset; return true; }

  /** read data from resource and returns the number of bytes read (or -1 for EOF). */
  public int read (byte[] buf, int boff, int bytes) {
    // The below has been updated to ensure that reads do not go beyond
    // RamFile's 2 GiB limit for BUG 2585 (since 3.1.2)
    long bytesToRead = Math.min(bytes, (long)ramfile.length - offset);
    if (bytesToRead <= 0) return -1; // make sure not to read beyond RamFile buffer
    int status = (int) bytesToRead;  // at this point bytesToRead is <= RamFile.MAX_SIZE
    System.arraycopy(ramfile.buffer,(int)offset, buf,boff, status);
    offset += status;
    return status;
  }

  /** write data to resource and returns the number of bytes written. */
  public int write (byte[] buf, int boff, int bytes) {
    // Since 3.1.2: RamFile.setLength throws a meaningful exception, which guarantees
    // (BUG 2585)   that offset will be in valid range [0 - RamFile.MAX_SIZE]
    if (offset+bytes > ramfile.length) ramfile.setLength(offset+bytes);
    System.arraycopy(buf,boff, ramfile.buffer,(int)offset, bytes);
    offset += bytes;
    ramfile.lastModified = Time.current(); // 3.3.0
    return bytes;
  }

  /** get List of container entries */
  public String[] getEntryList (FilenameFilter filter) {
    return filterEntries(filter, M.pipes.getKeys());
  }

  /** get entry from Container */
  public IOResource getEntry (String name) { return null; }

  /** @since NeXtMidas 3.3.0 */
  @Override
  public Time lastModifiedTime () {
    return new Time(ramfile.lastModified);
  }

  /** @since NeXtMidas 3.3.0 */
  @Override
  public long lastModified () {
    return (long) Time.toJ1970(ramfile.lastModified); // convert J1950 to J1970
  }

  /** Class to hold a file in memory (RamFile) as a byte array (i.e. byte[]). */
  public static class RamFile {
    /** Maximum buffer (size) of RamFile is <code>(2^31 - 1) =~ 2 GiB = {@value}</code> */
    public static final int MAX_SIZE       = Integer.MAX_VALUE;
    /** Maximum "initial" buffer (size) of RamFile. 16 MiB. */
    public static final int MAX_INITIAL    = 16*1048576;
    /** Maximum increment of RamFile buffer when more memory is needed. 4 MiB. */
    private static final int MAX_INCREMENT =  4*1048576;
    /** current length of file (in bytes), could be smaller than actual buffer size */
    public int length=0;
    /** internal memory buffer */
    public byte[] buffer;
    /** memory buffer increment size for RamFile (initially 1 KiB) */
    private int increment = 1024;
    /** last modified time (J1950) (defaults to time this was created) */
    double lastModified = Time.current();

    /** default constructor */
    public RamFile () {}

    /** Set the length (size) of this RamFile. This is limited to 2 GiB.
        @param bytes length (size) of this RamFile
        @throws MidasException if trying to set size pass 2 GiB limit, or to a negative value.
     */
    public void setLength (long bytes) {
      // since 3.1.2: added the following two checks, since RamFile is limited
      //              to max size of 2 GiB and cannot be negative (BUG 2585)
      if (bytes > RamFile.MAX_SIZE) throw new MidasException("Cannot set RamFile size larger then 2 GiB limit ["+bytes+"]");
      if (bytes < 0) throw new MidasException("Cannot set RamFile size to negative ["+bytes+"]");

      byte[] oldbuffer = buffer;
      int    oldlength = length;
      length = (int) bytes;
      if (oldbuffer == null) {                // don't have an old buffer, should alloc new memory
        bytes = Math.min(MAX_INITIAL, bytes); // cap initial memory alloc to MAX_INITIAL
        length = (int) bytes;                 // need to re-adjust RamFile's length
      }
      else if (length <= oldbuffer.length) {  // if requested # bytes is smaller or equal to current,
        return;                               // not reallocating (reducing) memory blocks
      }

      int newBufSize = (int) Math.min(MAX_SIZE, bytes + increment); // since 3.1.2: prevents newBufSize from becoming negative (BUG 2585)
      buffer = new byte[newBufSize];
      if (increment < MAX_INCREMENT) {                       // in 2.9.0: double "next" memory increment size (BUG 2181)
        increment = (int) Math.min(MAX_INCREMENT, bytes*2L); // in 2.9.0: BUT cap "next" increment to MAX_INCREMENT (BUG 2131)
      }
      if (oldbuffer != null) {
        System.arraycopy(oldbuffer,0, buffer,0, Math.min(oldlength,length));
      }
    }

    /** Get last modified time
     *  @return Last modified time of this RamFile in seconds since J1950.
      * @since NeXtMidas 3.3.0
     */
    public double getLastModified () {
      return lastModified;
    }

  } // end class RamFile

} // end class RamResource
