package com.cedricziel.idea.typo3.index;

import com.cedricziel.idea.typo3.util.FilesystemUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.CachedValue;
import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.psi.util.PsiModificationTracker;
import com.intellij.util.indexing.*;
import com.intellij.util.io.EnumeratorStringDescriptor;
import com.intellij.util.io.KeyDescriptor;
import gnu.trove.THashMap;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import static com.cedricziel.idea.typo3.util.ComposerUtil.findExtensionKey;

public class ResourcePathIndex extends ScalarIndexExtension<String> {

    public static final ID<String, Void> KEY = ID.create("com.cedricziel.idea.typo3.index.resource_path");

    private static final Key<CachedValue<Collection<String>>> RESOURCE_KEYS = new Key<>("TYPO3_CMS_RESOURCE_KEYS");

    private static final ConcurrentMap<Project, Collection<String>> RESOURCE_KEYS_LOCAL_CACHE = new ConcurrentHashMap<>();

    public static Collection<String> getAvailableExtensionResourceFiles(@NotNull Project project) {
        return getAllResourceKeys(project);
    }

    public static boolean projectContainsResourceFile(@NotNull Project project, @NotNull String resourceId) {
        return getAllResourceKeys(project).contains(resourceId);
    }

    @NotNull
    private synchronized static Collection<String> getAllResourceKeys(@NotNull Project project) {
        CachedValue<Collection<String>> userData = project.getUserData(RESOURCE_KEYS);
        if (userData != null && userData.hasUpToDateValue()) {
            return RESOURCE_KEYS_LOCAL_CACHE.getOrDefault(project, new ArrayList<>());
        }

        CachedValue<Collection<String>> cachedValue = CachedValuesManager.getManager(project).createCachedValue(() -> {
            Collection<String> allKeys = FileBasedIndex.getInstance().getAllKeys(ResourcePathIndex.KEY, project);
            if (RESOURCE_KEYS_LOCAL_CACHE.containsKey(project)) {
                RESOURCE_KEYS_LOCAL_CACHE.replace(project, allKeys);
            } else {
                RESOURCE_KEYS_LOCAL_CACHE.put(project, allKeys);
            }

            return CachedValueProvider.Result.create(new ArrayList<>(), PsiModificationTracker.MODIFICATION_COUNT);
        }, false);
        project.putUserData(RESOURCE_KEYS, cachedValue);

        return RESOURCE_KEYS_LOCAL_CACHE.getOrDefault(project, cachedValue.getValue());
    }

    public static boolean projectContainsResourceDirectory(@NotNull Project project, @NotNull String resourceId) {
        String interpolatedKey = StringUtils.strip(resourceId, "/") + "/";

        return FileBasedIndex.getInstance().getAllKeys(ResourcePathIndex.KEY, project)
            .parallelStream()
            .anyMatch(x -> x.startsWith(interpolatedKey));
    }

    public static PsiElement[] findElementsForKey(@NotNull Project project, @NotNull String identifier) {
        Set<String> keys = new HashSet<>();
        keys.add(identifier);
        Set<PsiElement> elements = new HashSet<>();

        FileBasedIndex.getInstance().getFilesWithKey(ResourcePathIndex.KEY, keys, virtualFile -> {
            elements.add(PsiManager.getInstance(project).findFile(virtualFile));

            return true;
        }, GlobalSearchScope.allScope(project));

        return elements
            .stream()
            .filter(Objects::nonNull)
            .toArray(PsiElement[]::new);
    }

    @NotNull
    @Override
    public ID<String, Void> getName() {
        return KEY;
    }

    @NotNull
    @Override
    public DataIndexer<String, Void, FileContent> getIndexer() {
        return inputData -> {
            Map<String, Void> map = new THashMap<>();

            String path = inputData.getFile().getPath();
            if (path.contains("sysext") || path.contains("typo3conf/ext")) {
                map.putAll(compileId(inputData));

                return map;
            }

            VirtualFile extensionRootFolder = FilesystemUtil.findExtensionRootFolder(inputData.getFile());
            if (extensionRootFolder != null) {
                // 1. try to read sibling composer.json
                VirtualFile composerJsonFile = extensionRootFolder.findChild("composer.json");
                if (composerJsonFile != null) {
                    String extensionKey = findExtensionKey(composerJsonFile);
                    if (extensionKey != null) {
                        map.putAll(compileId(extensionRootFolder, extensionKey, inputData.getFile()));
                        return map;
                    }
                }

                // 2. try to infer from directory name
                map.putAll(compileId(extensionRootFolder.getName(), extensionRootFolder.getPath(), inputData.getFile()));
            }

            return map;
        };
    }

    private Map<String, Void> compileId(FileContent inputData) {
        Map<String, Void> map = new HashMap<>();
        String path = inputData.getFile().getPath();
        String filePosition = "";
        if (path.contains("typo3conf/ext/")) {
            filePosition = path.split("typo3conf/ext/")[1];
        }
        if (path.contains("sysext/")) {
            filePosition = path.split("sysext/")[1];
        }

        String primaryKey = "EXT:" + filePosition;

        putSecondaryKeyIfNeeded(map, primaryKey);

        return map;
    }

    private void putSecondaryKeyIfNeeded(Map<String, Void> map, String primaryKey) {
        map.put(primaryKey, null);
        if (primaryKey.endsWith(".xlf") || primaryKey.endsWith(".xml")) {
            map.put("LLL:" + primaryKey, null);
        }
    }

    private Map<String, Void> compileId(String extensionKey, String directoryPath, VirtualFile file) {
        Map<String, Void> map = new HashMap<>();
        String primaryKey = "EXT:" + extensionKey + file.getPath().replace(directoryPath, "");

        putSecondaryKeyIfNeeded(map, primaryKey);

        return map;
    }

    private Map<String, Void> compileId(VirtualFile extensionRootDirectory, String extensionKey, VirtualFile file) {
        Map<String, Void> map = new HashMap<>();
        String primaryKey = "EXT:" + extensionKey + file.getPath().replace(extensionRootDirectory.getPath(), "");

        putSecondaryKeyIfNeeded(map, primaryKey);

        return map;
    }

    @NotNull
    @Override
    public KeyDescriptor<String> getKeyDescriptor() {
        return EnumeratorStringDescriptor.INSTANCE;
    }

    @Override
    public int getVersion() {
        return 2;
    }

    @NotNull
    @Override
    public FileBasedIndex.InputFilter getInputFilter() {
        return VirtualFile::isInLocalFileSystem;
    }

    @Override
    public boolean dependsOnFileContent() {
        return false;
    }
}