// Copyright 2015 The Bazel Authors. All rights reserved.
//
// 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 com.google.devtools.build.android;

import com.android.ide.common.res2.MergingException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Predicate;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Ordering;

import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;

/**
 * Filters a {@link MergedAndroidData} resource drawables to the specified densities.
 */
public class DensitySpecificResourceFilter {
  private static class ResourceInfo {
    /** Path to an actual file resource, instead of a directory. */
    private Path resource;
    private String restype;
    private String qualifiers;
    private String density;
    private String resid;

    public ResourceInfo(Path resource, String restype, String qualifiers, String density,
        String resid) {
      this.resource = resource;
      this.restype = restype;
      this.qualifiers = qualifiers;
      this.density = density;
      this.resid = resid;
    }

    public Path getResource() {
      return this.resource;
    }

    public String getRestype() {
      return this.restype;
    }

    public String getQualifiers() {
      return this.qualifiers;
    }

    public String getDensity() {
      return this.density;
    }

    public String getResid() {
      return this.resid;
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .add("resource", resource)
          .add("restype", restype)
          .add("qualifiers", qualifiers)
          .add("density", density)
          .add("resid", resid)
          .toString();
    }
  }

  private static class RecursiveFileCopier extends SimpleFileVisitor<Path> {
    private final Path copyToPath;
    private final List<Path> copiedSourceFiles = new ArrayList<>();
    private Path root;

    public RecursiveFileCopier(final Path copyToPath, final Path root) {
      this.copyToPath = copyToPath;
      this.root = root;
    }

    @Override
    public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
      Path copyTo = copyToPath.resolve(root.relativize(path));
      Files.createDirectories(copyTo.getParent());
      Files.copy(path, copyTo, LinkOption.NOFOLLOW_LINKS);
      copiedSourceFiles.add(copyTo);
      return FileVisitResult.CONTINUE;
    }

    public List<Path> getCopiedFiles() {
      return copiedSourceFiles;
    }
  }

  private final List<String> densities;
  private final Path out;
  private final Path working;

  private static final Map<String, Integer> DENSITY_MAP =
      new ImmutableMap.Builder<String, Integer>()
          .put("nodpi", 0)
          .put("ldpi", 120)
          .put("mdpi", 160)
          .put("tvdpi", 213)
          .put("hdpi", 240)
          .put("280dpi", 280)
          .put("xhdpi", 320)
          .put("340dpi", 340)
          .put("400dpi", 400)
          .put("420dpi", 420)
          .put("xxhdpi", 480)
          .put("560dpi", 560)
          .put("xxxhdpi", 640)
          .build();

  private static final Function<ResourceInfo, String> GET_RESOURCE_ID =
      new Function<ResourceInfo, String>() {
        @Override
        public String apply(ResourceInfo info) {
          return info.getResid();
        }
      };

  private static final Function<ResourceInfo, String> GET_RESOURCE_QUALIFIERS =
      new Function<ResourceInfo, String>() {
        @Override
        public String apply(ResourceInfo info) {
          return info.getQualifiers();
        }
      };

  private static final Function<ResourceInfo, Path> GET_RESOURCE_PATH =
      new Function<ResourceInfo, Path>() {
        @Override
        public Path apply(ResourceInfo info) {
          return info.getResource();
        }
      };

  /**
   * @param densities An array of string densities to use for filtering resources
   * @param out The path to use for name spacing the final resource directory.
   * @param working The path of the working directory for the filtering
   */
  public DensitySpecificResourceFilter(List<String> densities, Path out, Path working)
      throws MergingException {
    this.densities = densities;
    this.out = out;
    this.working = working;

    for (String density : densities) {
      if (!DENSITY_MAP.containsKey(density)) {
        throw MergingException.withMessage(density + " is not a known density qualifier.").build();
      }
    }
  }

  @VisibleForTesting
  List<Path> getResourceToRemove(List<Path> resourcePaths) {
    Predicate<ResourceInfo> requestedDensityFilter = new Predicate<ResourceInfo>() {
      @Override
      public boolean apply(@Nullable ResourceInfo info) {
        return !densities.contains(info.getDensity());
      }
    };

    List<ResourceInfo> resourceInfos = getResourceInfos(resourcePaths);
    List<ResourceInfo> densityResourceInfos = filterDensityResourceInfos(resourceInfos);
    List<ResourceInfo> resourceInfoToRemove = new ArrayList<>();

    Multimap<String, ResourceInfo> fileGroups = groupResourceInfos(densityResourceInfos,
        GET_RESOURCE_ID);

    for (String key : fileGroups.keySet()) {
      Multimap<String, ResourceInfo> qualifierGroups = groupResourceInfos(fileGroups.get(key),
          GET_RESOURCE_QUALIFIERS);

      for (String qualifiers : qualifierGroups.keySet()) {
        Collection<ResourceInfo> qualifierResourceInfos = qualifierGroups.get(qualifiers);

        if (qualifierResourceInfos.size() != 1) {
          List<ResourceInfo> sortedResourceInfos = Ordering.natural().onResultOf(
              new Function<ResourceInfo, Double>() {
                @Override
                public Double apply(ResourceInfo info) {
                  return matchScore(info, densities);
                }
              }).immutableSortedCopy(qualifierResourceInfos);
          resourceInfoToRemove.addAll(Collections2.filter(
              sortedResourceInfos.subList(1, sortedResourceInfos.size()),
              requestedDensityFilter));
        }
      }
    }

    return ImmutableList.copyOf(Lists.transform(resourceInfoToRemove, GET_RESOURCE_PATH));
  }

  private static void removeResources(List<Path> resourceInfoToRemove) {
    for (Path resource : resourceInfoToRemove) {
      resource.toFile().delete();
    }
  }

  private static Multimap<String, ResourceInfo> groupResourceInfos(
      final Collection<ResourceInfo> resourceInfos, Function<ResourceInfo, String> keyFunction) {
    Multimap<String, ResourceInfo> resourceGroups = ArrayListMultimap.create();

    for (ResourceInfo resourceInfo : resourceInfos) {
      resourceGroups.put(keyFunction.apply(resourceInfo), resourceInfo);
    }

    return ImmutableMultimap.copyOf(resourceGroups);
  }

  private static List<ResourceInfo> getResourceInfos(final List<Path> resourcePaths) {
    List<ResourceInfo> resourceInfos = new ArrayList<>();

    for (Path resourcePath : resourcePaths) {
      String qualifiers = resourcePath.getParent().getFileName().toString();
      String density = "";

      for (String densityName : DENSITY_MAP.keySet()) {
        if (qualifiers.contains("-" + densityName)) {
          qualifiers = qualifiers.replace("-" + densityName, "");
          density = densityName;
        }
      }

      String[] qualifierArray = qualifiers.split("-");
      String restype = qualifierArray[0];
      qualifiers = (qualifierArray.length) > 0 ? Joiner.on("-").join(Arrays.copyOfRange(
          qualifierArray, 1, qualifierArray.length)) : "";
      resourceInfos.add(new ResourceInfo(resourcePath, restype, qualifiers, density,
          resourcePath.getFileName().toString()));
    }

    return ImmutableList.copyOf(resourceInfos);
  }

  private static List<ResourceInfo> filterDensityResourceInfos(
      final List<ResourceInfo> resourceInfos) {
    List<ResourceInfo> densityResourceInfos = new ArrayList<>();

    for (ResourceInfo info : resourceInfos) {
      if (info.getRestype().equals("drawable") && !info.getDensity().equals("")
          && !info.getDensity().equals("nodpi") && !info.getResid().endsWith(".xml")) {
        densityResourceInfos.add(info);
      }
    }

    return ImmutableList.copyOf(densityResourceInfos);
  }

  private static double matchScore(ResourceInfo resource, List<String> densities) {
    double score = 0;
    for (String density : densities) {
      score += computeAffinity(DENSITY_MAP.get(resource.getDensity()), DENSITY_MAP.get(density));
    }
    return score;
  }

  private static double computeAffinity(int resourceDensity, int density) {
    if (resourceDensity == density) {
      // Exact match is the best.
      return -2;
    } else if (resourceDensity == 2 * density) {
      // It's very efficient to downsample an image that's exactly 2x the screen
      // density, so we prefer that over other non-perfect matches.
      return -1;
    } else {
      double affinity = Math.log((double) density / resourceDensity) / Math.log(2);

      // We give a slight bump to images that have the same multiplier but are higher quality.
      if (affinity < 0) {
        affinity = Math.abs(affinity) - 0.01;
      }
      return affinity;
    }
  }

  /** Filters the contents of a resource directory. */
  public Path filter(Path unFilteredResourceDir) {
    // no densities to filter, so skip.
    if (densities.isEmpty()) { 
      return unFilteredResourceDir;
    }
    final Path filteredResourceDir =
        out.resolve(working.relativize(unFilteredResourceDir));
    RecursiveFileCopier fileVisitor =
        new RecursiveFileCopier(filteredResourceDir, unFilteredResourceDir);
    try {
      Files.walkFileTree(unFilteredResourceDir, EnumSet.of(FileVisitOption.FOLLOW_LINKS),
          Integer.MAX_VALUE, fileVisitor);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    removeResources(getResourceToRemove(fileVisitor.getCopiedFiles()));
    return filteredResourceDir;
  }
}
