package nxm.sys.lib;

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.net.URLConnection;

/** Resource for interacting with entries in a uniform resource locator (URL).
    If the registered URLConnection implementation for URL supports setting the read and connect
    timeouts, then the TIMEOUT and CONNECTTIMEOUT qualifiers can be use to set them (in seconds).

    NOTE: most URLConnections DO NOT support output (write access).
    @author  Neon Ngo
    @since NeXtMidas 3.3.0
 */
public class UrlResource extends IOResource {
  /** URL reference */
  private URL url;
  /** URL Connection */
  private URLConnection urlConn;
  /** Current input stream from URLConnection (for reading from)*/
  private InputStream in;
  /** Current output stream from URLConnection (for writing to) */
  private OutputStream out;

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

  /** Return a string containing a description of this object. */
  public String toString () {
    return "UrlResource: " + getURL();
  }

  /** Sets the fully qualified URL of the resource.
      @param urlstr The fully qualified URL of the resource.
   */
  @Override
  public void setURL (String urlstr) {
    try {
      this.url = (new URI(urlstr)).toURL();
      urlstr   = this.url.toExternalForm(); // normalizes URL String
    } catch (MalformedURLException | URISyntaxException e) {
      throw new MidasException("UrlResource: Error accessing URL="+urlstr, e);
    }
    super.setURL(urlstr);
  }

  @Override
  public boolean exists () {
    if (url == null) return false;
    // if (urlConn != null) return true; // previously opened (so it should exist)? TODO: do optimization?
    try {
      return url.openConnection() != null; // if able to open connection to URL, then it exists
    }
    catch (IOException ioe) {
      // return false since we can't open a connection to URL at this moment
      return false;
    }
  }

  /** Opens the connection to the URL and sets the headers on the request.
      @param url  The URL to attempt to connect to.
      @param dir  The I/O mode (direction) to open the connection in.
      @return The URLConnection object resulting from opening the connection.
   */
  protected URLConnection openConnection (URL url, int dir) throws IOException {
    URLConnection conn;
    double timeout        = 0.0;
    double connectTimeout = 0.0;

    if (qualTable != null) { // check qualifiers for this IOResource
      timeout        = qualTable.getD("TIMEOUT", 0.0); // read timeout
      connectTimeout = qualTable.getD("CONNECTTIMEOUT", timeout);
    }

    // Go ahead and open the connection.  Note that this does not
    // actually establish the connection, it just creates the objects.
    conn = url.openConnection();

    if (timeout > 0.0) {
      int rtimeout = (int)(timeout * 1000);       // convert seconds to ms
      conn.setReadTimeout(rtimeout);
      if (rtimeout != conn.getReadTimeout()) {    // some platforms may ignore the timeout settings
        M.warning("This platform is unable to set timeout for "+url+" to "+rtimeout+" ms using "+conn.getReadTimeout()+" ms.");
      }
    }
    if (connectTimeout > 0.0) {
      int ctimeout = (int)(connectTimeout * 1000); // convert seconds to ms
      conn.setConnectTimeout(ctimeout);
      if (ctimeout != conn.getConnectTimeout()) {  // some platforms may ignore the timeout settings
        M.warning("This platform is unable to set timeout for "+url+" to "+ctimeout+" ms using "+conn.getConnectTimeout()+" ms.");
      }
    }

    if (dir <= DIR_INOUT) { // Input
      conn.setDoInput(true);
    }
    if (dir >= DIR_INOUT) { // Output
      conn.setDoOutput(true);
    }
    return conn;
  }

  @Override
  public boolean open () {
    isOpen = false;
    try {
      urlConn = openConnection(url, dir);

      if (dir <= DIR_INOUT) { // Input
        in = urlConn.getInputStream();
      }
      if (dir >= DIR_INOUT) { // Output
        out = urlConn.getOutputStream(); // NOTE: most URLConnections DO NOT support output/write access
      }

      isOpen = true;

      if (offset != 0) {
        seek(0L,offset);
      }
    } catch (Exception e) {
      // Same as FileResuorce - Ignore the exception and return isOpen (false)
    }
    return isOpen;
  }

  @Override
  public boolean close () {
    boolean stat = true;

    stat   &= closeIO(in);
    stat   &= closeIO(out);
    in      = null;
    out     = null;
    urlConn = null;
    offset  = 0;
    isOpen  = false;

    return stat;
  }

  public long getLength () {
    return urlConn.getContentLength();
  }

  /** Seek ahead in the input stream.
      @param offset Current offset.
      @param off    New offset (must be greater than offset).
   */
  private void seek (long offset, long off) {
    try {
      if (in != null) { in.skip(off-offset); }
      offset = off;
    } catch (IOException e) {
      throw new MidasException("Unable to seek to "+off+" URL="+url,e);
    }
  }

  @Override
  public boolean seek (long off) {
    if (isOpen && off!=offset) {
      // URL Streams are not random-access. Need to close and then reopen.
      if (off > offset) {
        seek(offset,off);
      } else {
        close();
        offset = off;
        open();
      }
    }
    return true;
  }

  @Override
  public long avail () {
    if (in == null) return 0L;

    try {
      return in.available();
    } catch (IOException e) {
      return 0L;
    }
  }

  @Override
  public int read (byte[] buf, int boff, int bytes) {
    if (in == null) {
      return -1; // cannot read if we don't have input stream
    }
    int status=0, off=boff;
    while (bytes>0) {
      try {
        status = in.read(buf,off,bytes);
      } catch (IOException e) {
        status = -1;
      }
      if (status>0) { bytes-=status; off+=status; }
      else break;
    } bytes = off-boff;
    if (status<0 && bytes==0) return status;
    offset += bytes;
    return bytes;
  }

  @Override
  public int write (byte[] buf, int boff, int bytes) {
    if (out == null) {
      return -1; // cannot write if we don't have output stream
    }
    try {
      out.write(buf, boff, bytes);
    }
    catch (IOException e) {
      return -1;
    }
    offset += bytes;
    return bytes;
  }

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