package nxm.sys.lib;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;

import nxm.sys.lib.CoreIO.IOOptions;

/**
  Resource for interacting with a local File.

  <table border="1">
    <caption style="visibility:hidden">""</caption>
    <tr>
      <th colspan="2">Special File Qualifiers</th>
    </tr>
    <tr>
      <th>Qualifier</th>
      <th>Description</th>
    </tr>
    <tr>
      <td>MKDIR</td>
      <td>Make parent directories for this file</td>
    </tr>
    <tr>
      <td>DELETEONEXIT</td>
      <td>Requests that the file be deleted when the JVM exits normally after the file is opened
          successfully. See {@link File#deleteOnExit()} for details. Once deletion has been
          requested, it is not possible to cancel the request.
          Since NeXtMidas 3.1.1.</td>
    </tr>
  </table>
  @author Jeff Schoen
*/
public class FileResource extends IOResource {

  /** The File object representing the abstract pathname to the local file
      being wrapped by this resource.  */
  protected File file;

  private RandomAccessFile raf;

  /** default constructor */
  public FileResource() { super(); }

  /** return if this resource is a container of other resources */
  @Override
  public boolean isContainer() { return file.isDirectory(); }

  /** Test for the existence of the resource. */
  @Override
  public boolean exists () {
    boolean retVal = false;

    try {
      retVal = file.exists();
    } catch(java.lang.SecurityException e) {
      // This exception was common when running via an applet (applet support has been removed).
      retVal = false;
    }

    return retVal;
  }

  /** Set the URL that this resource points to.  If the string begins with
      'file:' or another prefix, only the data after the prefix is used as the pathname.
   */
  @Override
  public void setURL (String str) {
    super.setURL(str);
    String localFileName = FileUtil.toLocalFileName(str);
    if (localFileName != null) { // if able to get local file name from URL string
      file = new File(localFileName);
    }
    else { // otherwise, just use URL string as specified (since 3.5.3 for Bug 2876 (and fixes original problem in BUG 2171)
      file = new File(str);
    }
  }

  /** Return the full URL String for this object. */
  @Override
  public String getURL () {
    int i = (urlstr!=null)? urlstr.indexOf(":/") : -1;
    if (i>0) {       // Bug 1823: ignore if already have <scheme>:<absolute_path_segment>
      return urlstr; //           in urlstr, e.g. "ramd:/" for ramd: protocol (scheme)
    }
    else if (file!=null) {   // Fix for Bug 1679 for Windows Platform
      try { return file.toURI().toURL().toString(); }
      catch (Exception e) {} // ignore and continue original logic below
    }
    if (urlstr==null) return "file:<null url>";
    if (i<0) i = urlstr.indexOf(":\\"); // is windows path? e.g. "c:\"
    if (i<0) return "file:"+urlstr;
    return urlstr;
  }

  /** This will return a new FileInputStream to the this file. User's must call close()
      on returned InputStream otherwise resource may not be cleanup. User still
      must call close() on this IOResource to cleanup any resources held by this object.
      @return null on error, otherwise a new InputStream to read this file (e.g. new open file descriptor).
    */
  @Override
  public InputStream getInputStream () {
    FileInputStream fis = null;
    try { fis = new FileInputStream(file); }
    catch (IOException e) { M.error(e); }
    return fis;
  }

  /** This will return a new FileOutputStream to the this file. User's must call close()
      on returned OutputStream otherwise resource may not be cleanup. User still
      must call close() on this IOResource to cleanup any resources held by this object.
      @return null on error, otherwise a new OutputStream to read this file (e.g. new open file descriptor).
   */
  @Override
  public OutputStream getOutputStream () {
    FileOutputStream fis = null;
    try { fis = new FileOutputStream(file); }
    catch (IOException e) { M.error(e); }
    return fis;
  }

  /** open the resource */
  public boolean open () {
    isOpen = false;
    if ((dir > DIR_INOUT) && getStateQualifier("MKDIR") && !mkdirs())
      M.error("Could not create parent directories for "+this);
    try {
      if (dir >= DIR_INOUT) raf = new RandomAccessFile (file,"rw");
      else                  raf = new RandomAccessFile (file,"r");
      if (dir == DIR_OUTPUT_TRUNC) {
        long newLength = (length<0)? 0 : length; // NTN: set to internal length when > 0
        raf.setLength(newLength); // Text File overwrite
      }
      isOpen = true;

      if (getStateQualifier("DELETEONEXIT")) { // since 3.1.1 for BUG 2538
        file.deleteOnExit();
      }
    }
    catch (Exception e) { }
    return isOpen;
  }

  /** close the resource */
  @Override
  public boolean close () {
    if (!isOpen) return false;
    try {
      if (raf!=null) { raf.close(); }
      isOpen = false;
    }
    catch (IOException e) { return false; }
    return true;
  }

  /** delete the resource */
  @Override
  public boolean delete () { return file.delete(); }

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

  /** Seek to the specified offset in file (measured from the beginning) at
      which the next read or write occurs. Setting the offset beyond the
      end of the file (EOF) does not change the file length. The file length will
      change only by writing after the offset has been set beyond EOF.
      @param offset offset position, measured in bytes from the beginning of the
                    file, at which to set the file pointer. This should NOT be negative.
      @return true if successful, otherwise false.
   */
  @Override
  public boolean seek (long offset) {
    if (offset==this.offset) return true;
    try { raf.seek(offset); }
    catch (IOException e) { return false; }
    this.offset = offset;
    return true;
  }

  /** get currently available length (in bytes) of resource */
  @Override
  public long avail () { return file.length()-offset; }

  /** read data from resource */
  @Override
  public int read (byte[] buf, int boff, int bytes) {
    int status=0, off=boff;
    while (bytes>0) {
      try { status = raf.read(buf,off,bytes); }
      catch (IOException e) {
        if (M.io.isOptionSet(IOOptions.FileIONoAbort)){
          status = -1;
        }
        else {
          // Throw MidasIOException unless IOOption is set revert to just returning status of -1 (since 3.7.1 for NXM-2758)
          throw new MidasIOException("FileResource: Error Reading File!",e);
        }
      }
      if (status>0) { bytes-=status; off+=status; }
      else break;
    } bytes = off-boff;
    if (status<0 && bytes==0) return status;
    offset += bytes;
    return bytes;
  }

  /** write data to resource */
  @Override
  public int write (byte[] buf, int boff, int bytes) {
    try { raf.write(buf,boff,bytes); }
    catch (IOException e) {
      if (M.io.isOptionSet(IOOptions.FileIONoAbort)){
        return -1;
      }
      else {
         // Throw MidasIOException unless IOOption is set revert to just returning bytes written value of -1 (since 3.7.1 for NXM-2758)
        throw new MidasIOException("FileResource: Error Writing File!",e);
      }
    }
    offset += bytes;
    return bytes;
  }

  /**
    Get the list of entries that are in the currently set directory that
    pass the given filter.  If the filter is null then all of the entries
    in the directory are accepted.

    @param filter The filter that a filename must pass to be included in this list.
    @return A String array containing the names of the entries in the
              directory or null if the current URL is not a directory.
   */
  @Override
  public String[] getEntryList (FilenameFilter filter) { return file.list(filter); }

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

  /** Return a printable string containing information about this object. */
  @Override
  public String toString () { return "FileResource : " + getURL(); }

  /** make parent directories for this file
   *  @return true if parent file is directory */
  public boolean mkdirs () {
    File dir = file.getParentFile();
    if (!dir.exists()) return dir.mkdirs();
    else if (dir.isDirectory()) return true;
    else return false;
  }

  @Override
  public Table getStatFS () {
    String path = urlstr;
    if (path==null) return null;
    if (path.startsWith("file:")) path = path.substring(5);
    if (path.indexOf(':')>0) return null;
    long[] data = new long[10];
    int stat = Native.statfs (path,data);
    if (stat<0) { data[0]=data[1]=data[2]=1024; data[3]=0; }
    int tmby = (int)(data[0]*data[1]/1000000);       // total Mbyte
    int fmby = (int)(data[0]*data[2]/1000000);       // free  Mbyte
    int files = (int)data[3];                        // number of files
    int used = (int)(100-100*data[2]/Math.max(1,data[1])); // used percentage
    Table stats = new Table();
    stats.put("PATH",  path);
    stats.put("FILES", files);
    stats.put("TMBY",  tmby);
    stats.put("FMBY",  fmby);
    stats.put("USED",  used);
    return stats;
  }

  @Override
  public long lastModified () {
    return (file==null)? 0 : file.lastModified();
  }
}
