/*
 * 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 WARRANTIESOR 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.aries.util.filesystem.impl;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

import org.apache.aries.util.IORuntimeException;
import org.apache.aries.util.filesystem.IDirectory;
import org.apache.aries.util.filesystem.IFile;

/**
 * An implementation of IFile that represents a file entry in a zip.
 */
public class ZipFileImpl implements IFile
{
  /** The name of the file */
  private String name;
  /** The size of the file */
  private final long size;
  /** The last time the file was updated */
  private final long lastModified;
  /** The zip file this is contained in */
  protected final File zip;
  /** The entry in the zip this IFile represents */
  protected final ZipEntry entry;
  /** The parent directory */
  private final IDirectory parent;
  /** The URL of the zip file we are looking inside of */
  private final String url;
  /** The path of the zip archive to the VFS root */
  private final String zipPathToRoot;
  /** The closeable directory that caches the open ZipFile */
  protected final ZipCloseableDirectory cache;

  /**
   * This constructor is used to create a file entry within the zip.
   *
   * @param zip1    the zip file the entry is in.
   * @param entry1  the entry this IFile represents.
   * @param parent1 the parent directory.
   */
  public ZipFileImpl(File zip1, ZipEntry entry1, ZipDirectory parent1, ZipCloseableDirectory cache)
  {
    this.zip = zip1;
    this.entry = entry1;

    this.zipPathToRoot = parent1.getZipPathToRoot();

    name = zipPathToRoot + entry1.getName();

    if (entry1.isDirectory()) name = name.substring(0, name.length() - 1);

    lastModified = entry1.getTime();
    size = entry1.getSize();

    url = ((ZipFileImpl)parent1).url;

    this.parent = parent1;
    this.cache = cache;
  }

  /**
   * This is called to construct the root directory of the zip.
   *
   * @param zip1 the zip file this represents.
   * @param parent
   * @throws MalformedURLException
   */
  protected ZipFileImpl(File zip1, IDirectory parent) throws MalformedURLException
  {
    this.zip = zip1;
    this.entry = null;

    if (parent == null) {
        name = "";
        zipPathToRoot = "";
        this.parent = null;
    } else {
    	this.parent = parent;
    	name = parent.getName() + "/" + zip1.getName();
    	zipPathToRoot = name+"/";
    }

    lastModified = zip1.lastModified();
    size = zip1.length();
    url = zip1.toURI().toURL().toExternalForm();
    this.cache = null;
  }

  public ZipFileImpl(ZipFileImpl other, ZipCloseableDirectory cache) {
	  name = other.name;
	  size = other.size;
	  lastModified = other.lastModified;
	  zip = other.zip;
	  entry = other.entry;
	  parent = other.parent;
	  url = other.url;
	  zipPathToRoot = other.zipPathToRoot;
	  this.cache = cache;
  }

  /**
   * Obtain the path of the zip file to the VFS root
   */
  public String getZipPathToRoot() {
	  return zipPathToRoot;
  }

  public IDirectory convert()
  {
    return null;
  }

  public IDirectory convertNested() {
	  if (isDirectory()) return convert();
	  else if (FileSystemImpl.isValidZip(this)) return new NestedZipDirectory(this);
	  else return null;
  }

  public long getLastModified()
  {
    return lastModified;
  }

  public String getName()
  {
    return name;
  }

  public String getNameInZip()
  {
	  if (entry == null) return "";
	  else {
		  String name = entry.getName();
		  if (isDirectory()) return name.substring(0, name.length()-1);
		  else return name;
	  }
  }

  public IDirectory getParent()
  {
    return parent;
  }

  public long getSize()
  {
    return size;
  }

  public boolean isDirectory()
  {
    return false;
  }

  public boolean isFile()
  {
    return true;
  }

  public InputStream open() throws IOException
  {
    InputStream is = new SpecialZipInputStream(entry);
    return is;
  }

  public IDirectory getRoot()
  {
    return parent.getRoot();
  }

  public URL toURL() throws MalformedURLException
  {
    URL result;

    if(name.equals(zipPathToRoot))
      result = new URL(url);
    else {

      String entryURL = "jar:" + url + "!/";
      if(entry != null)
        entryURL += entry.getName();
      else {
        entryURL += name.substring(zipPathToRoot.length());
      }
      result = new URL(entryURL);
    }

    return result;
  }

  @Override
  public boolean equals(Object obj)
  {
    if (obj == null) return false;
    if (obj == this) return true;

    if (obj.getClass() == getClass()) {
      return toString().equals(obj.toString());
    }

    return false;
  }

  @Override
  public int hashCode()
  {
    return toString().hashCode();
  }

  @Override
  public String toString()
  {
	  if (name != null && name.length() != 0) return url.substring(5)+ "/" + name;
	  else return url.substring(5);
  }

  ZipFile openZipFile(){
    ZipFile z = null;

    if (cache != null && !cache.isClosed()) {
    	z = cache.getZipFile();
    } else {
	    try {
	      z = new ZipFile(zip);
	    } catch (IOException e) {
	      throw new IORuntimeException("IOException in ZipFileImpl.openZipFile", e);
	    }
    }
    return z;
  }

  void closeZipFile(ZipFile z){
	  if (cache != null && cache.getZipFile() == z) {
		  // do nothing
	  } else {
		  try{
			  z.close();
		  }
		  catch (IOException e) {
			  throw new IORuntimeException("IOException in ZipFileImpl.closeZipFile", e);
		  }
	  }
  }

  /**
   * A simple class to delegate to the InputStream of the constructor
   * and to call close on the zipFile when we close the stream.
   *
   */
  private class SpecialZipInputStream extends InputStream{

    private ZipFile zipFile;
    private InputStream is;

    public SpecialZipInputStream(ZipEntry anEntry){
      try{
      this.zipFile = openZipFile();
      this.is = zipFile.getInputStream(anEntry);
      }
      catch (ZipException e) {
        throw new IORuntimeException("ZipException in SpecialZipInputStream()", e);
      } catch (IOException e) {
        throw new IORuntimeException("IOException in SpecialZipInputStream()", e);
      }
    }

    @Override
    public int read(byte[] b) throws IOException {
      return is.read(b);
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
      return is.read(b, off, len);
    }

    @Override
    public int read() throws IOException
    {
      return is.read();
    }

    @Override
    public void close() throws IOException{
        //call close on the input stream, probably does nothing
        is.close();
        //call close on the zip file, important for tidying up
        closeZipFile(zipFile);
    }

  }

}
