/**
 *  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.aries.unittest.fixture;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.osgi.framework.Constants;

/**
 * Utility class for creating archive-based fixtures such as EBA archives, jar files etc.
 * This class provides a flow based api for defining such fixtures. For example, a simple EBA archive could 
 * be defined as such:
 * 
 * <code>
 * ArchiveFixtures.ZipFixture zip = ArchiveFixtures.newZip()
 *   .jar("test.jar")
 *     .manifest()
 *       .symbolicName("com.ibm.test")
 *       .version("2.0.0")
 *     .end()
 *     .file("random.txt", "Some text")
 *   .end();
 * </code>
 * 
 * This defines a zip archive containing a single jar file (hence no application manifest). The jar file itself has
 * a manifest and a text file.
 * 
 * To actually create the physical archive use the <code>writeOut</code> method on the archive fixture.
 */
public class ArchiveFixture
{
  /**
   * Create a new zip file fixture
   * @return
   */
  public static ZipFixture newZip() {
    return new ZipFixture(null);
  }
  
  /**
   * Create a new jar file fixture
   * @return
   */
  public static JarFixture newJar() {
    return new JarFixture(null);
  }
  
  /**
   * Utility to copy an InputStream into an OutputStream. Closes the InputStream afterwards.
   * @param in
   * @param out
   * @throws IOException
   */
  private static void copy(InputStream in, OutputStream out) throws IOException
  {
    try {
      int len;
      byte[] b = new byte[1024];
      while ((len = in.read(b)) != -1)
        out.write(b,0,len);
    }
    finally {
      in.close();
    }
  }

  /**
   * Base interface for every fixture.
   */
  public interface Fixture {
    /**
     * Write the physical representation of the fixture to the given OutputStream
     * @param out
     * @throws IOException
     */
    void writeOut(OutputStream out) throws IOException;
  }
  
  /**
   * Abstract base class for fixtures. Archive fixtures are by nature hierarchical.
   */
  public static abstract class AbstractFixture implements Fixture {
    private ZipFixture parent;
    
    protected AbstractFixture(ZipFixture parent) {
      this.parent = parent;
    }
    
    /**
     * Ends the current flow target and returns the parent flow target. For example, in the
     * following code snippet the <code>end</code> after <code>.version("2.0.0")</code> marks
     * the end of the manifest. Commands after that relate to the parent jar file of the manifest.
     * 
     * <code>
     * ArchiveFixtures.ZipFixture zip = ArchiveFixtures.newZip()
     *   .jar("test.jar")
     *     .manifest()
     *       .symbolicName("com.ibm.test")
     *       .version("2.0.0")
     *     .end()
     *     .file("random.txt", "Some text")
     *   .end();
     * </code>
     * @return
     */
    public ZipFixture end() {
      return (parent == null) ? (ZipFixture) this : parent;
    }
  }

  /**
   * Simple fixture for text files.
   */
  public static class FileFixture extends AbstractFixture {
    private StringBuffer text = new StringBuffer();
    
    protected FileFixture(ZipFixture parent) {
      super(parent);
    }
    
    /**
     * Add a line to the file fixture. The EOL character is added automatically.
     * @param line
     * @return
     */
    public FileFixture line(String line) {
      text.append(line);
      text.append("\n");
      return this;
    }
    
    public void writeOut(OutputStream out) throws IOException {
      out.write(text.toString().getBytes());
    }
  }
  
  public static class IStreamFixture extends AbstractFixture {
    private byte[] bytes;
    
    protected IStreamFixture(ZipFixture parent, InputStream input) throws IOException {
      super(parent);

      ByteArrayOutputStream output = new ByteArrayOutputStream();
      try {
        copy(input, output);
      } finally {
        output.close();
      }
      
      bytes = output.toByteArray();
    }

    public void writeOut(OutputStream out) throws IOException {
      copy(new ByteArrayInputStream(bytes), out);
    }
  }
  
  /**
   * Fixture for (bundle) manifests. By default, they contain the lines
   * 
   * <code>
   * Manifest-Version: 1
   * Bundle-ManifestVersion: 2
   * </code>
   */
  public static class ManifestFixture extends AbstractFixture {
    private Manifest mf;
    
    protected Manifest getManifest()
    {
      return mf;
    }
    
    protected ManifestFixture(ZipFixture parent) {
      super(parent);
      mf = new Manifest();
      mf.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1");
      mf.getMainAttributes().putValue(Constants.BUNDLE_MANIFESTVERSION, "2");
    }
    
    /**
     * Set the symbolic name of the bundle
     * @param name
     * @return
     */
    public ManifestFixture symbolicName(String name)
    {
      mf.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, name);
      return this;
    }
    
    /**
     * Set the version of the bundle
     * @param version
     * @return
     */
    public ManifestFixture version(String version)
    {
      mf.getMainAttributes().putValue(Constants.BUNDLE_VERSION, version);
      return this;
    }
    
    /**
     * Add a custom attribute to the manifest. Use the more specific methods for symbolic name and version.
     * @param name
     * @param value
     * @return
     */
    public ManifestFixture attribute(String name, String value)
    {
      mf.getMainAttributes().putValue(name, value);
      return this;
    }
    
    public void writeOut(OutputStream out) throws IOException
    {
      mf.write(out);
    }
  }

  /**
   * Fixture for a jar archive. It offers the same functionality as zip fixtures.
   * The main difference is that in a jar archive the manifest will be output as the first file,
   * regardless of when it is added.
   */
  public static class JarFixture extends ZipFixture {
    private ManifestFixture mfFixture;
    
    protected JarFixture(ZipFixture parent) {
      super(parent);
    }
    
    @Override
    public ManifestFixture manifest()
    {
      if (mfFixture != null)
        throw new IllegalStateException("Only one manifest allowed, you dummy ;)");
      
      mfFixture = new ManifestFixture(this);
      return mfFixture;
    }
    
    @Override
    public InputStream getInputStream() throws IOException
    {
      if (bytes == null) {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        JarOutputStream jout;
        if (mfFixture != null)
          jout = new JarOutputStream(bout, mfFixture.getManifest());
        else
          jout = new JarOutputStream(bout);
        
        try {
          writeAllEntries(jout);
        } finally {
          jout.close();
        }
        
        bytes = bout.toByteArray();
      }
      
      return new ByteArrayInputStream(bytes);
    }
  }
  
  /**
   * Base fixture for any kind of zip archive. Zip archives can contain any number of child archives 
   * given by an archive type and a path. The order in which these child archives are added is important
   * because it will be the order in which they are added to the zip.
   */
  public static class ZipFixture extends AbstractFixture {
    protected static class ChildFixture {
      public String path;
      public Fixture fixture;
      
      public ChildFixture(String path, Fixture fixture)
      {
        this.path = path;
        this.fixture = fixture;
      }
    }
    
    protected List<ChildFixture> children = new ArrayList<ChildFixture>();
    protected byte[] bytes = null;
    
    protected ZipFixture(ZipFixture parent) {
      super(parent);
    }
        
    /**
     * Create a child zip fixture at the given target.
     * @param path
     * @return
     */
    public ZipFixture zip(String path) {
      ZipFixture res = new ZipFixture(this);
      children.add(new ChildFixture(path, res));
      
      return res;
    }
    
    /**
     * Create a child jar fixture at the given path.
     * @param path
     * @return
     */
    public ZipFixture jar(String path) {
      JarFixture res = new JarFixture(this);
      children.add(new ChildFixture(path, res));
      
      return res;
    }
    
    /**
     * Create a complete child file fixture at the given path and with the content.
     * Note: this will return the current zip fixture and not the file fixture.
     * 
     * @param path
     * @param content
     * @return
     */
    public ZipFixture file(String path, String content) 
    {
      return file(path).line(content).end();
    }
    
    /**
     * Create an empty file fixture at the given path.
     * 
     * @param path
     * @return
     */
    public FileFixture file(String path)
    {
      FileFixture res = new FileFixture(this);
      children.add(new ChildFixture(path, res));
      
      return res;
    }
    
    /**
     * Create a binary file with the content from the input stream
     * @param path
     * @param input
     * @return
     */
    public ZipFixture binary(String path, InputStream input) throws IOException {
      
      if (input == null) throw new IllegalArgumentException("Provided input stream cannot be null");
      
      IStreamFixture child = new IStreamFixture(this, input);
      children.add(new ChildFixture(path, child));
      
      return this;
    }
    
    /**
     * Create a binary file that is populated from content on the classloader
     * @param path
     * @param resourcePath Path that the resource can be found in the current classloader
     * @return
     */
    public ZipFixture binary(String path, String resourcePath) throws IOException {
      return binary(path, getClass().getClassLoader().getResourceAsStream(resourcePath));
    }
    
    /**
     * Create a manifest fixture at the given path.
     * @return
     */
    public ManifestFixture manifest()
    {
      ManifestFixture res = new ManifestFixture(this);
      children.add(new ChildFixture("META-INF/MANIFEST.MF", res));
      
      return res;
    }
    
    /**
     * Ensure that the necessary directory entries for the entry are available
     * in the zip file. Newly created entries are added to the set of directories.
     * 
     * @param zout
     * @param entry
     * @param existingDirs
     * @throws IOException
     */
    private void mkDirs(ZipOutputStream zout, String entry, Set<String> existingDirs) throws IOException
    {
      String[] parts = entry.split("/");
      String dirName = "";
      for (int i=0;i<parts.length-1;i++) {
        dirName += parts[i] + "/";
        if (!existingDirs.contains(dirName)) {
          ZipEntry ze = new ZipEntry(dirName);
          zout.putNextEntry(ze);
          zout.closeEntry();
          
          existingDirs.add(dirName);
        }
      }
    }
    
    /**
     * Add all entries to the ZipOutputStream
     * @param zout
     * @throws IOException
     */
    protected void writeAllEntries(ZipOutputStream zout) throws IOException
    {
      Set<String> dirs = new HashSet<String>();
      
      for (ChildFixture child : children) {
        mkDirs(zout, child.path, dirs);
        
        ZipEntry ze = new ZipEntry(child.path);
        zout.putNextEntry(ze);
        child.fixture.writeOut(zout);
        zout.closeEntry();
      }      
    }
    
    public void writeOut(OutputStream out) throws IOException 
    {
      copy(getInputStream(), out);
    }
    
    public InputStream getInputStream() throws IOException
    {
      /*
       * For better reuse this method delegate the writing to writeAllEntries, which
       * can be reused by the JarFixture.
       */
      ByteArrayOutputStream bout = new ByteArrayOutputStream();
      ZipOutputStream zout = new ZipOutputStream(bout);
      try {
        writeAllEntries(zout);
      } finally {
        zout.close();
      }
      
      bytes = bout.toByteArray();

      return new ByteArrayInputStream(bytes);
    }
  }
}
