// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.util.indexing;

import com.google.common.hash.HashFunction;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileType.CharsetHintSupplied.CharsetHint.ForcedCharset;
import com.intellij.util.indexing.flavor.FileIndexingFlavorProvider;
import com.intellij.util.indexing.flavor.HashBuilder;
import com.intellij.util.io.DigestUtil;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;

@ApiStatus.Internal
public final class IndexedHashesSupport {
  // TODO replace with sha-256
  private static final HashFunction INDEXED_FILE_CONTENT_HASHER = Hashing.sha1();

  public static final int HASH_SIZE_IN_BYTES = INDEXED_FILE_CONTENT_HASHER.bits() / Byte.SIZE;

  public static int getVersion() {
    return 3;
  }

  public static byte @NotNull [] getOrInitIndexedHash(@NotNull FileContentImpl content) {
    byte[] hash = content.getIndexedFileHash();
    if (hash != null) return hash;
    byte[] contentHash = getBinaryContentHash(content.getContent());
    hash = calculateIndexedHash(content, contentHash);
    content.setIndexedFileHash(hash);
    return hash;
  }

  public static byte @NotNull [] getBinaryContentHash(byte @NotNull [] content) {
    // TODO: simplify to calculating content hash of only the content[].
    // Shared Indexes that are already available on CDN will have their hashes invalidated after it.
    MessageDigest digest = DigestUtil.sha1();
    digest.update(String.valueOf(content.length).getBytes(StandardCharsets.UTF_8));
    digest.update("\u0000".getBytes(StandardCharsets.UTF_8));
    digest.update(content);
    return digest.digest();
  }

  public static byte @NotNull [] calculateIndexedHash(@NotNull IndexedFile indexedFile, byte @NotNull [] contentHash) {
    Hasher hasher = INDEXED_FILE_CONTENT_HASHER.newHasher();
    hasher.putBytes(contentHash);

    if (!FileContentImpl.getFileTypeWithoutSubstitution(indexedFile).isBinary()) {
      FileType fileType = indexedFile.getFileType();
      FileType.CharsetHintSupplied.CharsetHint charsetHint =
        fileType instanceof FileType.CharsetHintSupplied ? ((FileType.CharsetHintSupplied)fileType).getCharsetHint() : null;
      // we don't need charset if it depends only on content
      if (charsetHint != FileType.CharsetHintSupplied.CONTENT_DEPENDENT_CHARSET) {
        Charset charset = charsetHint instanceof ForcedCharset
                          ? ((ForcedCharset)charsetHint).getCharset()
                          : getCharsetFromIndexedFile(indexedFile);
        hasher.putString(charset.name(), StandardCharsets.UTF_8);
      }
    }

    hasher.putString(indexedFile.getFileName(), StandardCharsets.UTF_8);

    FileType fileType = indexedFile.getFileType();
    hasher.putString(fileType.getName(), StandardCharsets.UTF_8);

    @Nullable
    FileIndexingFlavorProvider<?> provider = FileIndexingFlavorProvider.INSTANCE.forFileType(fileType);
    if (provider != null) {
      buildFlavorHash(indexedFile, provider, new HashBuilder() {
        @Override
        public @NotNull HashBuilder putInt(int val) {
          hasher.putInt(val);
          return this;
        }

        @Override
        public @NotNull HashBuilder putBoolean(boolean val) {
          hasher.putBoolean(val);
          return this;
        }

        @Override
        public @NotNull HashBuilder putString(@NotNull CharSequence charSequence) {
          hasher.putString(charSequence, StandardCharsets.UTF_8);
          return this;
        }
      });
    }

    return hasher.hash().asBytes();
  }

  @NotNull
  private static Charset getCharsetFromIndexedFile(@NotNull IndexedFile indexedFile) {
    return indexedFile instanceof FileContentImpl ? ((FileContentImpl)indexedFile).getCharset() : indexedFile.getFile().getCharset();
  }

  private static <F> void buildFlavorHash(@NotNull IndexedFile indexedFile,
                                          @NotNull FileIndexingFlavorProvider<F> flavorProvider,
                                          @NotNull HashBuilder hashBuilder) {
    F flavor = flavorProvider.getFlavor(indexedFile);
    hashBuilder.putString(flavorProvider.getId());
    hashBuilder.putInt(flavorProvider.getVersion());
    if (flavor != null) {
      flavorProvider.buildHash(flavor, hashBuilder);
    }
  }
}
@@ -430,24 +430,67 @@
 cedCharset;%0A
+import com.intellij.util.SystemProperties;%0A
 import com.i
@@ -944,16 +944,152 @@
 pport %7B%0A
+  private static final boolean SKIP_CONTENT_DEPENDENT_CHARSETS = SystemProperties.is(%22idea.index.hash.skip.content.dependent.charset%22);%0A
   // TOD
@@ -2888,16 +2888,52 @@
 _CHARSET
+ %7C%7C !SKIP_CONTENT_DEPENDENT_CHARSETS
 ) %7B%0A    

add an option to skip file type for content dependent charsets