/*
 * Copyright (C) 2014 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed 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.jboss.errai.offline.linker;

import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.Set;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.gwt.core.ext.Linker;
import com.google.gwt.core.ext.LinkerContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.AbstractLinker;
import com.google.gwt.core.ext.linker.Artifact;
import com.google.gwt.core.ext.linker.ArtifactSet;
import com.google.gwt.core.ext.linker.EmittedArtifact;
import com.google.gwt.core.ext.linker.LinkerOrder;
import com.google.gwt.core.ext.linker.LinkerOrder.Order;
import com.google.gwt.core.ext.linker.Shardable;
import com.google.gwt.core.ext.linker.Transferable;
import com.google.gwt.core.ext.linker.impl.SelectionInformation;
import com.google.gwt.dev.util.collect.HashSet;

/**
 * Linker that generates permutation specific HTML5 Cache Manifest files for
 * Errai applications.
 * 
 * This linker is based on <a
 * href="https://code.google.com/p/google-web-toolkit/source/browse/
 * trunk/dev/core/src/
 * com/google/gwt/core/linker/SimpleAppCacheLinker.java?r=10136
 * ">SimpleAppCacheLinker</a>.
 * 
 * <p>
 * To use:
 * <ol>
 * <li>Define the linker in your gwt.xml module descriptor:
 * 
 * <pre>
 *   {@code <define-linker name="offline" class="org.jboss.errai.offline.linker.DefaultCacheManifestLinker" />}
 *   {@code <add-linker name="offline" />}
 * </pre>
 * 
 * </li>
 * 
 * <li>Add {@code manifest="YOURMODULENAME/errai.appcache"} to the
 * {@code <html>} tag in your host page e.g.,
 * {@code <html manifest="mymodule/errai.appcache">} <br>
 * <br>
 * </li>
 * 
 * <li>Add a mime-mapping to your web.xml file (you can skip this step if you
 * deploy the errai-javaee-all.jar as part of your application):
 * 
 * <pre>
 * {@code <mime-mapping>
 *   <extension>manifest</extension>
 *   <mime-type>text/cache-manifest</mime-type>
 * </mime-mapping>
 * }
 * </pre>
 * 
 * </li>
 * 
 * <li>Make sure the errai-common.jar file is deployed as part of your
 * application. It contains a servlet that will provide the correct user-agent
 * specific manifest file in response to requests to
 * YOURMODULENAME/errai.appcache</li>
 * </ol>
 * 
 * <p>
 * To obtain manifests that contain other files in addition to those generated
 * by this linker, create a class that inherits from this one and overrides
 * {@code otherCachedFiles()}, and use it as a linker instead:
 * 
 * <pre>
 * {@code @Shardable}
 * {@code @LinkerOrder(Order.POST)}
 * public class MyCacheManifestLinker extends DefaultCacheManifestLinker {
 *   {@code @Override}
 *   protected String[] otherCachedFiles() {
 *     return new String[] {"/my-app/index.html","/my-app/css/application.css"};
 *   }
 * }
 * </pre>
 * 
 * @author Christian Sadilek <csadilek@redhat.com>
 */
@Shardable
@LinkerOrder(Order.POST)
public class DefaultCacheManifestLinker extends AbstractLinker {
  private static final String MANIFEST = "appcache.manifest";

  @Transferable
  private class PermutationCacheManifestArtifact extends Artifact<PermutationCacheManifestArtifact> {
    private static final long serialVersionUID = 1L;

    private final Set<String> cachedFiles = new HashSet<String>();
    private final String name;
    private final Map<String, String> props;

    public PermutationCacheManifestArtifact(Class<? extends Linker> linker, String name, Map<String, String> props) {
      super(linker);
      this.name = name;
      this.props = props;
    }

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

    @Override
    protected int compareToComparableArtifact(PermutationCacheManifestArtifact o) {
      return name.compareTo(o.name);
    }

    @Override
    protected Class<PermutationCacheManifestArtifact> getComparableArtifactType() {
      return PermutationCacheManifestArtifact.class;
    }

    public void addCachedFile(String file) {
      cachedFiles.add(file);
    }

  }

  @Override
  public String getDescription() {
    return "to generate cache manifest files";
  }

  @Override
  public ArtifactSet link(TreeLogger logger, LinkerContext context, ArtifactSet artifacts, boolean onePermutation)
          throws UnableToCompleteException {

    final ArtifactSet toReturn = new ArtifactSet(artifacts);
    if (toReturn.find(SelectionInformation.class).isEmpty()) {
      logger.log(TreeLogger.INFO, "devmode: generating empty " + MANIFEST);
      toReturn.add(emitString(logger, "# Empty in DevMode\n", "dev." + MANIFEST));
    }
    else if (onePermutation) {
      // Create an artifact representing the cache manifest for the current
      // permutation
      toReturn.add(createPermutationCacheManifestArtifact(context, logger, artifacts));
    }
    else {
      // Group permutations per user agent
      final Multimap<String, PermutationCacheManifestArtifact> permutations = ArrayListMultimap.create();
      for (final PermutationCacheManifestArtifact pcma : artifacts.find(PermutationCacheManifestArtifact.class)) {
        permutations.put(pcma.props.get("user.agent"), pcma);
      }

      for (final String userAgent : permutations.keySet()) {
        // Create a cache manifest file for every user agent
        toReturn.add(emitUserAgentCacheManifestFile(userAgent, permutations.get(userAgent), artifacts, logger));

      }

      logger.log(TreeLogger.INFO,
              "Make sure you have the following attribute added to your host page's <html> tag: <html manifest=\""
                      + context.getModuleFunctionName() + "/" + MANIFEST + "\">");
    }
    return toReturn;
  }

  /**
   * Override this method to include additional files in the manifest.
   */
  protected String[] otherCachedFiles() {
    return null;
  }

  /**
   * Creates the cache manifest artifact specific to the current permutation.
   * 
   * @param context
   *          the linker environment
   * @param logger
   *          the tree logger to record to
   * @param artifacts
   *          {@code null} in case no permutation specific artifacts exist
   */
  private Artifact<?> createPermutationCacheManifestArtifact(LinkerContext context, TreeLogger logger,
          ArtifactSet artifacts) {

    final SelectionInformation si = artifacts.find(SelectionInformation.class).first();
    final PermutationCacheManifestArtifact cacheArtifact = new PermutationCacheManifestArtifact(this.getClass(),
            si.getStrongName(), si.getPropMap());

    for (final Artifact<?> artifact : artifacts) {
      if (artifact instanceof EmittedArtifact) {
        final EmittedArtifact ea = (EmittedArtifact) artifact;
        final String pathName = ea.getPartialPath();
        if (shouldBeCached(pathName)) {
          cacheArtifact.addCachedFile(pathName);
        }
      }
    }
    return cacheArtifact;
  }

  /**
   * Emits the cache manifest file for the user agent represented by the
   * provided artifacts.
   * 
   * @param userAgent
   *          the user agent name
   * @param artifacts
   *          the user agent specific cache manifest artifacts
   * @param globalArtifacts
   *          all artifacts known to this linker
   * @param logger
   *          the tree logger to record to
   * @return a synthetic artifact representing the cache manifest file for the
   *         user agent
   * 
   * @throws UnableToCompleteException
   */
  private Artifact<?> emitUserAgentCacheManifestFile(String userAgent,
          Collection<PermutationCacheManifestArtifact> artifacts, ArtifactSet globalArtifacts, TreeLogger logger)
          throws UnableToCompleteException {

    // Add static external resources
    final StringBuilder staticResoucesSb = new StringBuilder();
    final String[] cacheExtraFiles = getCacheExtraFiles();
    for (int i = 0; i < cacheExtraFiles.length; i++) {
      staticResoucesSb.append(cacheExtraFiles[i]);
      staticResoucesSb.append("\n");
    }

    // Add generated resources
    final Set<String> cacheableGeneratedResources = new HashSet<String>();

    // Add permutation independent resources
    if (globalArtifacts != null) {
      for (final Artifact<?> a : globalArtifacts) {
        if (a instanceof EmittedArtifact) {
          final EmittedArtifact ea = (EmittedArtifact) a;
          final String pathName = ea.getPartialPath();
          if (shouldBeCached(pathName) && !isPermutationSpecific(globalArtifacts, pathName)) {
            cacheableGeneratedResources.add(pathName);
          }
        }
      }
    }

    // Add permutation specific resources
    if (artifacts != null) {
      for (final PermutationCacheManifestArtifact artifact : artifacts) {
        for (final String cachedFile : artifact.cachedFiles) {
          cacheableGeneratedResources.add(cachedFile);
        }
      }
    }

    // Build manifest
    final StringBuilder sb = new StringBuilder();
    sb.append("CACHE MANIFEST\n");
    // we have to generate this unique id because the resources can change but
    // the hashed cache.html files can remain the same.
    sb.append("# Unique id #" + (new Date()).getTime() + "." + Math.random() + "\n");
    sb.append("\n");
    sb.append("CACHE:\n");
    sb.append("# Static app files\n");
    sb.append(staticResoucesSb.toString());
    sb.append("\n# Generated permutation specific app files");
    for (final String resource : cacheableGeneratedResources) {
      sb.append("\n");
      sb.append(resource);
    }
    sb.append("\n\n");
    sb.append("# All other resources require the user to be online.\n");
    sb.append("NETWORK:\n");
    sb.append("*\n");

    // Create the user agent specific manifest as a new artifact and return it
    return emitString(logger, sb.toString(), userAgent + "." + MANIFEST);
  }

  /**
   * Obtains the extra files to include in the manifest. Ensures the returned
   * array is not null.
   */
  private String[] getCacheExtraFiles() {
    final String[] cacheExtraFiles = otherCachedFiles();
    return cacheExtraFiles == null ? new String[0] : Arrays.copyOf(cacheExtraFiles, cacheExtraFiles.length);
  }

  private Set<String> permutationFiles;

  /**
   * Checks whether the provided file is specific to a permutation.
   * 
   * @param artifacts
   *          all artifacts passed to this linker
   * @param file
   *          the file to check
   * @return true if the file is specific to a permutation, otherwise false.
   * 
   */
  private boolean isPermutationSpecific(ArtifactSet artifacts, String file) {
    if (permutationFiles == null) {
      permutationFiles = new HashSet<String>();
      for (final PermutationCacheManifestArtifact pcma : artifacts.find(PermutationCacheManifestArtifact.class)) {
        permutationFiles.addAll(pcma.cachedFiles);
      }
    }

    return permutationFiles.contains(file);
  }

  /**
   * Checks whether or not the provided file should be cached.
   * 
   * @param file
   *          the file to check
   * @return true if the file is cacheable and should therefore be listed in the
   *         cache manifest file, otherwise false.
   */
  private boolean shouldBeCached(String file) {
    return !(file.endsWith("symbolMap") || file.endsWith(".xml.gz") || file.endsWith("rpc.log")
            || file.endsWith("gwt.rpc") || file.endsWith("manifest.txt") || file.startsWith("rpcPolicyManifest") 
            || file.startsWith("deferredjs") || file.startsWith("hosted") || file.startsWith("junit") );
  }
}
