/**
 * Copyright@2011 wro4j
 */
package ro.isdc.wro.model.transformer;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

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

import ro.isdc.wro.model.WroModel;
import ro.isdc.wro.model.group.Group;
import ro.isdc.wro.model.group.Inject;
import ro.isdc.wro.model.resource.Resource;
import ro.isdc.wro.model.resource.locator.UriLocator;
import ro.isdc.wro.model.resource.locator.factory.UriLocatorFactory;
import ro.isdc.wro.model.resource.locator.wildcard.DefaultWildcardStreamLocator;
import ro.isdc.wro.model.resource.locator.wildcard.WildcardExpanderHandlerAware;
import ro.isdc.wro.model.resource.locator.wildcard.WildcardStreamLocator;
import ro.isdc.wro.model.resource.locator.wildcard.WildcardUriLocatorSupport;
import ro.isdc.wro.util.Function;
import ro.isdc.wro.util.Transformer;


/**
 * A decorator which looks up for resources containing wildcards and replaces them with the corresponding collection of
 * specific resources. For example, a resource of this type:
 * <p/>
 * {@code
 *   <js>/path/to/**.js</js>
 * }
 * <p/>
 * can be transformed into:
 * <p/>
 * {@code
 *   <js>/path/to/a1.js</js>
 * } <br/>
 * {@code
 *   <js>/path/to/a2.js</js>
 * } <br/>
 * {@code
 *   <js>/path/to/a3.js</js>
 * } <br/>
 * <p/>
 * This model transformation is also known as wildcard expander, because it mutates the model after it is built by
 * adding resources to the group which contains resources with wildcard uri.
 *
 * @author Alex Objelean
 * @created 18 Jul 2011
 * @since 1.4.0
 */
@SuppressWarnings("serial")
public class WildcardExpanderModelTransformer
    implements Transformer<WroModel> {
  private static final Logger LOG = LoggerFactory.getLogger(WildcardExpanderModelTransformer.class);

  @Inject
  private UriLocatorFactory locatorFactory;

  /**
   * An instance of IOException having a special purpose: to skip subsequent attempts to localize a stream.
   */
  public static class NoMoreAttemptsIOException
      extends IOException {
    public NoMoreAttemptsIOException(final String message) {
      super(message);
    }
  }

  /**
   * {@inheritDoc}
   */
  public synchronized WroModel transform(final WroModel input) {
    final WroModel model = input;

    for (final Group group : model.getGroups()) {
      final List<Resource> resources = group.getResources();
      for (final Resource resource : resources) {
        processResource(group, resource);
      }
    }
    LOG.debug("Transformed model: {}", model);
    return model;
  }

  /**
   * Process each resource and replace it with a collection of resources if it contains wildcard.
   */
  private void processResource(final Group group, final Resource resource) {
    final UriLocator uriLocator = locatorFactory.getInstance(resource.getUri());

    if (uriLocator instanceof WildcardUriLocatorSupport) {
      final WildcardStreamLocator wildcardStreamLocator = ((WildcardUriLocatorSupport) uriLocator).getWildcardStreamLocator();

      // TODO should we probably handle the situation when wildcard is present, but the implementation is not
      // expandedHandledAware?
      if (wildcardStreamLocator.hasWildcard(resource.getUri())
          && wildcardStreamLocator instanceof WildcardExpanderHandlerAware) {

        final WildcardExpanderHandlerAware expandedHandler = (WildcardExpanderHandlerAware) wildcardStreamLocator;
        LOG.debug("Expanding resource: {}", resource.getUri());

        final String baseNameFolder = computeBaseNameFolder(resource, uriLocator, expandedHandler);
        LOG.debug("baseNameFolder: {}", baseNameFolder);

        expandedHandler.setWildcardExpanderHandler(createExpanderHandler(group, resource, baseNameFolder));
        try {
          // trigger the wildcard replacement
          uriLocator.locate(resource.getUri());
        } catch (final IOException e) {
          // log only
          LOG.debug("[FAIL] problem while trying to expand wildcard for the following resource uri: {}",
              resource.getUri());
        } finally {
          // remove the handler, it is not needed anymore
          expandedHandler.setWildcardExpanderHandler(null);
        }
      }
    }
  }

  /**
   * Computes the file name of the folder where the resource is located. The implementation uses a trick by invoking the
   * {@link WildcardExpanderHandlerAware} to get the baseName.
   */
  private String computeBaseNameFolder(final Resource resource, final UriLocator uriLocator,
      final WildcardExpanderHandlerAware expandedHandler) {
    // Find the baseName
    // add a recursive wildcard to trigger the wildcard detection. The simple wildcard ('*') is not enough because it
    // won't work for folders containing only directories with no files.
    LOG.debug("computeBaseNameFolder for resource {}", resource);
    final String resourcePath = FilenameUtils.getFullPath(resource.getUri())
        + DefaultWildcardStreamLocator.RECURSIVE_WILDCARD;
    LOG.debug("resourcePath: {}", resourcePath);
    // use thread local because we need to assign a File inside an anonymous class and it fits perfectly
    final ThreadLocal<String> baseNameFolderHolder = new ThreadLocal<String>();
    expandedHandler.setWildcardExpanderHandler(createBaseNameComputerFunction(baseNameFolderHolder));

    try {
      uriLocator.locate(resourcePath);
    } catch (final Exception e) {
      LOG.debug("[FAIL] Exception caught during wildcard expanding for resource: {}\n with exception message {}",
          resourcePath, e.getMessage());
    }
    if (baseNameFolderHolder.get() == null) {
      LOG.debug("[FAIL] Cannot compute baseName folder for resource: {}", resource);
    }
    return baseNameFolderHolder.get();
  }

  private Function<Collection<File>, Void> createBaseNameComputerFunction(final ThreadLocal<String> baseNameFolderHolder) {
    return new Function<Collection<File>, Void>() {
      public Void apply(final Collection<File> input)
          throws Exception {
        LOG.debug("\texpanded Files: {}", input);
        for (final File file : input) {
          LOG.debug("\tsetting baseNameFolder: {}", file.getParent());
          baseNameFolderHolder.set(file.getParent());
          // no need to continue
          break;
        }
        // use this to skip wildcard stream detection, we are only interested in the baseName
        throw new NoMoreAttemptsIOException("BaseNameFolder computed successfully, skip further wildcard processing..");
      }
    };
  }

  /**
   * create the handler which expand the resources containing wildcard.
   */
  public Function<Collection<File>, Void> createExpanderHandler(final Group group, final Resource resource,
      final String baseNameFolder) {
    LOG.debug("createExpanderHandler using baseNameFolder: {}\n for resource {}", baseNameFolder, resource);
    final Function<Collection<File>, Void> handler = new Function<Collection<File>, Void>() {
      public Void apply(final Collection<File> files) {
        if (baseNameFolder == null) {
          // replacing group with empty list since the original uri has no associated resources.
          // No BaseNameFolder found
          LOG.warn("The resource {} is probably invalid, removing it from the group.", resource);
          group.replace(resource, new ArrayList<Resource>());
        } else {
          final List<Resource> expandedResources = new ArrayList<Resource>();
          LOG.debug("baseNameFolder: {}", baseNameFolder);
          for (final File file : files) {
            final String resourcePath = getFullPathNoEndSeparator(resource);
            LOG.debug("\tresourcePath: {}", resourcePath);
            LOG.debug("\tfile path: {}", file.getPath());
            final String computedResourceUri = resourcePath
                + StringUtils.removeStart(file.getPath(), baseNameFolder).replace('\\', '/');

            final Resource expandedResource = Resource.create(computedResourceUri, resource.getType());
            LOG.debug("\texpanded resource: {}", expandedResource);
            expandedResources.add(expandedResource);
          }
          LOG.debug("\treplace resource {}", resource);
          group.replace(resource, expandedResources);
        }
        return null;
      }

      /**
       * This method fixes the problem when a resource in a group uses deep wildcard and starts at the root.
       * <p/>
       * Find more details <a href="https://github.com/alexo/wro4j/pull/44">here</a>.
       */
      private String getFullPathNoEndSeparator(final Resource resource) {
        final String result = FilenameUtils.getFullPathNoEndSeparator(resource.getUri());
        if (result != null && 1 == result.length() && 0 == FilenameUtils.indexOfLastSeparator(result)) {
          return "";
        }

        return result;
      }
    };
    return handler;
  }
}
