/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * 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.intellij.psi.search;

import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.roots.TestSourcesFilter;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiBundle;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.scope.packageSet.*;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ObjectUtils;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;

public class GlobalSearchScopesCore {
  @NotNull
  public static GlobalSearchScope projectProductionScope(@NotNull Project project) {
    return new ProductionScopeFilter(project);
  }

  @NotNull
  public static GlobalSearchScope projectTestScope(@NotNull Project project) {
    return new TestScopeFilter(project);
  }

  @NotNull
  public static GlobalSearchScope directoryScope(@NotNull PsiDirectory directory, final boolean withSubdirectories) {
    return new DirectoryScope(directory, withSubdirectories);
  }

  @NotNull
  public static GlobalSearchScope directoryScope(@NotNull Project project, @NotNull VirtualFile directory, final boolean withSubdirectories) {
    return new DirectoryScope(project, directory, withSubdirectories);
  }

  @NotNull
  public static GlobalSearchScope directoriesScope(@NotNull Project project, boolean withSubdirectories, @NotNull VirtualFile... directories) {
    if (directories.length ==1) {
      return directoryScope(project, directories[0], withSubdirectories);
    }
    BitSet withSubdirectoriesBS = new BitSet(directories.length);
    if (withSubdirectories) {
      withSubdirectoriesBS.set(0, directories.length);
    }
    return new DirectoriesScope(project, directories, withSubdirectoriesBS);
  }

  public static GlobalSearchScope filterScope(@NotNull Project project, @NotNull NamedScope set) {
    return new FilterScopeAdapter(project, set);
  }

  private static class FilterScopeAdapter extends GlobalSearchScope {
    private final NamedScope mySet;
    private final PsiManager myManager;

    private FilterScopeAdapter(@NotNull Project project, @NotNull NamedScope set) {
      super(project);
      mySet = set;
      myManager = PsiManager.getInstance(project);
    }

    @Override
    public boolean contains(@NotNull VirtualFile file) {
      Project project = getProject();
      NamedScopesHolder holder = NamedScopeManager.getInstance(project);
      final PackageSet packageSet = mySet.getValue();
      if (packageSet != null) {
        if (packageSet instanceof PackageSetBase) return ((PackageSetBase)packageSet).contains(file, project, holder);
        PsiFile psiFile = myManager.findFile(file);
        return psiFile != null && packageSet.contains(psiFile, holder);
      }
      return false;
    }

    @NotNull
    @Override
    public String getDisplayName() {
      return mySet.getName();
    }

    @NotNull
    @Override
    public Project getProject() {
      //noinspection ConstantConditions
      return super.getProject();
    }

    @Override
    public int compare(@NotNull VirtualFile file1, @NotNull VirtualFile file2) {
      return 0;

    }

    @Override
    public boolean isSearchInModuleContent(@NotNull Module aModule) {
      return true; //TODO (optimization?)
    }

    @Override
    public boolean isSearchInLibraries() {
      return true; //TODO (optimization?)
    }
  }

  private static class ProductionScopeFilter extends GlobalSearchScope {
    private final ProjectFileIndex myFileIndex;

    private ProductionScopeFilter(@NotNull Project project) {
      super(project);
      myFileIndex = ProjectRootManager.getInstance(project).getFileIndex();
    }

    @Override
    public boolean contains(@NotNull VirtualFile file) {
      return myFileIndex.isInSourceContent(file) && !TestSourcesFilter.isTestSources(file, ObjectUtils.assertNotNull(getProject()));
    }

    @Override
    public int compare(@NotNull VirtualFile file1, @NotNull VirtualFile file2) {
      return 0;
    }

    @Override
    public boolean isSearchInModuleContent(@NotNull Module aModule) {
      return true;
    }

    @Override
    public boolean isSearchInModuleContent(@NotNull final Module aModule, final boolean testSources) {
      return !testSources;
    }

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

    @NotNull
    @Override
    public String getDisplayName() {
      return PsiBundle.message("psi.search.scope.production.files");
    }
  }

  private static class TestScopeFilter extends GlobalSearchScope {
    private TestScopeFilter(@NotNull Project project) {
      super(project);
    }

    @Override
    public boolean contains(@NotNull VirtualFile file) {
      return TestSourcesFilter.isTestSources(file, ObjectUtils.assertNotNull(getProject()));
    }

    @Override
    public int compare(@NotNull VirtualFile file1, @NotNull VirtualFile file2) {
      return 0;
    }

    @Override
    public boolean isSearchInModuleContent(@NotNull Module aModule) {
      return true;
    }

    @Override
    public boolean isSearchInModuleContent(@NotNull final Module aModule, final boolean testSources) {
      return testSources;
    }

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

    @NotNull
    @Override
    public String getDisplayName() {
      return PsiBundle.message("psi.search.scope.test.files");
    }
  }

  private static class DirectoryScope extends GlobalSearchScope {
    private final VirtualFile myDirectory;
    private final boolean myWithSubdirectories;

    private DirectoryScope(@NotNull PsiDirectory psiDirectory, final boolean withSubdirectories) {
      super(psiDirectory.getProject());
      myWithSubdirectories = withSubdirectories;
      myDirectory = psiDirectory.getVirtualFile();
    }

    private DirectoryScope(@NotNull Project project, @NotNull VirtualFile directory, final boolean withSubdirectories) {
      super(project);
      myWithSubdirectories = withSubdirectories;
      myDirectory = directory;
    }

    @Override
    public boolean contains(@NotNull VirtualFile file) {
      return myWithSubdirectories ? VfsUtilCore.isAncestor(myDirectory, file, false) : myDirectory.equals(file.getParent());
    }

    @Override
    public int compare(@NotNull VirtualFile file1, @NotNull VirtualFile file2) {
      return 0;
    }

    @Override
    public boolean isSearchInModuleContent(@NotNull Module aModule) {
      return true;
    }

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

    @Override
    public String toString() {
      //noinspection HardCodedStringLiteral
      return "directory scope: " + myDirectory + "; withSubdirs:"+myWithSubdirectories;
    }

    @Override
    public int hashCode() {
      return myDirectory.hashCode() *31 + (myWithSubdirectories?1:0);
    }

    @Override
    public boolean equals(Object obj) {
      return obj instanceof DirectoryScope &&
             myDirectory.equals(((DirectoryScope)obj).myDirectory) &&
             myWithSubdirectories == ((DirectoryScope)obj).myWithSubdirectories;
    }

    @NotNull
    @Override
    public GlobalSearchScope uniteWith(@NotNull GlobalSearchScope scope) {
      if (equals(scope)) return this;
      if (scope instanceof DirectoryScope) {
        DirectoryScope other = (DirectoryScope)scope;
        VirtualFile otherDirectory = other.myDirectory;
        if (myWithSubdirectories && VfsUtilCore.isAncestor(myDirectory, otherDirectory, false)) return this;
        if (other.myWithSubdirectories && VfsUtilCore.isAncestor(otherDirectory, myDirectory, false)) return other;
        BitSet newWithSubdirectories = new BitSet();
        newWithSubdirectories.set(0, myWithSubdirectories);
        newWithSubdirectories.set(1, other.myWithSubdirectories);
        return new DirectoriesScope(getProject(), new VirtualFile[]{myDirectory,otherDirectory}, newWithSubdirectories);
      }
      return super.uniteWith(scope);
    }

    @NotNull
    @Override
    public Project getProject() {
      //noinspection ConstantConditions
      return super.getProject();
    }

    @NotNull
    @Override
    public String getDisplayName() {
      return "Directory '" + myDirectory.getName() + "'";
    }
  }

  static class DirectoriesScope extends GlobalSearchScope {
    private final VirtualFile[] myDirectories;
    private final BitSet myWithSubdirectories;

    private DirectoriesScope(@NotNull Project project, @NotNull VirtualFile[] directories, @NotNull BitSet withSubdirectories) {
      super(project);
      myWithSubdirectories = withSubdirectories;
      myDirectories = directories;
      if (directories.length < 2) {
        throw new IllegalArgumentException("Expected >1 directories, but got: " + Arrays.asList(directories));
      }
    }

    @Override
    public boolean contains(@NotNull VirtualFile file) {
      VirtualFile parent = file.getParent();
      return parent != null && in(parent);
    }

    private boolean in(@NotNull VirtualFile parent) {
      for (int i = 0; i < myDirectories.length; i++) {
        VirtualFile directory = myDirectories[i];
        boolean withSubdirectories = myWithSubdirectories.get(i);
        if (withSubdirectories ? VfsUtilCore.isAncestor(directory, parent, false) : directory.equals(parent)) return true;
      }
      return false;
    }

    @Override
    public int compare(@NotNull VirtualFile file1, @NotNull VirtualFile file2) {
      return 0;
    }

    @Override
    public boolean isSearchInModuleContent(@NotNull Module aModule) {
      return true;
    }

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

    @Override
    public String toString() {
      //noinspection HardCodedStringLiteral
      return "Directories scope: " + Arrays.asList(myDirectories);
    }

    @Override
    public int hashCode() {
      int result = 1;
      for (int i = 0; i < myDirectories.length; i++) {
        VirtualFile directory = myDirectories[i];
        boolean withSubdirectories = myWithSubdirectories.get(i);
        result = result*31 + directory.hashCode() *31 + (withSubdirectories?1:0);
      }
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      return obj instanceof DirectoriesScope &&
             Arrays.equals(myDirectories, ((DirectoriesScope)obj).myDirectories) &&
             myWithSubdirectories.equals(((DirectoriesScope)obj).myWithSubdirectories);
    }

    @NotNull
    @Override
    public GlobalSearchScope uniteWith(@NotNull GlobalSearchScope scope) {
      if (equals(scope)) {
        return this;
      }
      if (scope instanceof DirectoryScope) {
        if (in(((DirectoryScope)scope).myDirectory)) {
          return this;
        }
        VirtualFile[] newDirectories = ArrayUtil.append(myDirectories, ((DirectoryScope)scope).myDirectory, VirtualFile.class);
        BitSet newWithSubdirectories = (BitSet)myWithSubdirectories.clone();
        newWithSubdirectories.set(myDirectories.length, ((DirectoryScope)scope).myWithSubdirectories);
        return new DirectoriesScope(getProject(), newDirectories, newWithSubdirectories);
      }
      if (scope instanceof DirectoriesScope) {
        DirectoriesScope other = (DirectoriesScope)scope;
        List<VirtualFile> newDirectories = new ArrayList<>(myDirectories.length + other.myDirectories.length);
        newDirectories.addAll(Arrays.asList(myDirectories));
        BitSet newWithSubdirectories = (BitSet)myWithSubdirectories.clone();
        VirtualFile[] otherDirectories = other.myDirectories;
        for (int i = 0; i < otherDirectories.length; i++) {
          VirtualFile otherDirectory = otherDirectories[i];
          if (!in(otherDirectory)) {
            newWithSubdirectories.set(newDirectories.size(), other.myWithSubdirectories.get(i));
            newDirectories.add(otherDirectory);
          }
        }
        return new DirectoriesScope(getProject(), newDirectories.toArray(new VirtualFile[newDirectories.size()]), newWithSubdirectories);
      }
      return super.uniteWith(scope);
    }

    @NotNull
    @Override
    public Project getProject() {
      //noinspection ConstantConditions
      return super.getProject();
    }

    @NotNull
    @Override
    public String getDisplayName() {
      if (myDirectories.length == 1) {
        VirtualFile root = myDirectories[0];
        return "Directory '" + root.getName() + "'";
      }
      return "Directories " + StringUtil.join(myDirectories, file -> "'" + file.getName() + "'", ", ");
    }

  }
}
