/*
 * Copyright 2003-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 jetbrains.mps.extapi.persistence;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.annotations.Immutable;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;

import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;

/**
 * Maps {@link SourceRootKind} to {@link SourceRoot}.
 * Preserves the order of the <code>SourcePath</code>s.
 */
final class SourcePaths {
  private final Map<SourceRootKind, List<SourceRoot>> myFileKind2SourcePaths = new LinkedHashMap<>();
  @NotNull private final Predicate<SourceRootKind> isFileKindAllowed;

  public SourcePaths() {
    this((kind) -> true); // always allows
  }

  public SourcePaths(@NotNull Predicate<SourceRootKind> isFileKindAllowed0) {
    isFileKindAllowed = isFileKindAllowed0;
  }

  /**
   * @return an empty list if the kind is not registered.
   */
  @NotNull
  @Immutable
  public synchronized List<SourceRoot> getByKind(@NotNull SourceRootKind kind) {
    List<SourceRoot> list = myFileKind2SourcePaths.get(kind);
    return list != null ? unmodifiableList(list) : emptyList();
  }

  /**
   * Adds source roots to the container
   *
   * @throws FileKindIsNotAllowedException if provided file kind is not allowed
   * @throws SourceRootAlreadyExistsException if such source path already exists
   */
  public synchronized void addSourceRoot(@NotNull SourceRootKind kind, @NotNull SourceRoot root) {
    if (!isKindAllowed(kind)) {
      throw new FileKindIsNotAllowedException(kind, root);
    }
    SourceRootKind existingRootKind = getKind(root);
    if (existingRootKind != null) {
      throw new SourceRootAlreadyExistsException(root, kind, root, existingRootKind);
    }
    myFileKind2SourcePaths.putIfAbsent(kind, new ArrayList<>());
    myFileKind2SourcePaths.get(kind).add(root);
  }

  private boolean isKindAllowed(@NotNull SourceRootKind kind) {
    return isFileKindAllowed.test(kind);
  }

  @Nullable
  private SourceRootKind getKind(@NotNull SourceRoot root) {
    for (SourceRootKind kind : myFileKind2SourcePaths.keySet()) {
      List<SourceRoot> sourceRoots = myFileKind2SourcePaths.get(kind);
      if (sourceRoots.contains(root)) {
        return kind;
      }
    }
    return null;
  }

  @Nullable
  public SourceRootKind removeSourceRoot(SourceRoot root) {
    for (SourceRootKind kind : myFileKind2SourcePaths.keySet()) {
      List<SourceRoot> sourceRoots = myFileKind2SourcePaths.get(kind);
      if (sourceRoots.contains(root)) {
        sourceRoots.remove(root);
        return kind;
      }
    }
    return null;
  }

  public void clearAll() {
    myFileKind2SourcePaths.clear();
  }

  @Override
  public int hashCode() {
    return Objects.hash(myFileKind2SourcePaths);
  }

  @Override
  public boolean equals(Object obj) {
    if (obj instanceof SourcePaths) {
      return Objects.equals(myFileKind2SourcePaths, ((SourcePaths) obj).myFileKind2SourcePaths);
    }
    return false;
  }

  @Override
  public String toString() {
    return "SourcePaths [" + myFileKind2SourcePaths.values().size() + " source root paths]";
  }

  public static final class SourceRootAlreadyExistsException extends IllegalStateException {
    private final SourceRoot myRoot;
    private final SourceRootKind myPathKind;
    private final SourceRoot myExistingRoot;
    private final SourceRootKind myExistingRootKind;

    public SourceRootAlreadyExistsException(SourceRoot root, SourceRootKind pathKind,
                                            SourceRoot existingRoot, SourceRootKind existingPathKind) {
      myRoot = root;
      myPathKind = pathKind;
      myExistingRoot = existingRoot;
      myExistingRootKind = existingPathKind;
    }

    @Override
    @NotNull
    public String getMessage() {
      return MessageFormat.format("Trying to register the source root: [{1}, kind {2}] whilst [{3}, kind {4}] " +
                                  "is already registered", myRoot, myPathKind, myExistingRoot, myExistingRootKind);
    }
  }
}
