package nxm.sys.lib;

import java.io.Closeable;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;

import nxm.sys.inc.IOResourceFactory;

/** The base class for all objects that provide data to NextMidas. Supports reading and writing to
    and from most data sources including Disk files, HTTP connections, Sockets, JAR files, etc. <br>
    <br>
    <h2>Direction Values</h2>
    When opening a resource, it requires that an I/O direction be specified. Values &lt;=0 are for
    input (i.e. reading a file), values &gt;=0 are for output (i.e. writing to a file). This
    notation permits I/O access to the file using a value of 0. <br>
    <br>
    By default only {@link #DIR_INPUT} (-1), {@link #DIR_OUTPUT} (1) and {@link #DIR_INOUT} (0) are
    used for BLUE files (see {@link DataFile}. Since 2.5.0, all other file types use the
    <code>_TRUNC</code> versions for input and output. By default, the normal {@link #DIR_INPUT} and
    {@link #DIR_OUTPUT} options allow the different Resource types (e.g. {@link FileResource}) to
    open a file for overwriting. <br>
    <br>
    When overwriting a file, the existing content is NOT cleared out, but rather the new content
    is just written over the old content. This is done for efficiency reasons where applicable (it
    prevents extra reallocation of disk resources at the I/O level for those Resource types that
    allow it). This works fine with BLUE files (see {@link DataFile}) since their size is recorded
    in the header and any extra bytes on disk are ignored when reading the file (this is as per the
    BLUE file specifications). <br>
    <br>
    The overwrite option is the default for BLUE files since this matches legacy behavior (this
    behavior pre-dates X-Midas and X-Midas/NeXtMidas maintain this behavior for compatibility
    reasons). File types that do not wish to use this feature should use {@link #DIR_OUTPUT_TRUNC}
    to indicate that all file resources should truncate the file to the length actually written.
    The {@link #DIR_INPUT_TRUNC} option is provided as a parallel to {@link #DIR_OUTPUT_TRUNC}
    although it is functionally identical to {@link #DIR_INPUT}. Undefined values (&lt;-2 and &gt;2)
    are reserved for future use and should not be used at this time.
    <pre>
      &lt;-2 - input             ^         (reserved for future use, no mnemonics)
       -2 - {@link #DIR_INPUT_TRUNC}   | INPUT
       -1 - {@link #DIR_INPUT}         |
        0 - {@link #DIR_INOUT}   ------+ INOUT
        1 - {@link #DIR_OUTPUT}        |
        2 - {@link #DIR_OUTPUT_TRUNC}  | OUTPUT
       &gt;2 - output            v         (reserved for future use, no mnemonics)
    </pre>
    @author  Jeff Schoen
 */
public class IOResource {

  /** The resource type */
  protected int   type;
  /** The Midas context in use */
  protected Midas M;
  /** The I/O direction IN|OUT|BOTH. See {@link #DIR_INPUT}, {@link #DIR_INOUT}, {@link #DIR_OUTPUT}, etc. constants */
  protected int   dir;
  /** The current seek pointer */
  protected long  offset=0;
  /** The total length in bytes */
  protected long  length=-1;

  /** The status of the resource indicating if it is open or closed. */
  public boolean isOpen;

  /** Resource type indicating a resource of unknown type (see {@link #typeList}). */
  public static final int UNKNOWN=0;
  /** Resource type indicating a file resource (see {@link #typeList}, {@link FileResource}). */
  public static final int FILE=1;
  /** Resource type indicating a piped resource (see {@link #typeList}, {@link PipeResource}). */
  public static final int PIPE=2;
  /** Resource type indicating a socket resource (see {@link #typeList}, {@link SocketResource}). */
  public static final int SOCK=3;
  /** Resource type indicating a UDP resource (see {@link #typeList}, {@link UdpResource}). */
  public static final int UDP=4;
  /** Resource type indicating a HTTP resource (see {@link #typeList}, {@link HttpResource}). */
  public static final int HTTP=5;
  /** Resource type indicating a JAR resource (see {@link #typeList}, {@link JarResource}). */
  public static final int JAR=6;
  /** Resource type indicating a RAM resource (see {@link #typeList}, {@link RamResource}). */
  public static final int RAM=7;
  /** Resource type indicating a native file resource of unknown type (see {@link #typeList}, {@link NFileResource}). */
  public static final int NFILE=8;
  /** Resource type indicating a RAM-disk resource (see {@link #typeList}). */
  public static final int RAMD=9;
  /** Resource type indicating a Midas-FTP resource (see {@link #typeList}, {@link MftpResource}). */
  public static final int MFTP=10;
  /** Resource type indicating a FTP resource. */
  public static final int FTP=11;  // @since NeXtMidas 2.9.0
  /** Resource type indicating a generic URL resource (see {@link UrlResource}).*/
  public static final int URL=12;  // @since NeXtMidas 3.3.0
  /** Resource type indicating a user-configured resource. */
  public static final int USER=13; // @since NeXtMidas 2.1.0

  /** Comma-separated list of resource types {@value}. */
  public static final String typeList
    = "Unknown,File,Pipe,Socket,Udp,Http,Jar,Ram,NFile,RamDisk,MFtp,Ftp,Url,User";

  /** List of I/O directions. This list is private since it does not start at 0, see dirToString(..). */
  private static final String dirList = "InputTrunc,Input,InOut,Output,OutputTrunc";

  /** I/O Direction: Read ({@value}). <i>(Functionally identical to {@link #DIR_INPUT}, used as a
      parallel for {@link #DIR_OUTPUT_TRUNC}.)</i>
      See "Direction Values" at the top of {@link IOResource} for details.
      @since NeXtMidas 2.5.0
   */
  public static final int DIR_INPUT_TRUNC = -2;

  /** I/O Direction: Input ({@value}).
      See "Direction Values" at the top of {@link IOResource} for details.
      @since NeXtMidas 2.5.0
   */
  public static final int DIR_INPUT = -1;

  /** I/O Direction: Input+Output ({@value}).
      See "Direction Values" at the top of {@link IOResource} for details.
      @since NeXtMidas 2.5.0
   */
  public static final int DIR_INOUT  = 0;

  /** I/O Direction: Output ({@value}). <i>(Output file, write on top of existing file.)</i>
      See "Direction Values" at the top of {@link IOResource} for details.
      @since NeXtMidas 2.5.0
   */
  public static final int DIR_OUTPUT = 1;

  /** I/O Direction: Write ({@value}). <i>(Output file, write new file.)</i>
      See "Direction Values" at the top of {@link IOResource} for details.
      @since NeXtMidas 2.5.0
   */
  public static final int DIR_OUTPUT_TRUNC = 2;

  /** Zero-length arrays are immutable. See Effective Java, Item 27 for more info. */
  static final String[] EMPTY_STRING_ARRAY = new String[0]; // since 2.9.1

  /** The full URL to the source of data for this instance. */
  protected String urlstr;

  /** The table of qualifiers. */
  protected Table qualTable;

  /** Gets the integer enumeration corresponding to the type of the given URL.
      @param urlstr The URL string for the resource to get the type of.
      @return The integer enumeration corresponding to the type of the given
              URL. (This will be one of items in {@link #typeList}.)
  */
  public static int getType (String urlstr) {
    int type;
    urlstr = urlstr.toLowerCase();
    // JC4: Prior to 2.5.0 JAR was handled with the following line...
    //    else if (urlstr.startsWith("jar:") || urlstr.indexOf(".jar")>0) type = JAR;
    // This can cause an infinite loop in the case where we are looking for the JAR file
    // self rather than a file in the JAR. This is a rare condition (more often seen via
    // WebStart) that occurs when looking for "<name>.jar" without a path or full URL
    // specified.

         if (urlstr.startsWith("_"    ))  type = PIPE;
    else if (urlstr.startsWith("file:"))  type = FILE;
    else if (urlstr.startsWith("jar:" ))  type = JAR;
    else if (urlstr.startsWith("http:"))  type = HTTP;
    else if (urlstr.startsWith("https:")) type = HTTP; // BUG 2378 (to allow WebStart client from HTTPS web server)
    else if (urlstr.startsWith("udp:" ))  type = UDP;
    else if (urlstr.startsWith("stp:" ))  type = SOCK;
    else if (urlstr.startsWith("ftp:" ))  type = FTP;
    else if (urlstr.startsWith("ram:_"))  type = PIPE;
    else if (urlstr.startsWith("ram:" ))  type = RAM;
    else if (urlstr.startsWith("ramd:"))  type = RAMD;
    else if (urlstr.startsWith("ice:"))   type = RAMD;
    else if (urlstr.startsWith("mftp:"))  type = MFTP;
    else if (isUserResource(urlstr))      type = USER;
    else if (isURLResource(urlstr))       type = URL;  // since 3.3.0
    else                                  type = FILE;

    return type;
  }

  /** Factory method to return a new {@link IOResource} of the required type that is initialized
   *  with the supplied parameters.
   *  @param M      The current Midas context.
   *  @param type   The type of resource. (This will be one of items in {@link #typeList}.)
   *  @param urlstr The URL string for the resource to instantiate.
   *  @param dir    One of: {@link #DIR_INPUT}, {@link #DIR_INOUT}, {@link #DIR_OUTPUT}, etc.
   *  @param quals  The qualifier table to attempt to look up a file in.
   *  @return A new IOResource that is appropriate for the specified resource type.
   */
  public static IOResource getInstance (Midas M, int type, String urlstr, int dir, Table quals) {
    IOResource res = null;
    String rg; // Resource group (Jeff's addition since 3.9.0)

    if (type==UNKNOWN) type = getType(urlstr);
    if (type==RAMD && !urlstr.endsWith(".det") && dir!=DIR_INOUT) type = FILE;
    if (type==FILE && quals!=null && quals.getState("USENATIVE")) type = NFILE;
    if (quals!=null && (rg=quals.getString("RG"))!=null) { // Jeff's addition since 3.9.0
      res = (IOResource)Registry.getHandler("RESOURCE."+rg);
    }
    else {
      switch (type) {
        case FILE:  res = new FileResource();   break;
        case PIPE:  res = new PipeResource();   break;
        case SOCK:  res = new SocketResource(); break;
        case FTP:   res = new FtpResource();    break;
        case UDP:   res = new UdpResource();    break;
        case HTTP:  res = new HttpResource();   break;
        case JAR:   res = new JarResource();    break;
        case RAM:   res = new RamResource();    break;
        case NFILE: res = new NFileResource();  break;
        case MFTP:  res = new MftpResource();   break;
        case URL:   res = new UrlResource();    break;
        case RAMD:  // is also a User Resource
        case USER:  return initUserInstance(M, type, urlstr, dir, quals);
        default:    break;
      }
    }

    if (res != null) res.init(M, type, urlstr, dir, quals);
    return res;
  }

  /** Factory method to return a new {@link IOResource} of the required type that is initialized
   *  with the supplied parameters.
   *  @param M      The current Midas context.
   *  @param type   The type of resource. (This will be one of items in {@link #typeList}.)
   *  @param urlstr The URL string for the resource to instantiate.
   *  @param dir    One of: {@link #DIR_INPUT}, {@link #DIR_INOUT}, {@link #DIR_OUTPUT}, etc.
   *  @param quals  The qualifier table to attempt to look up a file in.
   *  @return {@code res}
   *  @since NeXtMidas 3.7.2
   */
  public final IOResource init (Midas M, int type, String urlstr, int dir, Table quals) {
    setContext(M);
    setDir(dir);
    setType(type);
    setURL(urlstr);
    setQualifiers(quals);
    return this;
  }

  /** Creates a new instance. */
  public IOResource () {
  }

  /** Gets the current Midas context.
      @return The Midas context in use.
  */
  public Midas getContext () { return M; }

  /** Sets the current Midas context.
      @param M The Midas context to use.
  */
  public void setContext (Midas M) { this.M = M; }

  /** Gets the resource type.
      @return The resource type (see {@link #typeList}).
      @see #getTypeString()
  */
  public int getType () { return type; }

  /** Sets the resource type.
      @param type The resource type (this should be one of the values in {@link #typeList}).
  */
  public void setType (int type) { this.type = type; }

  /** Gets the resource type.
      @return The resource type as a string (see {@link #typeList}).
  */
  public String getTypeString () { return Parser.get(typeList, getType(), -1); }

  /** Gets the resource type.
      @param midas The midas context to use.
      @param url   A given URL string (cannot be null).
      @return The resource type as a string (see {@link #typeList}).
  */
  public static String getTypeString (Midas midas, String url) {
    int    type = getType(url);
    String str  = Parser.get(typeList, type, -1);

    if (type == USER) {
      String name = url.substring(0, url.indexOf(':')).toUpperCase();
      str = Registry.getHandlerName("RESOURCE."+name);
      int index = str.lastIndexOf('.');
      if (index > 0)                { str = str.substring(index+1); }
      if (str.endsWith("Resource")) { str = str.substring(0,str.length()-8); }
    }

    return str;
  }

  /** Gets the fully qualified URL of the resource.
      @return The fully qualified URL of the resource.
  */
  public String getURL () {
    return urlstr;
  }

  /** Sets the fully qualified URL of the resource.
      @param urlstr The fully qualified URL of the resource.
  */
  public void setURL (String urlstr) { this.urlstr=urlstr; }

  /** Resolve the URL for an archive. When possible fix invalid names. This assumes that names will
      roughly follow the standards given in {@link java.net.JarURLConnection}.
      @param prefix The required prefix (e.g. "jar:").
      @param ext    The typical file extension (e.g. ".jar").
      @param roots  The legal roots in the archive in preference order (e.g. JarResource uses
                    <code>{ "!/" }</code>, TarResource uses <code>{ "!", "!/" }</code>).
      @param urlstr The url string provided by the user.
   */
  protected void setURL (String prefix, String ext, String[] roots, String urlstr) { // package-private
    // This is used by used by JarResource and TarResource. In the future we may think of
    // having an abstract class ArchiveResource and moving this method into there. For now
    // this is package-private so that it will be easy to move in the future.
    String  name = getClass().getName();
    String  tmp  = urlstr;
    boolean ok   = false;

    // FIX THE PATH
    for (int i = 0; !ok && (i < roots.length); i++) {
      ok = urlstr.indexOf(roots[i]) > 0;
    }
    if (!ok && (ext != null) && urlstr.endsWith(ext)) { // Fix case of using top level of archive
      urlstr = urlstr + roots[0];
      if (roots.length != 1) {
        M.warning(name+": Missing file root specifier at end of "+tmp+", should be one of '"
                + StringUtil.join(roots,"', '")+"'.");
      }
    }
    if (!ok) { // Fix case of "!" but missing "<root>" after "!"
      int index = urlstr.indexOf('!');
      if (index > 0) {
        urlstr = urlstr.substring(0,index) + roots[0] + urlstr.substring(index+1);
        ok     = true;
        M.warning(name+": Missing file root specifier in "+tmp+", found '!' but expected one of '"
                + StringUtil.join(roots,"', '")+"'.");
      }
    }
    if (!ok && (ext != null)) { // Fix case of "<root>" but missing "!" before "<root>"
      int    index = -1;
      String root  = null;
      for (int i = 0; (index == -1) && (i < roots.length); i++) {
        if (roots[i].length() > 1) {
          root  = roots[i].substring(1);
          index = urlstr.indexOf(ext+root);
        }
      }
      if (index > 0) {
        index  = index + ext.length();
        urlstr = urlstr.substring(0,index) + "!" + urlstr.substring(index);
        ok     = true;
        M.warning(name+": Missing file root specifier in "+tmp+", found '"+root+"' but expected one of '"
                + StringUtil.join(roots,"', '")+"'.");
      }
    }
    if (!ok && (ext != null)) { // Fix case of missing "!<root>"
      int index = urlstr.indexOf(ext);
      if (index > 0) {
        index  = index + ext.length();
        urlstr = urlstr.substring(0,index) + roots[0] + urlstr.substring(index);
        ok     = true;
        M.warning(name+": Missing file root specifier in "+tmp+", found '"+ext+"' but expected one of '"
                + StringUtil.join(roots,"', '")+"' immediately after "+ext+".");
      }
    }
    if (!ok) { // Can not fix it.
      throw new MidasException("Illegal archive specification, cound not find file/archive "
                             + "specifier in "+tmp+".");
    }

    // FIX THE PREFIX
    int    index    = urlstr.indexOf('!');
    String filename = urlstr.substring(index);   // "!<filename>"
    String archname = urlstr.substring(0,index); // path to the archive

    if (archname.startsWith(prefix)) { // this *should* always be true
      archname = archname.substring(prefix.length());
    }

    // RESOLVE ARCHIVE LOCATION
    IOResource archio = M.io.findResource(archname, IOResource.DIR_INPUT);

    //FIX THE URL (workaround for Bug 83)
    //this.urlstr = prefix + archio.getURL() + filename;
    String archioURL = archio.getURL();
    if (!archioURL.startsWith("file:") && FileUtil.isLocalFileName(archioURL)) {
      archioURL = "file:" + archioURL; // Add required prefix.
    }
    archioURL = archioURL.replace('\\', '/'); // Fix Windows slashes

    this.urlstr = prefix + archioURL + filename;
  }

  /** Get an input stream to the current URL.
      @return An input stream to the current URL.
      @throws MalformedURLException If the current url is not valid.
      @throws IOException           If there is an error opening the stream to the URL.
      @see URL#openStream()
  */
  public InputStream getUrlStream () throws MalformedURLException, IOException {
    URL url = null;
    try {
      url = (new URI(getURL())).toURL();
    }
    catch (URISyntaxException e) {
      throw new MalformedURLException("Forming URL via URI failed:"+e.getMessage()+" "+e.getStackTrace());
    }
    return url.openStream();
  }

  /** Set the qualifiers that are to be used by this instance. This is a
      short term fix that is in place until the MidasURL (MURL) class is
      created.  The default implementation is to do nothing but subclasses can
      override this.
      @param table The new qualifier table.
      @see #getStateQualifier(String)
  */
  public void setQualifiers (Table table) {
    qualTable = table;
  }

  /** Gets a named qualifier out of the qualifier table.
      @param key The qualifier's key.
      @return The value of the specified qualifier.
      @see #getStateQualifier(String)
  */
  public Object getQualifier (String key) {
    if (qualTable==null) return null;
    else return qualTable.get(key);
  }

  /** Gets a named state qualifier out of the qualifier table.
      @param key The state qualifier's key.
      @return The value of the specified state qualifier.
      @see #getQualifier(String)
  */
  public boolean getStateQualifier (String key) {
    Object obj = getQualifier(key);
    if (obj==null) return false;
    return Convert.o2bool(obj,M);
  }

  /** Converts an I/O direction to a string.
      See "Direction Values" at the top of {@link IOResource} for details.
      @param dir The I/O direction.
      @return A string describing the I/O direction.
   */
  public static String dirToString (int dir) {
    String str = Parser.get(dirList, dir, -3);

    if (str.length() == 0) {
      str = (dir < DIR_INOUT)? "UnknownInput" : "UnknownOutput";
    }
    return str;
  }

  /** Gets the I/O direction.
      See "Direction Values" at the top of {@link IOResource} for details.
      @return The I/O direction (see {@link #setDir(int)} for details).
      @see #dirToString(int)
  */
  public int getDir () { return dir; }

  /** Sets the I/O direction.
      See "Direction Values" at the top of {@link IOResource} for details.
      @param dir The I/O direction.
  */
  public void setDir (int dir) { this.dir = dir; }

  /** Return the current seek offset.
      @return Current seek offset in bytes (0..N-1).
   */
  public long seek() { return offset; }

  /** Set the current seek offset
      @param offset Current seek offset in bytes (0..N-1).
      @return true on success, false on failure.
   */
  public boolean seek (long offset) { return (offset==this.offset); }

  /** Indicates if this resource is a container of other resources.
   *  @return true if it is, false otherwise.
   */
  public boolean isContainer () { return false; }

  /** Indicates if this resource is a stream or static resource.
   *  @return true if it is, false otherwise.
   */
  public boolean isStream () { return false; }

  /** Indicates if this resource is a stream or static resource.
   *  @return true if it is, false otherwise.
   */
  public boolean isStreaming () { return false; }

  /** Indicates if this resource is a Midas data pipe. Prior to NeXtMidas 2.1.0 the only NeXtMidas
   *  pipes were recognized (usually using <code>(getType() == PIPE)</code>). This method makes it
   *  possible to implement other types of pipes (e.g. X-Midas pipes).
   *  @return true if it is, false otherwise.
   *  @since NeXtMidas 2.1.0
   */
  public boolean isPipe () { return (type==PIPE); }

  /** Indicates if this resource is a traditional file. This method was added in NeXtMidas 2.1.0 to
   *  go along with {@link #isPipe()} (previously <code>(getType() == FILE)</code> was used).
   *  @return true if it is, false otherwise.
   *  @since NeXtMidas 2.1.0
   */
  public boolean isFile() { return (type==FILE) || (type==NFILE); }

  /** Indicates if this resource is a JAR file
   *  @return true if it is, false otherwise.
   *  @since NeXtMidas 4.1.4
   */
  public boolean isJarFile() { return (type==JAR);}

  /** Tests the existence of the resource.
   *  @return true if it exists, false otherwise.
   */
  public boolean exists () { return false; }

  /** Opens the resource.
   *  @return true if it is successful, false otherwise.
   */
  public boolean open () { return false; }

  /** Closes the resource.
   *  @return true if it is successful, false otherwise.
   */
  public boolean close () { return false; }

  /** Deletes the resource.
   *  @return true if it is successful, false otherwise.
   */
  public boolean delete () { return false; }

  /** Gets length of resource in bytes.
   *  @return The length of the resource in bytes.
   */
  public long getLength () { return 0; }

  /** Set length of resource in bytes.
   *  @param val The length in bytes.
   */
  public void setLength (long val) { this.length = val; }

  /** Gets the number of currently-available bytes of the resource.
   *  @return The number of bytes currently available for reading.
   */
  public long avail () { return getLength()-offset; }

  /** Check to see if the specified number of bytes are available for reading.
      @param numBytes The number of bytes to check for reading.
      @return true if the number of bytes specified are available for reading or if the resource
              ends/restarts before the specified number of bytes will be available; false if
              not near the end of the resource and the specified bytes are not available yet.
      @throws MidasException if the number of bytes specified exceeds the size of the pipe.
   */
  public boolean isReady (int numBytes) {
    if (!isStream()        ) return true; // static resource (always available or EOF)
    if (avail() >= numBytes) return true; // available

    long remainingBytes = getLength()-seek(); // bytes until EOF
    if (remainingBytes < numBytes) {
      return (avail() == remainingBytes); // all remaining bytes ready for reading
    }
    return false;
  }

  /** get last modified time of this resource.
      Time is in milliseconds to match {@link java.io.File#lastModified()}.
      @return milliseconds since UTC (J1970), 0 if resource does not exist or
              if an I/O error occurs or it is not available for this resource.
      @see java.io.File#lastModified()
      @since NeXtMidas 2.7.1
   */
  public long lastModified () { return 0; }

  /** get last modified time of this resource as a Midas Time object.
      @return Time object, if an I/O occurs or if time it not available for this
              resource, the returned Time will be zero (e.g. Time.isZero() will be true)
      @since NeXtMidas 3.3.0
   */
  public Time lastModifiedTime () {
    Time time = new Time();
    long utcLastModified = lastModified();
    if (utcLastModified != 0) {
      time.fromJ1970ms(utcLastModified);
    }
    return time;
  }

  /** Read data from resource.
      @param buf   The byte buffer to read into.
      @param boff  The byte offset into the byte array to start writing at.
      @param bytes The number of bytes to try to read.
      @return The number of bytes read (may be less than <code>bytes</code> when at end-of-file
              or no more data is available), -1 at the end of the file (EOF),
              or {@value nxm.sys.lib.CoreIO#ERROR} ({@link CoreIO#ERROR}) on error.
  */
  public int read (byte[] buf, int boff, int bytes) { return -1; }

  /** Write data to resource.
      @param buf   The byte buffer to write from.
      @param boff  The byte offset into the byte array to start writing from.
      @param bytes The number of bytes to try to write.
      @return The number of bytes written or -1 if none could be written (possibly a read-only source).
  */
  public int write (byte[] buf, int boff, int bytes) { return -1; }

  /** Get List of container entries. (This is used by the <code>FILES</code> command.)
      @param filter A filter to select matching entries (null=AcceptAll).
      @return The entries that match the filter or null if this is not a container.
   */
  public String[] getEntryList (FilenameFilter filter) { return null; }

  /** Filters entries in an array based on a given filter.
      @param filter  The filter (if null, accept all).
      @param entries The file names to filter.
      @return The file names accepted by the filter.
   */
  protected String[] filterEntries (FilenameFilter filter, String[] entries) {
    if (filter == null) { return entries; }

    ArrayList<String> list = new ArrayList<>(entries.length);
    for (String name : entries) {
      if (filter.accept(null, name)) {
        list.add(name);
      }
    }
    return list.toArray(new String[list.size()]);
  }

  /** Gets a named entry from Container.
      @param name The name of the entry.
      @return The applicable IOResource or null if not found.
   */
  public IOResource getEntry (String name) { return null; }

  /** Open for read or write. This is the same as:
   *  <pre>
   *    setDir(dir);
   *    open();
   *  </pre>
   *  @param dir The I/O direction. See "Direction Values" at the top of {@link IOResource} for details.
   *  @return The results of calling {@link #open()}.
   */
  public final boolean open (int dir) {
    setDir(dir);
    return open();
  }

  /** Read into the byte array at boff from the specified file offset. This is the same as:
      <pre>
        seek(offset);
        read(buf,boff,bytes);
      </pre>
      @param buf    The buffer to read the data into.
      @param boff   The offset into <code>buf</code> to start at.
      @param bytes  The number of bytes to try reading.
      @param offset The file offset to read from.
      @return Number of bytes actually read in (may be less than <code>bytes</code> when at end-of-file
              or no more data is available) or a value &lt;0 if the end of the file has been
              reached or on error: -1 at the end of the file (EOF),
              or {@value nxm.sys.lib.CoreIO#ERROR} ({@link CoreIO#ERROR}) on error.
      @see #seek(long)
      @see #read(byte[], int, int)
   */
  public final int read (byte[] buf, int boff, int bytes, long offset) {
    seek(offset);
    return read(buf,boff,bytes);
  }

  /** Read into the native pointer at boff.
      @param lbuf   Native pointer to buffer.
      @param boff   The offset into <code>buf</code> to start at.
      @param bytes  The number of bytes to try reading.
      @return Number of bytes actually read in (may be less than <code>bytes</code> when at end-of-file
              or no more data is available) or a value &lt;0 if the end of the file has been
              reached or on error: -1 at the end of the file (EOF),
              or {@value nxm.sys.lib.CoreIO#ERROR} ({@link CoreIO#ERROR}) on error.
      @see #read(byte[], int, int)
   */
  public int read (long lbuf, int boff, int bytes) {
    byte[] buf = new byte[bytes];
    int stat = read(buf,0,bytes);
    Native.ja2p(buf,0,lbuf,boff,bytes);
    return stat;
  }

  /** Write from the byte array at boff to the specified file offset. This is the same as:
      <pre>
        seek(offset);
        write(buf,boff,bytes);
      </pre>
      @param buf    The buffer to write the data from.
      @param boff   The offset into <code>buf</code> to start at.
      @param bytes  The number of bytes to write out.
      @param offset The file offset to start writing at.
      @return Number of bytes actually written. (In practice this is almost always equal to
              <code>bytes</code>.)
   */
  public final int write (byte[] buf, int boff, int bytes, long offset) {
    seek(offset);
    return write(buf,boff,bytes);
  }

  /** Write from native pointer at boff.
      @param lbuf   Native pointer to buffer.
      @param boff   The offset into <code>buf</code> to start at.
      @param bytes  The number of bytes to write out.
      @return Number of bytes actually written. (In practice this is almost always equal to
              <code>bytes</code>.)
   */
  public int write (long lbuf, int boff, int bytes) {
    byte[] buf = new byte[bytes];
    Native.p2ja(lbuf,boff,buf,0,bytes);
    int stat = write(buf,0,bytes);
    return stat;
  }

  /** Reads a single byte. Used by getInputStream() */
  private int read () {
    byte[] buf = new byte[1];
    int count = read(buf, 0, buf.length);
    return (count < 0)? -1: buf[0];
  }

  /** Writes a single byte. Used by getOutputStream() */
  private void write (int b) {
    byte[] buf = new byte[] { (byte)b };
    write(buf, 0, buf.length);
  }

  /** Gets an InputStream for this resource.
      @return An InputStream for this resource or <code>null</code> if such can not be created.
  */
  public InputStream getInputStream () {
    final IOResource self = this;

    return new InputStream() {
      @Override public int  available()  { return (int)Math.min(Integer.MAX_VALUE, self.avail()); }
      @Override public int  read()                             { return self.read();              }
      @Override public int  read(byte[] buf, int off, int len) { return self.read(buf, off, len); }
      @Override public void close()                            { self.close();                    }
    };
  }

  /** Gets an OutputStream for this resource.
      @return An OutputStream for this resource or <code>null</code> if such can not be created.
  */
  public OutputStream getOutputStream () {
    final IOResource self = this;

    return new OutputStream() {
      @Override public void write(int b)                        { self.write(b);             }
      @Override public void write(byte[] buf, int off, int len) { self.write(buf, off, len); }
      @Override public void close()                             { self.close();              }
    };
  }

  /** Determines if a given resource uses one of the defined resource handlers.
   *  @param urlstr The URL (or file name) of the resource.
   *  @since NeXtMidas 2.1.0
   */
  private static boolean isUserResource (String urlstr) {
    return (getUserResourceHandler(urlstr) != null);
  }

  /** Factory method to return a new {@link IOResource} of the required type that is initialized
   *  with the supplied parameters.
   *  @param M      The current Midas context.
   *  @param type   This should always be {@link #USER}.
   *  @param urlstr The URL string for the resource to instantiate.
   *  @param dir    One of: {@link #DIR_INPUT}, {@link #DIR_INOUT}, {@link #DIR_OUTPUT}, etc.
   *  @param quals  The qualifier table to attempt to look up a file in.
   *  @return A new IOResource that is appropriate for the specified resource type.
   *  @since NeXtMidas 3.7.2 - From NeXtMidas 2.1.0 to NeXtMidas 3.7.1 this was
   *         {@code getUserResource(M,urlstr)} and did not initialize the instance. This was
   *         changed in NeXtMidas 3.7.2 with the introduction of {@link IOResourceFactory}.
   */
  private static IOResource initUserInstance (Midas M, int type, String urlstr, int dir, Table quals) {
    Object handler = getUserResourceHandler(urlstr);
    if (handler instanceof IOResourceFactory) {
      // This is new in NeXtMidas 3.7.2
      IOResource res = ((IOResourceFactory)handler).getResourceFor(M, type, urlstr, dir, quals);
      if (res != null) return res;

      // Not found on classpath, return a generic IOResource that will just return false when
      // exists() or open() is called.
      return new IOResource().init(M, type, urlstr, dir, quals);
    }
    else {
      return ((IOResource)handler).init(M, type, urlstr, dir, quals);
    }
  }

  /** Gets the user resource handler.
   *  @param urlstr The URL (or file name) of the resource.
   *  @return The handler or null if not found.
   *  @since NeXtMidas 3.7.2
   */
  private static Object getUserResourceHandler (String urlstr) {
    int i = urlstr.indexOf(':');
    if (i < 2) return null; // ignores Windows/DOS drive letters (e.g. C:\path\file.ext)
    String name = urlstr.substring(0,i).toUpperCase();

    if (name.equals("CLASSPATH")) {
      // Special case used during Shell initialization since this method can get called while Shell
      // is loading the /nxm/sys/cfg/handlers.tbl file.
      return ClassPathResource.INSTANCE;
    }
    return Registry.getHandler("RESOURCE."+name);
  }

  /** Is specified string a valid URL and can be used by URLResource?
      Note: This is slightly more rigid than RFC 3966 Uniform Resource Identifier (URI):
            Generic Syntax (Section 3.1 Scheme) in that it requires two chars before the colon
            (since Windows/DOS drive letters look like URL strings (e.g. "C:\path\file.ext")).
      @return true if valid URLResource URL String, otherwise false.
      @since NeXtMidas 3.3.0
   */
  static boolean isURLResource (String str) {
    if (str == null) return false;
    if (str.indexOf(':') > 1) { // MUST contain a ':' colon after at least two chars
      return Character.isLetter(str.charAt(0)); // first char MUST be a letter/alpha
    }
    return false;
  }

  /** Gets statistics of file system that this resource is located on.
      @return Table of file system statistics of where this resource is located
              if available, otherwise null.
      @since NeXtMidas 2.7.1
   */
  public Table getStatFS () {
    return null;
  }

  /** Close specified Closeable I/O stream object (if not null).
      This utility method is useful for calling in close() methods of sub-classes.
      @param closeable object to close (null will be ignored)
      @return true if there was no exceptions, false otherwise
      @since NeXtMidas 3.3.0
   */
  @SuppressWarnings({"BroadCatchBlock", "TooBroadCatch"})
  protected boolean closeIO (Closeable closeable) {
    try {
      if (closeable != null) {
        closeable.close();
      }
      return true;
    }
    catch (Exception e) {
      // Ignore the exception and return false
      return false;
    }
  }
}
