package ro.isdc.wro.model.resource.locator.wildcard;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.isdc.wro.model.resource.locator.ClasspathUriLocator;


/**
 * Resolves classpath resources looking for wildcard patterns in both file system and in JAR files.
 * <p>
 * The {@link #locateStream(String, File)} overrides the default strategy defined in
 * {@link DefaultWildcardStreamLocator} and it tries to open the provided file as a JAR. If that's successfully opened
 * all entries inside this container will be verified against the wildcard pattern. If the JAR-lookup strategy fails,
 * default strategy is invoked.
 * </p>
 * <p>
 * For the moment this {@link WildcardStreamLocator} only supports a single wildcard.
 * </p>
 *
 * @author Matias Mirabelli <matias.mirabelli@globant.com>
 * @since 1.3.6
 */
public class JarWildcardStreamLocator
    extends DefaultWildcardStreamLocator {
  private static final Logger LOG = LoggerFactory.getLogger(JarWildcardStreamLocator.class);
  /**
   * A {@link List} of file extensions including the final dot. Valid examples are: .jar, .war. By default it only
   * supports .jar extension.
   */
  private static final List<String> SUPPORTED_EXTENSIONS = Arrays.asList(".jar");

  /**
   * Finds the specified URI pattern inside a JAR file. If the specified file isn't a valid JAR default strategy will be
   * used instead.
   */
  @Override
  public InputStream locateStream(final String uri, final File folder)
      throws IOException {
    Validate.notNull(folder);
    final File jarPath = getJarFile(folder);
    if (isSupported(jarPath)) {
      return locateStreamFromJar(uri, jarPath);
    }
    return super.locateStream(uri, folder);
  }

  /**
   * @return true if the file is of a certain supported type.
   */
  private boolean isSupported(final File jarPath) {
    LOG.debug("jarPath: {}", jarPath);
    for (final String supportedExtension : SUPPORTED_EXTENSIONS) {
      if (jarPath.getPath().endsWith(supportedExtension)) {
        return true;
      }
    }
    return false;
  }

  /**
   * @VisibleForTestOnly
   * @return the File corresponding to the folder from inside the jar.
   */
  File getJarFile(final File folder) {
    return new File(StringUtils.substringAfter(StringUtils.substringBeforeLast(folder.getPath(), "!"),
        "file:"));
  }

  /**
   * Validates an entry against a wildcard and determines whether the pattern matches or not. If the entry is accepted
   * this will be included in the result {@link InputStream}.
   *
   * @param entry
   *          Entry to evaluate. It cannot be null.
   * @param wildcard
   *          Wildcard to match. It cannot be null or empty.
   * @return <code>true</code> if the expression matches, <code>false</code> otherwise.
   */
  private boolean accept(final String entryName, final String wildcard) {
    return FilenameUtils.wildcardMatch(entryName, wildcard);
  }

  /**
   * Opens the specified JAR file and returns a valid handle.
   *
   * @param jarFile
   *          Location of the valid JAR file to read. It cannot be null.
   * @return A valid {@link JarFile} to read resources.
   * @throws IllegalArgumentException
   *           If the file cannot be opened because an {@link IOException}.
   *  @VisibleForTestOnly
   */
  JarFile open(final File jarFile) throws IOException {
    Validate.isTrue(jarFile.exists(), "The JAR file must exists.");
    return new JarFile(jarFile);
  }

  /**
   * Finds the specified wildcard-URI resource(s) inside a JAR file and returns an {@link InputStream} to read a bundle
   * of matching resources.
   *
   * @param uri
   *          Resource(s) URI to match. It cannot be null or empty.
   * @param jarPath
   *          A valid JAR file. It cannot be null.
   * @return A valid {@link InputStream} to read the bundle. Clients are responsible of closing this {@link InputStream}
   * @throws IOException
   *           If there's any error reading the JAR file.
   */
  private InputStream locateStreamFromJar(final String uri, final File jarPath)
      throws IOException {
    LOG.debug("Locating stream from jar: {}", jarPath);
    final WildcardContext wildcardContext = new WildcardContext(uri, jarPath);
    String classPath = FilenameUtils.getPath(uri);

    if (classPath.startsWith(ClasspathUriLocator.PREFIX)) {
      classPath = StringUtils.substringAfter(classPath, ClasspathUriLocator.PREFIX);
    }

    final JarFile file = open(jarPath);
    final List<JarEntry> jarEntryList = Collections.list(file.entries());
    final List<JarEntry> filteredJarEntryList = new ArrayList<JarEntry>();
    final List<File> allFiles = new ArrayList<File>();
    for (final JarEntry entry : jarEntryList) {
      final String entryName = entry.getName();
      //ignore the parent folder itself and accept only child resources
      final boolean isSupportedEntry = entryName.startsWith(classPath) && !entryName.equals(classPath)
        && accept(entryName, wildcardContext.getWildcard());
      if (isSupportedEntry) {
        allFiles.add(new File(entryName));
        LOG.debug("\tfound jar entry: {}", entryName);
        filteredJarEntryList.add(entry);
      }
    }
    final ByteArrayOutputStream out = new ByteArrayOutputStream();

    triggerWildcardExpander(allFiles, wildcardContext);
    for (final JarEntry entry : filteredJarEntryList) {
      final InputStream is = file.getInputStream(entry);
      IOUtils.copy(is, out);
      is.close();
    }
    return new BufferedInputStream(new ByteArrayInputStream(out.toByteArray()));
  }
}
