package com.airlock.iam.base.infrastructure.factory.guice;

import com.google.inject.Binding;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.Stage;
import com.google.inject.spi.DefaultElementVisitor;
import com.google.inject.spi.Elements;
import com.google.inject.spi.ExposedBinding;
import com.google.inject.spi.PrivateElements;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.NonNull;

public final class GuicePrivateElements {
   @NonNull
   private final Collection<PrivateElements> collection;

   public static GuicePrivateElements privateElements(Injector injector) {
      return new GuicePrivateElements(
         allExposedBindingsOf(injector).stream().<PrivateElements>map(ExposedBinding::getPrivateElements).collect(Collectors.toUnmodifiableSet())
      );
   }

   public static GuicePrivateElements privateElements(Iterable<? extends Module> modules) {
      Set<PrivateElements> set = new HashSet<>();
      Elements.getElements(Stage.PRODUCTION, modules).forEach(element -> element.acceptVisitor(new DefaultElementVisitor<Void>() {
         public Void visit(PrivateElements privateElements) {
            set.add(privateElements);
            return null;
         }
      }));
      return new GuicePrivateElements(Collections.unmodifiableSet(set));
   }

   public static GuicePrivateElements privateElements(Module... modules) {
      return privateElements(Arrays.asList(modules));
   }

   private static Set<ExposedBinding<?>> allExposedBindingsOf(Injector injector) {
      Set<ExposedBinding<?>> bindings = exposedBindingsOf(injector);

      for (Injector parent = injector.getParent(); parent != null; parent = parent.getParent()) {
         bindings.addAll(exposedBindingsOf(parent));
      }

      return Collections.unmodifiableSet(bindings);
   }

   private static Set<ExposedBinding<?>> exposedBindingsOf(Injector injector) {
      return injector.getAllBindings()
         .values()
         .stream()
         .filter(ExposedBinding.class::isInstance)
         .map(binding -> (ExposedBinding)binding)
         .collect(Collectors.toCollection(HashSet::new));
   }

   Stream<Binding<?>> streamAllUnexposedBindings() {
      return this.collection.stream().map(GuicePrivateElements::unexposedBindings).flatMap(Collection::stream);
   }

   private static Set<Binding<?>> unexposedBindings(PrivateElements privateElements) {
      return privateElements.getElements()
         .stream()
         .filter(Binding.class::isInstance)
         .map(binding -> (Binding)binding)
         .filter(binding -> isNotExposed(privateElements, (Binding<?>)binding))
         .collect(Collectors.toUnmodifiableSet());
   }

   Stream<Binding<?>> streamAllBindings() {
      return this.collection
         .stream()
         .map(PrivateElements::getElements)
         .flatMap(Collection::stream)
         .filter(Binding.class::isInstance)
         .map(binding -> (Binding<?>)binding);
   }

   private static boolean isNotExposed(PrivateElements privateElements, Binding<?> binding) {
      return !privateElements.getExposedKeys().contains(binding.getKey());
   }

   @SuppressFBWarnings(justification = "generated code")
   private GuicePrivateElements(@NonNull Collection<PrivateElements> collection) {
      if (collection == null) {
         throw new NullPointerException("collection is marked non-null but is null");
      }

      this.collection = collection;
   }
}
