/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.uima.pear.generate;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jface.operation.IRunnableWithProgress;

/**
 * {@link IRunnableWithProgress} that creates a zip file containing a given set of resources *
 */
public class PearExportOperation implements IRunnableWithProgress {

  /** An array of files that should be included in the zip file */
  protected final IFile[] fExports;

  /** Prefix to strip off from the provided files paths */
  protected final IContainer fRoot;

  /** Filename for the destination zip file */
  protected final String fFilename;

  /** whether zip contents should be compressed */
  protected final boolean fCompress;

  /**
   * Constructor
   * 
   * @param exports
   *          An array of files that should be included in the zip file
   * @param virtualRoot
   *          Prefix to strip off from the provided files paths, e.g. if '/foo/bar/baz.txt' is
   *          included and '/foo' is the virtual root, than '/bar/baz.txt' will be created in the
   *          zip file
   * @param filename
   *          Filename for the destination zip file
   * @param compress
   *          <code>true</code> if the zip contents should be compressed, <code>false</code>
   *          otherwise
   */
  public PearExportOperation(final IFile[] exports, final IContainer virtualRoot,
          final String filename, final boolean compress) {
    fExports = exports;
    fRoot = virtualRoot;
    fFilename = filename;
    fCompress = compress;
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.eclipse.jface.operation.IRunnableWithProgress#run(org.eclipse.core.runtime.IProgressMonitor)
   */
  public void run(final IProgressMonitor monitor) throws InvocationTargetException,
          InterruptedException {
    try {
      final int SCALE = 100;
      final String jobName = "Exporting " + fExports.length + " files to Pear file " + fFilename
              + "...";
      monitor.beginTask(jobName, 2 * SCALE * fExports.length);

      // Create output zip stream
      final File pearFile = new File(fFilename);
      FileOutputStream fOut;
      try {
        fOut = new FileOutputStream(pearFile);
      } catch (final FileNotFoundException exception) {
        throw new InvocationTargetException(exception);
      }
      final ZipOutputStream stream = new ZipOutputStream(fOut);

      if (monitor.isCanceled()) {
        // Nothing written, just close and cancel
        stream.close();
        throw new OperationCanceledException();
      }

      // Add each file to the zip stream
      for (int i = 0; i < fExports.length; ++i) {
        final IFile file = fExports[i];
        // Verify file is child of root
        if (!fRoot.getFullPath().isPrefixOf(file.getFullPath())) {
          throw new IllegalArgumentException(fRoot + " is not a parent of " + file); //$NON-NLS-1$
        }

        // Read file contents
        String filename = file.getFullPath().toPortableString();
        final String cutPath = fRoot.getFullPath().addTrailingSeparator().toPortableString();
        filename = filename.substring(cutPath.length());

        monitor.subTask("Adding " + filename);
        final byte[] fileContents = readContents(file, new SubProgressMonitor(monitor, SCALE));

        // Add contents to zip file
        final ZipEntry entry = new ZipEntry(filename);
        // file.getModificationStamp() seems to use a different semantic
        // for the timestamp
        entry.setTime(file.getLocation().toFile().lastModified());
        if (fCompress) {
          entry.setMethod(ZipEntry.DEFLATED);
        } else {
          entry.setMethod(ZipEntry.STORED);
          entry.setSize(fileContents.length);
          final CRC32 checksumCalculator = new CRC32();
          checksumCalculator.update(fileContents);
          entry.setCrc(checksumCalculator.getValue());
        }
        stream.putNextEntry(entry);
        stream.write(fileContents);
        monitor.worked(SCALE);

        if (monitor.isCanceled()) {
          // Close zip file and remove it, then terminate
          stream.close();
          pearFile.delete();
          throw new OperationCanceledException();
        }
      }

      stream.close();
    } catch (final FileNotFoundException exception) {
      throw new InvocationTargetException(exception);
    } catch (final IOException exception) {
      throw new InvocationTargetException(exception);
    } finally {
      monitor.done();
    }

  }

  /**
   * @param file
   *          The file to read
   * @param monitor
   * @return A byte array with the file contents
   * @throws IOException
   */
  private byte[] readContents(final IFile file, IProgressMonitor monitor) throws IOException {
    final ByteArrayOutputStream output = new ByteArrayOutputStream();
    final File localFile = file.getLocation().toFile();
    final int bufferSize = 4096;
    int count = (int) (localFile.length() / bufferSize);
    try {
      monitor.beginTask("Reading file " + file.getFullPath().toPortableString(), count);
      final FileInputStream in = new FileInputStream(localFile);
      final BufferedInputStream bIn = new BufferedInputStream(in);
      int length = 0;
      byte[] buffer = new byte[bufferSize];
      byte[] bufferCopy = new byte[bufferSize];
      while ((length = bIn.read(buffer, 0, bufferSize)) != -1) {
        bufferCopy = new byte[length];
        System.arraycopy(buffer, 0, bufferCopy, 0, length);
        output.write(bufferCopy);
        monitor.worked(1);
      }
      bIn.close();
    } finally {
      monitor.done();
      output.close();
    }

    return output.toByteArray();
  }

}
