package nxm.sys.lib;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import nxm.sys.inc.ListFile;
import nxm.sys.inc.MidasReference;

/**
  A class to handle ZIP files.
*/
public class ZipFile extends BaseFile implements ListFile {
    /** FYI: zero-length arrays are immutable. For more info see Effective Java
      Item 27: Return zero-length arrays, not null. */
  private static final ZipEntry[] EMPTY_ZIPENTRY_ARRAY = new ZipEntry[0];

  ZipEntry[]            zipEntries = null; // The entries in the zip file
  java.util.zip.ZipFile zipFile    = null; // The zip file (null if not on local disk)

  /** ZipFile Default Constructor */
  public ZipFile () { }

  /** @param ref      The midas reference to use.
      @param filename The name of the file to use.
      @deprecated since 3.5.0 use {@link #ZipFile(MidasReference, Object)}
   */
  @Deprecated
  public ZipFile (Object ref, Object filename) {
    init(Convert.ref2Midas(ref), filename);
  }

  /** ZipFile Constructor
      @param ref      The midas reference to use.
      @param filename The name of the file to use.
      @since 3.5.0
   */
  public ZipFile (MidasReference ref, Object filename) {
    init(ref,filename);
  }

  @Override
  public String getMimeType () {
    return "application/zip";
  }

  /** @deprecated Since 3.5.0 use {@link #init(MidasReference, Object)} */
  @Deprecated
  public void init (Object ref, Object filename) {
    init(Convert.ref2Midas(ref), filename);
  }

  /** @since NeXtMidas 3.5.0 */
  public void init (MidasReference ref, Object filename) {
    super.init(ref, filename);
    zipEntries = null;
  }

  protected void setInternals () {
    super.setInternals();

    try {
      zipEntries = readZipFile();
    } catch (Exception e) {
      zipFile = null;
    }
    if (zipEntries == null) {
      try {
        zipEntries = readZipInputStream();
      } catch (Exception e) {
        if ((flags & NOABORT) == 0) {
          throw new MidasException("ZipFile: Unable to read from "+getName(),e);
        } else {
          M.printStackTrace("ZipFile: Unable to read from "+getName(),e);
        }
      }
    }
  }

  /** Reads a zip file. */
  ZipEntry[] readZipFile () throws IOException {
    String fname = FileUtil.toLocalFileName(getURL());
    return (fname == null)? null : readZipFile(new java.util.zip.ZipFile(fname));
  }

  /** Reads a zip file. */
  ZipEntry[] readZipInputStream () throws IOException {
    return readZipInputStream(new ZipInputStream(io.getInputStream()));
  }

  /** Reads a zip file. */
  ZipEntry[] readZipFile (java.util.zip.ZipFile zip) throws IOException {
    ArrayList<ZipEntry> entries = new ArrayList<>();

    zipFile = zip;
    Enumeration<? extends ZipEntry> e = zip.entries();
    while (e.hasMoreElements()) entries.add(e.nextElement());

    return entries.toArray(EMPTY_ZIPENTRY_ARRAY);
  }

  /** Reads a zip input stream. */
  ZipEntry[] readZipInputStream (ZipInputStream in) throws IOException {
    ArrayList<ZipEntry> entries = new ArrayList<>();
    ZipEntry entry;
    while ((entry = in.getNextEntry()) != null) entries.add(entry);
    in.close();
    return entries.toArray(EMPTY_ZIPENTRY_ARRAY);
  }

  /** Return the array of {@link #zipEntries}
      @since NeXtMidas 3.3.0
   */
  public Object[] getEntries () {
    return zipEntries;  // added for Bug 2574
  }

  /** short synopsis of file */
  public String toString() {
    String str = "ZipFile ";
    if (io!=null) str += io.getTypeString();
    str += " Resource";
    return str;
  }

  /** List the contents of the header */
  public String listHeader() {
    String list =
      "ZipFile   :  "+getURL()+"\n"
    + "Entries   :  "+getNumEntries()+"\n"
    + "Bytes     :  "+getSize()+"\n";
    return list;
  }

  /** Gets the number of entries in the file.
   *  @return number of entries in the file.
   */
  public int getNumEntries() { return zipEntries.length; }

  /** list a data element */
  public String listElements (double start, int elements, String format, int flags) {
    String str = null;

    if ((start >= 0) && (start < zipEntries.length)) {
      str = zipEntries[(int)start].getName()+"\n";
    }
    return str;
  }

  public int listElementsPerLine (int lineWidth, String form, int flags) {
    return 1;
  }

  /** Since 3.3.0, changes to getEntries() below */
  //@Deprecated public void getEntries() { } // JC4: This was used to count entries before I added getNumEntries()

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods to implement ListFile
  //////////////////////////////////////////////////////////////////////////////////////////////////
  void notEditable () { throw new UnsupportedOperationException("ZipFile is not editable."); }

  public double  getNumberOfRows ()                        { return getNumEntries(); }
  public boolean getProtected ()                           { return true;   } // not editable at this time
  public int     setData    (double d, Table t)            { notEditable(); return 0; }
  public void    setData    (double d, String s, Object o) { notEditable(); }
  public void    insertData (double d, Object o)           { notEditable(); }
  public void    removeData (double d)                     { notEditable(); }
  public void    removeData (double d, long c)             { notEditable(); }
  public void    setRecordDefs (Table t)                   { notEditable(); }

  public Table getDataTable (double offset) {
    if (offset < 0) return null;
    if (offset >= zipEntries.length) return null; // end-of-file

    Table    t = new Table();
    ZipEntry e = zipEntries[(int)offset];

    double time = Time.J1970TOJ1950 + (e.getTime() / 1000.0);

    String method;
    switch (e.getMethod()) {
      case ZipEntry.STORED:   method = "STORED";   break;
      case ZipEntry.DEFLATED: method = "DEFLATED"; break;
      default:                method = "UNKNOWN";  break;
    }

    String comment = e.getComment();
    if (comment == null) comment = "";

    t.put("NAME",        e.getName());
    t.put("COMMENT",     comment);
    t.put("TIME",        time);
    t.put("DIRECTORY",   e.isDirectory());
    t.put("COMP_SIZE",   e.getCompressedSize());
    t.put("COMP_METHOD", method);
    t.put("CRC",         e.getCrc());

    return t;
  }

  public int getRecordDefCount () {
    return 7;
  }

  public Table getRecordDef (int i) {
    Table t = new Table();
    switch (i) {
      case 0: t.put("NAME", "NAME");                                 break;
      case 1: t.put("NAME", "COMMENT");                              break;
      case 2: t.put("NAME", "TIME");         t.put("FORMAT", "SD");  break;
      case 3: t.put("NAME", "DIRECTORY");    t.put("FORMAT", "SZ");  break;
      case 4: t.put("NAME", "COMP_SIZE");    t.put("FORMAT", "SX");  break;
      case 5: t.put("NAME", "COMP_METHOD");                          break;
      case 6: t.put("NAME", "CRC");          t.put("FORMAT", "SX");  break;
      default: throw new ArrayIndexOutOfBoundsException("Invalid record definition i="+i);
    }
    return t;
  }

  public Table getRecordDefs() {
    Table t = new Table();
    for (int i = 0; i < getRecordDefCount(); i++) {
      t.put(Integer.toString(i), getRecordDef(i));
    }
    return t;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods to create a ZipFile
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Creates a new zip file. The files added to the zip are listed twice. First their names are
      listed as they are to appear within the zip file (must follow normal zip syntax, no file
      qualifiers). Secondly their files are listed as they currently exist on disk. <br> <br>
      Below is an example that creates a zip with three files; two data files that are in a data
      subdirectory and a readme.txt file at the top level.
      <pre>
        String[] names = { "data/temp.prm",  "data/myfile.prm",      "readme.txt" };
        String[] files = { "/data/temp.prm", "/data/gpb/myfile.prm", "/home/smith/readme.txt" };

        createZipFile(midas, "/home/smith/myzip.zip", names, files);
      </pre>
      @param midas   The midas context to use.
      @param zipFile The name of the zip file to create.
      @param names   The names of the files to write, as shown in the zip file. Names must use
                     forward slashes "/" not backward slashes "\" on all systems.
      @param files   The names of the files to write, as on disk. (Must be same length as names).
      @param verbose Show verbose output.
   */
  public static void createZipFile(Midas midas, FileName zipFile, String[] names, FileName[] files,
                                   boolean verbose) {
    try {
      FileOutputStream     dest = new FileOutputStream(zipFile.toString());
      BufferedOutputStream bos  = new BufferedOutputStream(dest);
      ZipOutputStream      out  = new ZipOutputStream(bos);

      createZipFile(midas,out,names,files,false,verbose);
    } catch (Exception e) {
      throw new MidasException("ZipFile: Error unable to create zip file",e);
    }
  }

  /** Writes files to a Jar/Zip file. Package-private, used by Zip-File and Jar-File. */
  static void createZipFile(Midas midas, ZipOutputStream out, String[] names, FileName[] files,
                            boolean jar, boolean verbose) throws IOException {
    byte[] buffer = new byte[BaseFile.BUFFER_SIZE]; // Buffer used when copying files

    if (names == null) { throw new MidasException("Names array is null"); }
    if (files == null) { throw new MidasException("Files array is null"); }
    if (names.length != files.length) {
      throw new MidasException("Names given do not match files given (names.length="+names.length
                              +" files.length="+files.length+").");
    }

    for (int i = 0; i < names.length; i++) {
      String  name = names[i];
      boolean done = false;

      if (name.startsWith(File.separator)) {
        name = name.substring(1); // Remove leading '/' for zip file.
      }

      ZipEntry entry = (jar)? new JarEntry(name) : new ZipEntry(name);

      // WRITE ZIP ENTRY ///////////////////////////////////////////
// JC4: This should use our internal FileResources so that we can create zips with
//      files not on the local disk. However, we can't do this until we resolve
//      issues with using file names with no extensions.
      out.putNextEntry(entry);
      if (name.endsWith("/")) {
        // directory
      } else if (files[i] != null) {
        FileInputStream     fis = new FileInputStream(files[i].getFullName());
        BufferedInputStream in  = new BufferedInputStream(fis,buffer.length);

        while (!done) {
          int bytesRead = in.read(buffer,0,buffer.length);

          if (bytesRead == 0) {
            Time.sleep(midas.pause);
          } else if (bytesRead < 0) {
            done = true;
          } else {
            out.write(buffer,0,bytesRead);
          }
        }
        // BUG 1153 fix: explicitly close out input file to prevent FileNotFoundException
        // with "Too many open files" message on multiprocessor systems (4+ CPUs)
        in.close();
        fis.close();
      }
      out.closeEntry();
      if (verbose) {
        String compRatio;
        if (entry.getSize() == 0) {
          compRatio = "(deflated 100%)";
        } else {
          float cr = (1.0f - (float)entry.getCompressedSize() / (float)entry.getSize()) * 100;
          compRatio = "(deflated "+((int)cr)+"%)";
        }
        midas.info(StringUtil.padRight("  adding: "+name+" "+compRatio,40));
      }
      // DONE ZIP ENTRY ////////////////////////////////////////////
    }
    out.close();
  }
}
