package javajs.util;

import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;

import javajs.J2SIgnoreImport;
import javajs.api.BytePoster;
import javajs.api.GenericOutputChannel;
import javajs.api.js.J2SObjectInterface;

/**
 * 
 * A generic output method. JmolOutputChannel can be used to:
 * 
 * add characters to a StringBuffer using fileName==null, append() and
 * toString()
 * 
 * add bytes utilizing ByteArrayOutputStream using writeBytes(),
 * writeByteAsInt(), append()*, and bytesAsArray() *append() can be used as long
 * as os==ByteArrayOutputStream or it is not used before one of the writeByte
 * methods.
 * 
 * output characters to a FileOutputStream using os==FileOutputStream,
 * asWriter==true, append(), and closeChannel()
 * 
 * output bytes to a FileOutputStream using os==FileOutputStream, writeBytes(),
 * writeByteAsInt(), append(), and closeChannel()
 * 
 * post characters or bytes to a remote server using fileName=="http://..." or
 * "https://...", writeBytes(), writeByteAsInt(), append(), and closeChannel()
 * 
 * send characters or bytes to a JavaScript function when JavaScript and (typeof
 * fileName == "function")
 * 
 * if fileName equals ";base64,", then the data are base64-encoded prior to
 * writing, and closeChannel() returns the data.
 * 
 * @author hansonr Bob Hanson hansonr@stolaf.edu 9/2013
 * 
 * 
 */

@J2SIgnoreImport({ java.io.FileOutputStream.class })
public class OC extends OutputStream implements GenericOutputChannel {

  private BytePoster bytePoster; // only necessary for writing to http:// or https://
  private String fileName;
  private BufferedWriter bw;
  private boolean isLocalFile;
  private int byteCount;
  private boolean isCanceled;
  private boolean closed;
  private OutputStream os;
  private SB sb;
  private String type;
  private boolean isBase64;
  private OutputStream os0;
  private byte[] bytes; // preset bytes; output only

  public boolean bigEndian = true;

  public OC() {
    // from reflection, requires setParams
  }

  public OC(String fileName) {
    setParams(null, fileName, false, null);
  }

  /**
   * Set up an output channel. String or byte data can be added without problem.
   * 
	 * @param bytePoster a byte poster can take the output byte[] when closing and
	 *                   do something with them
	 * @param fileName   TODO: It is possible that JavaScript will call this with a
	 *                   function name for fileName
	 * @param asWriter   string-based
	 * @param os         the desired target OutputStream - not the calling stream!
	 * @return OC
	 */
	public OC setParams(BytePoster bytePoster, String fileName, boolean asWriter, OutputStream os) {
    this.bytePoster = bytePoster;
    isBase64 = ";base64,".equals(fileName);
    if (isBase64) {
      fileName = null;
      os0 = os;
      os = null;
    }
    this.fileName = fileName;
    this.os = os;
    isLocalFile = (fileName != null && !isRemote(fileName));
    if (asWriter && !isBase64 && os != null)
      bw = new BufferedWriter(new OutputStreamWriter(os));
    return this;
  }

  @Override
  public boolean isBigEndian() {
    return bigEndian;
  }

  public void setBigEndian(boolean TF) {
    bigEndian = TF;
  }

  public OC setBytes(byte[] b) {
    bytes = b;
    return this;
  }

  public String getFileName() {
    return fileName;
  }

  public String getName() {
    return (fileName == null ? null
        : fileName.substring(fileName.lastIndexOf("/") + 1));
  }

  public int getByteCount() {
    return byteCount;
  }

  /**
   * 
   * @param type
   *        user-identified type (PNG, JPG, etc)
   */
  public void setType(String type) {
    this.type = type;
  }

  public String getType() {
    return type;
  }

  /**
   * will go to string buffer if bw == null and os == null
   * 
   * @param s
   * @return this, for chaining like a standard StringBuffer
   * 
   */
  public OC append(String s) {
    try {
      if (bw != null) {
        bw.write(s);
      } else if (os == null) {
        if (sb == null)
          sb = new SB();
        sb.append(s);
      } else {
        byte[] b = s.getBytes();
        os.write(b, 0, b.length);
        byteCount += b.length;
        return this;
      }
    } catch (IOException e) {
      // ignore
    }
    byteCount += s.length(); // not necessarily exactly correct if unicode
    return this;
  }

  @Override
  public void reset() {
    sb = null;
    initOS();
  }

  private void initOS() {
    if (sb != null) {
      String s = sb.toString();
      reset();
      append(s);
      return;
    }
    try {
      /**
       * @j2sNative
       * 
       *            this.os = null;
       */
      {
        if (os instanceof FileOutputStream) {
          os.close();
          os = new FileOutputStream(fileName);
        } else {
          os = null;
        }
      }
      if (os == null)
        os = new ByteArrayOutputStream();
      if (bw != null) {
        bw.close();
        bw = new BufferedWriter(new OutputStreamWriter(os));
      }
    } catch (Exception e) {
      // not perfect here.
      System.out.println(e.toString());
    }
    byteCount = 0;
  }

  /**
   * @param b
   */
  @Override
  public void writeByteAsInt(int b) {
    if (os == null)
      initOS();
    /**
     * @j2sNative
     * 
     *            this.os.writeByteAsInt(b);
     * 
     */
    {
      try {
        os.write(b);
      } catch (IOException e) {
      }
    }
    byteCount++;
  }

  /**
   * @j2sOverride
   */
  @Override
  public void write(byte[] buf, int i, int len) {
    if (os == null)
      initOS();
    if (len < 0)
      len = buf.length - i;
    try {
      os.write(buf, i, len);
    } catch (IOException e) {
    }
    byteCount += len;
  }

  @Override
  public void writeShort(short i) {
    if (isBigEndian()) {
      writeByteAsInt(i >> 8);
      writeByteAsInt(i);
    } else {
      writeByteAsInt(i);
      writeByteAsInt(i >> 8);
    }
  }

  @Override
  public void writeLong(long b) {
    if (isBigEndian()) {
      writeInt((int) ((b >> 32) & 0xFFFFFFFFl));
      writeInt((int) (b & 0xFFFFFFFFl));
    } else {
      writeByteAsInt((int) (b >> 56));
      writeByteAsInt((int) (b >> 48));
      writeByteAsInt((int) (b >> 40));
      writeByteAsInt((int) (b >> 32));
      writeByteAsInt((int) (b >> 24));
      writeByteAsInt((int) (b >> 16));
      writeByteAsInt((int) (b >> 8));
      writeByteAsInt((int) b);
    }
  }

  /**
   * Will break JavaScript if used.
   * 
   * @j2sIgnore
   * 
   * @param b
   */
  @Override
  @Deprecated
  public void write(int b) {
    // required by standard ZipOutputStream -- do not use, as it will break JavaScript methods
    if (os == null)
      initOS();
    try {
      os.write(b);
    } catch (IOException e) {
    }
    byteCount++;
  }

  // not in JSmol's OutputStream class, so not overriding  
  //  /**
  //   * Will break if used; no equivalent in JavaScript.
  //   * 
  //   * @j2sIgnore
  //   * 
  //   * @param b
  //   */
  //  @Override
  //  @Deprecated
  //  public void write(byte[] b) {
  //    // not used in JavaScript due to overloading problem there
  //    write(b, 0, b.length);
  //  }

  public void cancel() {
    isCanceled = true;
    closeChannel();
  }

  @Override
  @SuppressWarnings({ "null", "unused" })
  public String closeChannel() {
    if (closed)
      return null;
    // can't cancel file writers
    try {
      if (bw != null) {
        bw.flush();
        bw.close();
      } else if (os != null) {
        os.flush();
        os.close();
      }
      if (os0 != null && isCanceled) {
        os0.flush();
        os0.close();
      }
    } catch (Exception e) {
      // ignore closing issues
    }
    if (isCanceled) {
      closed = true;
      return null;
    }
    if (fileName == null) {
      if (isBase64) {
        String s = getBase64();
        if (os0 != null) {
          os = os0;
          append(s);
        }
        sb = new SB();
        sb.append(s);
        isBase64 = false;
        return closeChannel();
      }
      return (sb == null ? null : sb.toString());
    }
    closed = true;
    if (!isLocalFile) {
      String ret = postByteArray(); // unsigned applet could do this
      if (ret == null || ret.startsWith("java.net"))
        byteCount = -1;
      return ret;
    }
    J2SObjectInterface jmol = null;
    Object _function = null;
    /**
     * @j2sNative
     * 
     *            jmol = self.J2S || Jmol; _function = (typeof this.fileName ==
     *            "function" ? this.fileName : null);
     * 
     */
    {
    }
    if (jmol != null) {
      Object data = (sb == null ? toByteArray() : sb.toString());
      if (_function == null)
        jmol.doAjax(fileName, null, data, sb == null);
      else
        jmol.applyFunc(fileName, data);
    }
    return null;
  }

  public boolean isBase64() {
    return isBase64;
  }

  public String getBase64() {
    return Base64.getBase64(toByteArray()).toString();
  }

  public byte[] toByteArray() {
    return (bytes != null ? bytes
        : os instanceof ByteArrayOutputStream
            ? ((ByteArrayOutputStream) os).toByteArray()
            : null);
  }

  @Override
  @Deprecated
  public void close() {
    closeChannel();
  }

  @Override
  public String toString() {
    if (bw != null)
      try {
        bw.flush();
      } catch (IOException e) {
        // TODO
      }
    if (sb != null)
      return closeChannel();
    return byteCount + " bytes";
  }

  private String postByteArray() {
    byte[] bytes = (sb == null ? toByteArray() : sb.toString().getBytes());
    return bytePoster.postByteArray(fileName, bytes);
  }

  public final static String[] urlPrefixes = { "http:", "https:", "sftp:",
      "ftp:", "file:", "cache:" };
  // note that SFTP is not supported
  public final static int URL_LOCAL = 4, URL_CACHE = 5;

  public static boolean isRemote(String fileName) {
    if (fileName == null)
      return false;
    int itype = urlTypeIndex(fileName);
    return (itype >= 0 && itype < URL_LOCAL);
  }

  public static boolean isLocal(String fileName) {
    return (fileName != null && !isRemote(fileName));
  }

  public static int urlTypeIndex(String name) {
    if (name == null)
      return -2; // local unsigned applet
    for (int i = 0; i < urlPrefixes.length; ++i) {
      if (name.startsWith(urlPrefixes[i])) {
        return i;
      }
    }
    return -1;
  }

  @Override
  public void writeInt(int i) {
    if (bigEndian) {
      writeByteAsInt(i >> 24);
      writeByteAsInt(i >> 16);
      writeByteAsInt(i >> 8);
      writeByteAsInt(i);
    } else {
      writeByteAsInt(i);
      writeByteAsInt(i >> 8);
      writeByteAsInt(i >> 16);
      writeByteAsInt(i >> 24);
    }
  }

  public void writeFloat(float x) {
    writeInt(x == 0 ? 0 : Float.floatToIntBits(x));
  }

}
