package nxm.sys.net;

import java.net.HttpURLConnection;
import java.util.Date;
import java.util.Hashtable;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.Convert;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.KeyObject;
import nxm.sys.lib.Midas;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.libg.GMenu;
import nxm.sys.libg.GPrompt;
import nxm.sys.libg.GValue;
import nxm.sys.libg.GWidget;

/** A class to return the contents of a file to a Browser.
    @author Jeff Schoen / Neon Ngo
*/
public class HFile extends HSource {

  private BaseFile bf;
  private static boolean debug = false; // enable to turn on debug messages

  /** HFile Constructor
   *  @param name the name of the file
   *  @param bf   the Midas file to use
   */
  public HFile (String name, BaseFile bf) {
    this.name = name;
    this.bf = bf;
    if (debug) Shell.writeln("HFile() name="+name+" bf="+bf);
  }

  /** Set if debug is enabled
   *  @param val true to enable debug
   *  @since NeXtMidas 2.7.1 to turn on debugging */
  public static void setDebug(boolean val) { debug = val;  }
  /** Is debug enabled
   *  @return true if debugging is enabled
   *  @since NeXtMidas 2.7.1 to see if debugging is enabled */
  public static boolean isDebug()          { return debug; }

  @Override
  public boolean canHandleRequest (String uri) {
    String rname = getName();
    return uri.equals(rname);
  }

  @Override
  public void handleRequest (String uri, HPage hp) {
    handle (bf, hp, null);
  }

  /** Handles an HTTP request
   *  @param bf   the Midas file to use
   *  @param hp   the HPage output socket
   *  @param args the arguments
   */
  public static void handle (BaseFile bf, HPage hp, String args) {
    if (debug) Shell.writeln("HFile.handle() "+hp.getMethod()+" bf.url="+bf.getURL());
    boolean isHeadRequest = hp.getMethod().equals("HEAD");
    bf.open(BaseFile.INPUT|BaseFile.NOABORT);
    if (!bf.isOpen) {
      hp.setStatus(HttpURLConnection.HTTP_NOT_FOUND);
      hp.open();
      if (!isHeadRequest) { hp.writeNotFound(); }
      hp.close();
      return;
    }
    int bytes=0, bufsize=32768;
    double tbytes = bf.getSize();
    double offset = hp.getHeaderD("Range"); // NOTE: we only support first-byte-pos, last-byte-pos is ignored
    String mime   = getContentType(bf.getFileName().getExt(),null); // quick MIME type lookup
    if (mime==null) mime = BaseFile.getMimeType(null,bf.getName()); // everything else
    long lastModified = bf.getResource().lastModified();
    if (offset>0) { // got Range (partial content) request
      bf.seek(offset);
      hp.setContentRange((long)offset, (long)(tbytes-1), (long)tbytes); // set Content-Range and status of 206 (Partial Content)
      tbytes -= offset;
    }
    // NOTE: all response header fields (must be set before call to HPage.open()
    if (lastModified>0) hp.setLastModified(new Date(lastModified)); // set last-modified if available
    hp.setContentType(mime);
    hp.setContentLength((long)tbytes);
    if (isHeadRequest) { // DO NOT return content back for HEAD request
      hp.open();         // <-- this sends HTTP response headers back to client
    }
    else if (jnlpContentType.equals(mime)) {
      bytes = (int)tbytes;
      byte[] buf = new byte[bytes];
      bf.read(buf,0,bytes);
      String sbuf = new String(buf);
      if (hp.server!=null) {
        // TODO: deprecate MYHOST, MYPORT for NMSSPP=ON && (^{HS.HOST}:^{HS.HOST} or ^{HS.HOSTPORT})
        sbuf = sbuf.replaceAll("MYHOST",   hp.server.getHost());
        sbuf = sbuf.replaceAll("MYPORT",""+hp.server.getPort());
        sbuf = hp.server.parent.MA.evaluateCarets(sbuf);
        buf  = sbuf.getBytes();
        hp.setContentLength(buf.length); // update content length (just in case replacement was done)
      }
      hp.open();
      hp.write(buf);
    }
    else if (tbytes<=bufsize) { // since 3.5.1: do NM Server Side PreProcessing for files <= 32K
      bytes = (int)tbytes;      // TODO: roll this logic with below else branch so that we can do this for all file sizes?
      byte[] buf = new byte[bytes];
      bytes = bf.read(buf,0,bytes);
      if (!binaryContentType.equals(mime) && !jarContentType.equals(mime)) {
        String str = new String(buf,0,Math.min(200,bytes));  // NMSSPP=ON MUST be in first 200 bytes of file (TODO: why not 512, 1000, or 1024?)
        if (hp.server!=null && str.indexOf("NMSSPP=ON")>0) {
          StringBuffer sb = new StringBuffer( new String(buf,0,bytes) );
          bytes = serverSidePreProcess(sb,hp,args);
          buf = sb.toString().getBytes();
          hp.setContentLength((long)bytes); // update content length (just in case replacement was done)
        }
      }
      hp.open();
      hp.write(buf,0,bytes);
    }
    else {
      byte[] buf = new byte[bufsize];
      hp.open();
      for (; tbytes>0; tbytes-=bytes) {
        bytes = (int)Math.min(tbytes,(double)bufsize);
        int bytesRead = bf.read(buf,0,bytes);
        if (bytesRead<0) break;
        if (hp.write(buf,0,bytesRead)<0) break;
      }
    }
    hp.close();
    bf.close();
  }

  private static int serverSidePreProcess (StringBuffer sb, HPage hp, String args) {
    int i,j,k,l,l1,l2,id=1;
    if (hp==null || hp.server==null) return sb.length();
    Midas M = hp.server.getMidas();
    while ( (i=sb.indexOf("<!-- FORALL")) >= 0) { // since 3.7.0 support FORALL and ENDFOR (BUG 2847)
      j = sb.indexOf("<!-- ENDFOR -->",i);
      k = sb.indexOf(">",i);
      String sx = serverSideSub(sb.substring(i+12,k-2),M);
      l = sx.indexOf(':');
      l1 = Convert.s2l(sx.substring(l-1,l));
      l2 = Convert.s2l(sx.substring(l+1));
      String s = sb.substring(k+1,j);
      String value = "";
      for (int n=l1; n<=l2; n++) value=value+StringUtil.replaceAll(s,"#",""+n);
      sb.replace(i,j+15,value);
    }
    while ( (i=sb.lastIndexOf("^{")) >= 0) {
      j = sb.indexOf("}",i);
      String key = sb.substring(i+2,j).toUpperCase();
      Object obj=null; String value="XXX";
      if (key.startsWith("HS.")) obj = KeyObject.getKey(hp.server,key.substring(3),null,M);
      else if (M != null)        obj = M.results.get(key);
      if (obj instanceof GWidget) {
        GWidget gw = (GWidget)obj;
        value = getControlWidget(gw,id,hp.server.getMidas()); id++;
//        value = getControlWidgetOrig(gw,args,hp.server.getMidas());
      }
      else if (obj!=null) value = obj.toString();
      sb.replace(i,j+1,value);
    }
    return sb.length();
  }
  private static String serverSideSub (String sb, Midas M) {
    int i,j;
    while ( (i=sb.lastIndexOf("^{")) >= 0) {
      j = sb.indexOf("}",i);
      String key = sb.substring(i+2,j).toUpperCase();
      Object obj=null; String value="XXX";
      if (M != null) value = M.results.getS(key);
      sb = sb.substring(0,i)+value+sb.substring(j+1);
    }
    return sb;
  }

  /** Analyzes a GWidget to be outputted as html text
   *  @param gw the GWidget to be analyzed
   *  @param id the id number
   *  @param M  the Midas context
   *  @return the html text representing the GWidget
   */
  @InternalUseOnly("Since added in NeXtMidas 3.7.0")
  public static String getControlWidget (GWidget gw, int id, Midas M) {
    String seed  = gw.getValueString();
    String tag   = "'he"+id+"'";
    String tagq  = "'he"+id+"q'";
    String value = "<a id="+tag+">"+seed+"</a>";
    boolean noedit = gw.getFlag(GWidget.NOEDIT);
    if (noedit) {
      value = "<span id="+tag+" onmouseover=gnoedit("+tag+")>"+seed+"</span>";
    }
    else if (gw instanceof GPrompt) {
      GPrompt gwp = (GPrompt)gw;
      String list = "&ltEnter&gt";
      if (gwp.isFileChooser()) {
        String raux = gwp.getAux();
        String auxList = M.results.getString("AUX.READ");
        if (auxList.indexOf(raux)<0) auxList = raux+"|"+auxList;
        list += ",&ltAux:"+auxList+"&gt";
        list += ",&lt--Aux="+raux+"--&gt";
        String ext = "tmp|prm";
        String root = gwp.getFilter();
        int ip = root.indexOf('.');
        if (ip>0) { ext=root.substring(ip+1); root=root.substring(0,ip); }
        String path = M.io.getAuxPath(raux);
        String uext = raux.equals("RAM")? "" : ext;
        String[] files = M.io.listFiles(path,root,uext);
        if (files!=null) for (int i=0; i<files.length; i++) list += ","+files[i];
      }
      value = "<span id="+tag+" onmouseover=gselect("+tag+",'"+gw.label+"','"+list+"','"+seed+"')>"+seed+"</span>"
             +"<span id="+tagq+"> </span>";
    }
    else if (gw instanceof GPrompt) {
      GPrompt gwp = (GPrompt)gw;
      value = "<span id="+tag+" onclick=gprompt("+tag+",'"+gw.label+"','"+seed+"')>"+seed+"</span>";
    }
    else if (gw instanceof GMenu) {
      GMenu gwm = (GMenu)gw;
      String input = gwm.getFlag(GMenu.INPUT)? "&ltEnter&gt," : "";
      value = "<span id="+tag+" onmouseover=gselect("+tag+",'"+gw.label+"','"+input+gwm.getItems()+"','"+seed+"')>"+seed+"</span>";
    }
    else if (gw instanceof GValue) {
      GValue gwv = (GValue)gw;
      value = "<span id="+tag+" onmouseover=gvalue("+tag+",'"+gw.label+"','"+seed+"')>"+seed+"</span>";
    }
    else if (gw instanceof GValue) {
      GValue gwv = (GValue)gw;
      value = "<span id="+tag+" onclick=gselect("+tag+",'"+gw.label+"','"+seed+"')>"+seed+"</span>";
      value = "<span id="+tag+" onclick=gprompt("+tag+",'"+gw.label+"','"+seed+"')>"+seed+"</span>";
      value = "<span id="+tag+" onclick=gvalue("+tag+",'"+gw.label+"','"+seed+"')>"+seed+"</span>";
    }
    return value;
  }

  /** Analyzes a GWidget to be outputted as html text
   *  @param gw the GWidget to be analyzed
   *  @param args the arguments
   *  @param M  the Midas context
   *  @return the html text representing the GWidget
   */
  @InternalUseOnly("Since added in NeXtMidas 3.7.0")
  public static String getControlWidgetOrig (GWidget gw, String args, Midas M) {
    boolean expand = args!=null && args.equals(gw.label) && !gw.getFlag(GWidget.NOEDIT);
    String value = null;
        if (!expand) {
          String seed = gw.getValueString();
          if (seed==null || seed.length()==0) seed = "[]";
          value = "<a href=\"?"+gw.label+"\">"+seed+"</a>";
        }
        else if (gw instanceof GPrompt) {
          GPrompt gwp = (GPrompt)gw;
          String seed = gw.getValueString();
          if (seed==null || seed.length()==0) seed = "[]";
          if (gwp.isFileChooser()) {
            value = "[<a id=\""+gw.label+"\" onClick=\"gcprompt('"+gw.label+"','"+seed+"')\">Enter:</a>,<b>"+seed+"</b>,Auxes";
            String auxList = M.results.getString("AUX.READ");
            Parser auxes = new Parser(auxList,'|');
            for (auxes.reset(); auxes.more(); ) {
              String aux = auxes.next();
              String path = M.io.getAuxPath(aux);
              value += ",<a href='filesPage.html?Aux"+aux+"' target='filesFrame'>"+aux+"</a>";
            }
            value += "]";
          } else {
            value = "["+seed+"] <a id=\""+gw.label+"\" onClick=\"gcprompt('"+gw.label+"','"+seed+"')\">Enter</a> "
                  + "<a href=\"?"+gw.label+"="+seed+">Default</a>";
          }
        }
        else {
          value = gw.toString();
          value = HQuery.lookForChoices(gw.label,value,false);
          int lb = value.indexOf("[");
          int rb = value.lastIndexOf("]");
          if (lb>=0 && rb>lb) value = value.substring(lb,rb+1);
        }
        if (expand) value = "<a href=\"?\">&lt</a>"+value;
    return value;
  }

  /** Handles HTTP requests within a pipe
   *  @param bf the Midas file to be used
   *  @param hp the HPage output socket
   */
  public static void handlePipe (DataFile bf,  HPage hp) {
    bf.open(BaseFile.INPUT|BaseFile.NOABORT);
    boolean isHeadRequest = hp.getMethod().equals("HEAD");
    if (!bf.isOpen) {
      hp.setStatus(HttpURLConnection.HTTP_NOT_FOUND);
      hp.open();
      if (!isHeadRequest) { hp.writeNotFound(); }
      hp.close();
      return;
    }
    int bytes=0, bufsize=1024;
    hp.setContentType(binaryContentType);
    hp.setContentLength((long)(100*1024*1024));
    hp.open();
    if (!isHeadRequest) {
      bf.setDataStart(512.0);         // fake out HttpResource
      bf.setDataSize(1024.0*1024.0);  // fake out HttpResource
      hp.write(bf.hb,0,512);          // write header
      byte[] buf = new byte[bufsize];
      for (;;) {
        bytes = bufsize;
        int bytesRead = bf.read(buf,0,bytes);
        if (bytesRead<0) break;
        if (hp.write(buf,0,bytesRead)<0) break;
      }
    }
    hp.close();
    bf.close();
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Deprecated methods
  //////////////////////////////////////////////////////////////////////////////////////////////////
  private static final String htmlContentType = "text/html";
  private static final String textContentType = "text/plain";
  private static final String binaryContentType = "application/octet-stream";
  private static final String jarContentType    = "application/x-java-archive";
  private static final String jnlpContentType   = "application/x-java-jnlp-file";
  /** made private in 3.5.1, was deprecated in 2.7.0: Use {@link BaseFile#getMimeType()} */
  private static Hashtable<String,String> types;

  /** provide quick lookup for common MIME types. Use {@link BaseFile#getMimeType()} for full lookup.
      @param ext filename extension to lookup MIME type (cannot be null)
      @param def default value to return if no found in quick lookup table
      @return MIME type for specified extension (if found), otherwise returns default value
      @since 3.5.1
   */
  @InternalUseOnly public static String getContentType (String ext, String def) {
    ext = ext.toLowerCase();
    if (types==null) {
      Hashtable<String,String> typesMap = new Hashtable<>();
      typesMap.put("xhtml",htmlContentType);
      typesMap.put("html",htmlContentType);
      typesMap.put("htm" ,htmlContentType);
      typesMap.put("mm"  ,textContentType);
      typesMap.put("key" ,textContentType);
      typesMap.put("txt" ,textContentType);
      typesMap.put("java",textContentType);
      typesMap.put("c",   textContentType);
      typesMap.put("h",   textContentType);
      typesMap.put("tmp" ,binaryContentType);
      typesMap.put("prm" ,binaryContentType);
      typesMap.put("pkt" ,binaryContentType);
      typesMap.put("det" ,binaryContentType);

      // Extensions associated with Java Web Start
      // Specified under "Servlet Configuration" -> "Configuring file extensions and MIME types"
      // in "Packaging JNLP Applications in a Web Archive / Java TM Web Start 1.4.2"
      // http://java.sun.com/javase/6/docs/technotes/guides/javaws/downloadservletguide.html
      typesMap.put("jnlp",    jnlpContentType);
      typesMap.put("jar",     jarContentType);
      typesMap.put("jardiff", "application/x-java-archive-diff");
      types = typesMap;
    }
    String type = types.get(ext);
    if (type==null) type = def;
    return type;
  }

}
