/*
 * Copyright 2000-2017 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.openapi.roots.impl;

import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.roots.ContentEntry;
import com.intellij.openapi.roots.ContentFolder;
import com.intellij.openapi.roots.ExcludeFolder;
import com.intellij.openapi.roots.SourceFolder;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager;
import com.intellij.util.ArrayUtil;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jps.model.JpsElement;
import org.jetbrains.jps.model.JpsElementFactory;
import org.jetbrains.jps.model.java.JavaSourceRootType;
import org.jetbrains.jps.model.module.JpsModuleSourceRoot;
import org.jetbrains.jps.model.module.JpsModuleSourceRootType;
import org.jetbrains.jps.model.serialization.module.JpsModuleRootModelSerializer;

import java.util.*;

/**
 *  @author dsl
 */
public class ContentEntryImpl extends RootModelComponentBase implements ContentEntry, ClonableContentEntry, Comparable<ContentEntryImpl> {
  private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.roots.impl.SimpleContentEntryImpl");
  @NotNull private final VirtualFilePointer myRoot;
  @NonNls public static final String ELEMENT_NAME = JpsModuleRootModelSerializer.CONTENT_TAG;
  private final Set<SourceFolder> mySourceFolders = new LinkedHashSet<>();
  private final Set<ExcludeFolder> myExcludeFolders = new TreeSet<>(ContentFolderComparator.INSTANCE);
  @NonNls public static final String URL_ATTRIBUTE = JpsModuleRootModelSerializer.URL_ATTRIBUTE;

  ContentEntryImpl(@NotNull VirtualFile file, @NotNull RootModelImpl m) {
    this(file.getUrl(), m);
  }

  ContentEntryImpl(@NotNull String url, @NotNull RootModelImpl m) {
    super(m);
    myRoot = VirtualFilePointerManager.getInstance().create(url, this, null);
  }

  ContentEntryImpl(@NotNull Element e, @NotNull RootModelImpl m) throws InvalidDataException {
    this(getUrlFrom(e), m);
    initSourceFolders(e);
    initExcludeFolders(e);
  }

  private static String getUrlFrom(@NotNull Element e) throws InvalidDataException {
    LOG.assertTrue(ELEMENT_NAME.equals(e.getName()));

    String url = e.getAttributeValue(URL_ATTRIBUTE);
    if (url == null) throw new InvalidDataException();
    return url;
  }

  private void initSourceFolders(@NotNull Element e) throws InvalidDataException {
    for (Element child : e.getChildren(SourceFolderImpl.ELEMENT_NAME)) {
      addSourceFolder(new SourceFolderImpl(child, this));
    }
  }

  private void initExcludeFolders(@NotNull Element e) throws InvalidDataException {
    for (Element child : e.getChildren(ExcludeFolderImpl.ELEMENT_NAME)) {
      ExcludeFolderImpl excludeFolder = new ExcludeFolderImpl(child, this);
      addExcludeFolder(excludeFolder);
    }
  }

  @Override
  public VirtualFile getFile() {
    //assert !isDisposed();
    return myRoot.getFile();
  }

  @Override
  @NotNull
  public String getUrl() {
    return myRoot.getUrl();
  }

  @NotNull
  @Override
  public SourceFolder[] getSourceFolders() {
    return mySourceFolders.toArray(new SourceFolder[mySourceFolders.size()]);
  }

  @NotNull
  @Override
  public List<SourceFolder> getSourceFolders(@NotNull JpsModuleSourceRootType<?> rootType) {
    return getSourceFolders(Collections.singleton(rootType));
  }

  @NotNull
  @Override
  public List<SourceFolder> getSourceFolders(@NotNull Set<? extends JpsModuleSourceRootType<?>> rootTypes) {
    SmartList<SourceFolder> folders = new SmartList<>();
    for (SourceFolder folder : mySourceFolders) {
      if (rootTypes.contains(folder.getRootType())) {
        folders.add(folder);
      }
    }
    return folders;
  }

  @Override
  @NotNull
  public VirtualFile[] getSourceFolderFiles() {
    assert !isDisposed();
    final SourceFolder[] sourceFolders = getSourceFolders();
    ArrayList<VirtualFile> result = new ArrayList<>(sourceFolders.length);
    for (SourceFolder sourceFolder : sourceFolders) {
      final VirtualFile file = sourceFolder.getFile();
      if (file != null) {
        result.add(file);
      }
    }
    return VfsUtilCore.toVirtualFileArray(result);
  }

  @NotNull
  @Override
  public ExcludeFolder[] getExcludeFolders() {
    //assert !isDisposed();
    return myExcludeFolders.toArray(new ExcludeFolder[myExcludeFolders.size()]);
  }

  @NotNull
  @Override
  public List<String> getExcludeFolderUrls() {
    List<String> excluded = new ArrayList<>();
    for (ExcludeFolder folder : myExcludeFolders) {
      excluded.add(folder.getUrl());
    }
    for (DirectoryIndexExcludePolicy excludePolicy : Extensions.getExtensions(DirectoryIndexExcludePolicy.EP_NAME, getRootModel().getProject())) {
      for (VirtualFilePointer pointer : excludePolicy.getExcludeRootsForModule(getRootModel())) {
        excluded.add(pointer.getUrl());
      }
    }
    return excluded;
  }

  @Override
  @NotNull
  public VirtualFile[] getExcludeFolderFiles() {
    assert !isDisposed();
    ArrayList<VirtualFile> result = new ArrayList<>();
    for (ExcludeFolder excludeFolder : getExcludeFolders()) {
      ContainerUtil.addIfNotNull(result, excludeFolder.getFile());
    }
    for (DirectoryIndexExcludePolicy excludePolicy : Extensions.getExtensions(DirectoryIndexExcludePolicy.EP_NAME, getRootModel().getProject())) {
      for (VirtualFilePointer pointer : excludePolicy.getExcludeRootsForModule(getRootModel())) {
        ContainerUtil.addIfNotNull(result, pointer.getFile());
      }
    }
    return VfsUtilCore.toVirtualFileArray(result);
  }

  @NotNull
  @Override
  public SourceFolder addSourceFolder(@NotNull VirtualFile file, boolean isTestSource) {
    return addSourceFolder(file, isTestSource, SourceFolderImpl.DEFAULT_PACKAGE_PREFIX);
  }

  @NotNull
  @Override
  public SourceFolder addSourceFolder(@NotNull VirtualFile file, boolean isTestSource, @NotNull String packagePrefix) {
    JavaSourceRootType type = isTestSource ? JavaSourceRootType.TEST_SOURCE : JavaSourceRootType.SOURCE;
    return addSourceFolder(file, type);
  }

  @Override
  @NotNull
  public <P extends JpsElement> SourceFolder addSourceFolder(@NotNull VirtualFile file, @NotNull JpsModuleSourceRootType<P> type,
                                                             @NotNull P properties) {
    assertCanAddFolder(file);
    return addSourceFolder(new SourceFolderImpl(file, JpsElementFactory.getInstance().createModuleSourceRoot(file.getUrl(), type, properties), this));
  }

  @NotNull
  @Override
  public <P extends JpsElement> SourceFolder addSourceFolder(@NotNull VirtualFile file, @NotNull JpsModuleSourceRootType<P> type) {
    return addSourceFolder(file, type, type.createDefaultProperties());
  }

  @NotNull
  @Override
  public SourceFolder addSourceFolder(@NotNull String url, boolean isTestSource) {
    return addSourceFolder(url, isTestSource ? JavaSourceRootType.TEST_SOURCE : JavaSourceRootType.SOURCE);
  }

  @NotNull
  @Override
  public <P extends JpsElement> SourceFolder addSourceFolder(@NotNull String url, @NotNull JpsModuleSourceRootType<P> type) {
    return addSourceFolder(url, type, type.createDefaultProperties());
  }

  @NotNull
  @Override
  public <P extends JpsElement> SourceFolder addSourceFolder(@NotNull String url,
                                                             @NotNull JpsModuleSourceRootType<P> type,
                                                             @NotNull P properties) {
    assertFolderUnderMe(url);
    JpsModuleSourceRoot sourceRoot = JpsElementFactory.getInstance().createModuleSourceRoot(url, type, properties);
    return addSourceFolder(new SourceFolderImpl(sourceRoot, this));
  }

  @NotNull
  private SourceFolder addSourceFolder(@NotNull SourceFolderImpl f) {
    mySourceFolders.add(f);
    Disposer.register(this, f); //rewire source folder dispose parent from root model to this content root
    return f;
  }

  @Override
  public void removeSourceFolder(@NotNull SourceFolder sourceFolder) {
    assert !isDisposed();
    assertCanRemoveFrom(sourceFolder, mySourceFolders);
    doRemove(sourceFolder);
  }

  private void doRemove(SourceFolder sourceFolder) {
    mySourceFolders.remove(sourceFolder);
    Disposer.dispose((Disposable)sourceFolder);
  }

  @Override
  public void clearSourceFolders() {
    assert !isDisposed();
    getRootModel().assertWritable();
    for (SourceFolder folder : mySourceFolders) {
      Disposer.dispose((Disposable)folder);
    }
    mySourceFolders.clear();
  }

  @Override
  public ExcludeFolder addExcludeFolder(@NotNull VirtualFile file) {
    assert !isDisposed();
    assertCanAddFolder(file);
    return addExcludeFolder(new ExcludeFolderImpl(file, this));
  }

  @Override
  public ExcludeFolder addExcludeFolder(@NotNull String url) {
    assert !isDisposed();
    assertCanAddFolder(url);
    return addExcludeFolder(new ExcludeFolderImpl(url, this));
  }

  private void assertCanAddFolder(@NotNull VirtualFile file) {
    assertCanAddFolder(file.getUrl());
  }

  private void assertCanAddFolder(@NotNull String url) {
    getRootModel().assertWritable();
    assertFolderUnderMe(url);
  }

  @Override
  public void removeExcludeFolder(@NotNull ExcludeFolder excludeFolder) {
    assert !isDisposed();
    assertCanRemoveFrom(excludeFolder, myExcludeFolders);
    myExcludeFolders.remove(excludeFolder);
    Disposer.dispose((Disposable)excludeFolder);
  }

  @Override
  public boolean removeExcludeFolder(@NotNull String url) {
    for (ExcludeFolder folder : myExcludeFolders) {
      if (folder.getUrl().equals(url)) {
        myExcludeFolders.remove(folder);
        Disposer.dispose((Disposable)folder);
        return true;
      }
    }
    return false;
  }

  @Override
  public void clearExcludeFolders() {
    assert !isDisposed();
    getRootModel().assertWritable();
    for (ExcludeFolder excludeFolder : myExcludeFolders) {
      Disposer.dispose((Disposable)excludeFolder);
    }
    myExcludeFolders.clear();
  }

  private ExcludeFolder addExcludeFolder(ExcludeFolder f) {
    Disposer.register(this, (Disposable)f);
    myExcludeFolders.add(f);
    return f;
  }

  private <T extends ContentFolder> void assertCanRemoveFrom(T f, @NotNull Set<T> ff) {
    getRootModel().assertWritable();
    LOG.assertTrue(ff.contains(f));
  }

  private void assertFolderUnderMe(@NotNull String url) {
    final String path = VfsUtilCore.urlToPath(url);
    final String rootPath = VfsUtilCore.urlToPath(getUrl());
    if (!FileUtil.isAncestor(rootPath, path, false)) {
      LOG.error("The file '" + path + "' is not under content entry root '" + rootPath + "'");
    }
  }

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

  @Override
  @NotNull
  public ContentEntry cloneEntry(@NotNull RootModelImpl rootModel) {
    assert !isDisposed();
    ContentEntryImpl cloned = new ContentEntryImpl(myRoot.getUrl(), rootModel);
    for (final SourceFolder sourceFolder : mySourceFolders) {
      if (sourceFolder instanceof ClonableContentFolder) {
        ContentFolder folder = ((ClonableContentFolder)sourceFolder).cloneFolder(cloned);
        cloned.addSourceFolder((SourceFolderImpl)folder);
      }
    }

    for (final ExcludeFolder excludeFolder : myExcludeFolders) {
      if (excludeFolder instanceof ClonableContentFolder) {
        ContentFolder folder = ((ClonableContentFolder)excludeFolder).cloneFolder(cloned);
        cloned.addExcludeFolder((ExcludeFolder)folder);
      }
    }

    return cloned;
  }

  public void writeExternal(@NotNull Element element) throws WriteExternalException {
    assert !isDisposed();
    LOG.assertTrue(ELEMENT_NAME.equals(element.getName()));
    element.setAttribute(URL_ATTRIBUTE, myRoot.getUrl());
    for (final SourceFolder sourceFolder : mySourceFolders) {
      if (sourceFolder instanceof SourceFolderImpl) {
        JpsModuleRootModelSerializer.saveSourceRoot(element, sourceFolder.getUrl(), sourceFolder.getJpsElement().asTyped());
      }
    }

    for (final ExcludeFolder excludeFolder : myExcludeFolders) {
      if (excludeFolder instanceof ExcludeFolderImpl) {
        final Element subElement = new Element(ExcludeFolderImpl.ELEMENT_NAME);
        ((ExcludeFolderImpl)excludeFolder).writeExternal(subElement);
        element.addContent(subElement);
      }
    }
  }

  private static final class ContentFolderComparator implements Comparator<ContentFolder> {
    public static final ContentFolderComparator INSTANCE = new ContentFolderComparator();

    @Override
    public int compare(@NotNull ContentFolder o1, @NotNull ContentFolder o2) {
      return o1.getUrl().compareTo(o2.getUrl());
    }
  }

  @Override
  public int compareTo(@NotNull ContentEntryImpl other) {
    int i = getUrl().compareTo(other.getUrl());
    if (i != 0) return i;
    i = ArrayUtil.lexicographicCompare(getSourceFolders(), other.getSourceFolders());
    if (i != 0) return i;
    return ArrayUtil.lexicographicCompare(getExcludeFolders(), other.getExcludeFolders());
  }
}
