/*
 * Copyright 2014 The Kythe 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.kythe.extractors.shared;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.StandardSystemProperty.USER_DIR;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.hash.Hashing.sha256;
import static java.util.stream.Collectors.toCollection;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Ordering;
import com.google.common.collect.Streams;
import com.google.common.io.Files;
import com.google.common.util.concurrent.SettableFuture;
import com.google.devtools.kythe.proto.Analysis.CompilationUnit;
import com.google.devtools.kythe.proto.Analysis.CompilationUnit.FileInput;
import com.google.devtools.kythe.proto.Analysis.FileData;
import com.google.devtools.kythe.proto.Analysis.FileInfo;
import com.google.devtools.kythe.proto.Storage.VName;
import com.google.protobuf.ByteString;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;

/**
 * A class containing common utilities used by language extractors.
 *
 * <p>This class is a bit of a mishmash. It includes helpers for:
 *
 * <ul>
 *   <li>dealing with Google 3 specific file paths
 *   <li>loading files into FileData
 *   <li>converting from byte[] to FileData
 * </ul>
 */
// TODO: Split this class by domain.
public class ExtractorUtils {
  public static final Comparator<FileInput> FILE_INPUT_COMPARATOR =
      Comparator.comparing(FileInput::getInfo, Comparator.comparing(FileInfo::getPath));

  /**
   * Creates fully populated FileInput protocol buffers based on a provided set of files.
   *
   * @param filePathToFileDatas map with file contents.
   * @return fully populated FileInput protos
   * @throws ExtractionException
   */
  public static List<FileData> convertBytesToFileDatas(
      final Map<String, byte[]> filePathToFileContents) throws ExtractionException {
    checkNotNull(filePathToFileContents);

    return filePathToFileContents.keySet().stream()
        .map(path -> createFileData(path, filePathToFileContents.get(path)))
        .collect(toCollection(ArrayList::new));
  }

  public static FileData createFileData(String path, byte[] content) {
    return createFileData(path, getContentDigest(content), content);
  }

  public static List<FileData> processRequiredInputs(Iterable<String> files)
      throws ExtractionException {
    final SettableFuture<ExtractionException> exception = SettableFuture.create();

    List<FileData> result =
        Streams.stream(files)
            .map(
                path -> {
                  byte[] content = new byte[0];
                  try {
                    content = Files.toByteArray(new File(path));
                  } catch (IOException e) {
                    exception.set(new ExtractionException(e, false));
                  }
                  if (content == null) {
                    exception.set(
                        new ExtractionException(
                            String.format("Unable to locate required input %s", path), false));
                    return null;
                  }
                  String digest = getContentDigest(content);
                  return createFileData(path, digest, content);
                })
            .collect(toCollection(ArrayList::new));
    if (exception.isDone()) {
      try {
        throw exception.get();
      } catch (InterruptedException e) {
        throw new ExtractionException(e, true);
      } catch (ExecutionException e) {
        throw new ExtractionException(e, false);
      }
    }
    return result;
  }

  public static List<FileInput> toFileInputs(Iterable<FileData> fileDatas) {
    return toFileInputs(FileVNames.staticCorpus(""), p -> p, fileDatas);
  }

  public static Function<String, String> makeRelativizer(final Path rootDir) {
    return p -> tryMakeRelative(rootDir, Paths.get(p));
  }

  public static List<FileInput> toFileInputs(
      FileVNames fileVNames, Function<String, String> relativize, Iterable<FileData> fileDatas) {
    return Streams.stream(fileDatas)
        .map(
            fileData -> {
              VName vname = lookupVName(fileVNames, relativize, fileData.getInfo().getPath());
              return FileInput.newBuilder().setInfo(fileData.getInfo()).setVName(vname).build();
            })
        .sorted(FILE_INPUT_COMPARATOR)
        .collect(toImmutableList());
  }

  public static VName lookupVName(
      FileVNames fileVNames, Function<String, String> relativize, String path) {
    String relativePath = relativize.apply(path);
    VName vname = fileVNames.lookupBaseVName(relativePath);
    if (vname.getPath().isEmpty()) {
      vname = vname.toBuilder().setPath(relativePath).build();
    }
    return vname;
  }

  /** Tries to make a path relative to a root directory. Returns the fullpath otherwise. */
  public static String tryMakeRelative(String rootDir, String path) {
    return tryMakeRelative(Paths.get(rootDir), Paths.get(path));
  }

  /** Tries to make a path relative to a root directory. Returns the fullpath otherwise. */
  public static String tryMakeRelative(Path rootDir, Path path) {
    Path absRoot = rootDir.toAbsolutePath().normalize();
    Path absPath = path.toAbsolutePath().normalize();
    Path relPath = absRoot.relativize(absPath).normalize();
    if (relPath.toString().isEmpty()) {
      return ".";
    }
    return (relPath.startsWith("..") ? absPath : relPath).toString();
  }

  public static String getCurrentWorkingDirectory() {
    return USER_DIR.value();
  }

  public static String digestForPath(String path) throws NoSuchAlgorithmException, IOException {
    return digestForFile(new File(path));
  }

  public static String digestForFile(File file) throws NoSuchAlgorithmException, IOException {
    return getContentDigest(Files.toByteArray(file));
  }

  /** Computes a digest over the contents of a file. */
  @VisibleForTesting
  public static String getContentDigest(byte[] content) {
    return sha256().hashBytes(content).toString();
  }

  public static CompilationUnit normalizeCompilationUnit(CompilationUnit existingCompilationUnit) {
    CompilationUnit.Builder builder = CompilationUnit.newBuilder(existingCompilationUnit);
    List<FileInput> oldRequiredInputs =
        Ordering.from(FILE_INPUT_COMPARATOR).sortedCopy(builder.getRequiredInputList());
    builder.clearRequiredInput();
    builder.addAllRequiredInput(oldRequiredInputs);
    existingCompilationUnit = builder.build();
    return existingCompilationUnit;
  }

  private static FileData createFileData(String path, String digest, byte[] content) {
    return FileData.newBuilder()
        .setContent(ByteString.copyFrom(content))
        .setInfo(FileInfo.newBuilder().setDigest(digest).setPath(path))
        .build();
  }
}
