/*
 * Copyright (C) 2015 Google, Inc.
 *
 * 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 dagger.internal.codegen;

import com.google.auto.value.AutoValue;
import com.google.common.base.Optional;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static dagger.internal.codegen.ContributionBinding.contributionTypeFor;

/**
 * The collection of bindings that have been resolved for a binding key.
 *
 * @author Gregory Kick
 */
@AutoValue
abstract class ResolvedBindings {
  /**
   * The binding key for which the {@link #bindings()} have been resolved.
   */
  abstract BindingKey bindingKey();

  /**
   * The component in which the bindings in {@link #ownedBindings()},
   * {@link #ownedContributionBindings()}, and {@link #ownedMembersInjectionBinding()} were
   * resolved.
   */
  abstract ComponentDescriptor owningComponent();

  /**
   * The contribution bindings for {@link #bindingKey()} that were resolved in
   * {@link #owningComponent()} or its ancestor components, keyed by the component in which the
   * binding was resolved. If {@link #bindingKey()}'s kind is not
   * {@link BindingKey.Kind#CONTRIBUTION}, this is empty.
   */
  abstract ImmutableSetMultimap<ComponentDescriptor, ContributionBinding> allContributionBindings();

  /**
   * The members-injection bindings for {@link #bindingKey()} that were resolved in
   * {@link #owningComponent()} or its ancestor components, keyed by the component in which the
   * binding was resolved. If {@link #bindingKey()}'s kind is not
   * {@link BindingKey.Kind#MEMBERS_INJECTION}, this is empty.
   */
  abstract ImmutableMap<ComponentDescriptor, MembersInjectionBinding> allMembersInjectionBindings();

  /**
   * All bindings for {@link #bindingKey()}, regardless of in which component they were resolved.
   */
  ImmutableSet<? extends Binding> bindings() {
    switch (bindingKey().kind()) {
      case CONTRIBUTION:
        return contributionBindings();

      case MEMBERS_INJECTION:
        return ImmutableSet.copyOf(membersInjectionBinding().asSet());

      default:
        throw new AssertionError(bindingKey());
    }
  }

  /**
   * All bindings for {@link #bindingKey()} that were resolved in {@link #owningComponent()}.
   */
  ImmutableSet<? extends Binding> ownedBindings() {
    switch (bindingKey().kind()) {
      case CONTRIBUTION:
        return ownedContributionBindings();

      case MEMBERS_INJECTION:
        return ImmutableSet.copyOf(ownedMembersInjectionBinding().asSet());

      default:
        throw new AssertionError(bindingKey());
    }
  }

  /**
   * All contribution bindings, regardless of owning component.
   *
   * @throws IllegalStateException if {@link #bindingKey()} is not a
   * {@link BindingKey.Kind#CONTRIBUTION}.
   */
  ImmutableSet<ContributionBinding> contributionBindings() {
    checkState(bindingKey().kind().equals(BindingKey.Kind.CONTRIBUTION));
    return ImmutableSet.copyOf(allContributionBindings().values());
  }

  /**
   * The contribution bindings that were resolved in {@link #owningComponent()}.
   *
   * @throws IllegalStateException if {@link #bindingKey()} is not a
   * {@link BindingKey.Kind#CONTRIBUTION}.
   */
  ImmutableSet<ContributionBinding> ownedContributionBindings() {
    checkState(bindingKey().kind().equals(BindingKey.Kind.CONTRIBUTION));
    return allContributionBindings().get(owningComponent());
  }

  /**
   * The members-injection binding, regardless of owning component.
   *
   * @throws IllegalStateException if {@link #bindingKey()} is not a
   * {@link BindingKey.Kind#MEMBERS_INJECTION}.
   */
  Optional<MembersInjectionBinding> membersInjectionBinding() {
    checkState(bindingKey().kind().equals(BindingKey.Kind.MEMBERS_INJECTION));
    ImmutableSet<MembersInjectionBinding> membersInjectionBindings =
        FluentIterable.from(allMembersInjectionBindings().values()).toSet();
    return membersInjectionBindings.isEmpty()
        ? Optional.<MembersInjectionBinding>absent()
        : Optional.of(Iterables.getOnlyElement(membersInjectionBindings));
  }

  /**
   * The members-injection binding that was resolved in {@link #owningComponent()}.
   *
   * @throws IllegalStateException if {@link #bindingKey()} is not a
   * {@link BindingKey.Kind#MEMBERS_INJECTION}.
   */
  Optional<MembersInjectionBinding> ownedMembersInjectionBinding() {
    checkState(bindingKey().kind().equals(BindingKey.Kind.MEMBERS_INJECTION));
    return Optional.fromNullable(allMembersInjectionBindings().get(owningComponent()));
  }

  /**
   * Creates a {@link ResolvedBindings} for contribution bindings.
   */
  static ResolvedBindings forContributionBindings(
      BindingKey bindingKey,
      ComponentDescriptor owningComponent,
      Multimap<ComponentDescriptor, ? extends ContributionBinding> contributionBindings) {
    checkArgument(bindingKey.kind().equals(BindingKey.Kind.CONTRIBUTION));
    return new AutoValue_ResolvedBindings(
        bindingKey,
        owningComponent,
        ImmutableSetMultimap.<ComponentDescriptor, ContributionBinding>copyOf(contributionBindings),
        ImmutableMap.<ComponentDescriptor, MembersInjectionBinding>of());
  }

  /**
   * Creates a {@link ResolvedBindings} for contribution bindings.
   */
  static ResolvedBindings forContributionBindings(
      BindingKey bindingKey,
      ComponentDescriptor owningComponent,
      ContributionBinding... ownedContributionBindings) {
    return forContributionBindings(
        bindingKey,
        owningComponent,
        ImmutableSetMultimap.<ComponentDescriptor, ContributionBinding>builder()
            .putAll(owningComponent, ownedContributionBindings)
            .build());
  }

  /**
   * Creates a {@link ResolvedBindings} for members injection bindings.
   */
  static ResolvedBindings forMembersInjectionBinding(
      BindingKey bindingKey,
      ComponentDescriptor owningComponent,
      MembersInjectionBinding ownedMembersInjectionBinding) {
    checkArgument(bindingKey.kind().equals(BindingKey.Kind.MEMBERS_INJECTION));
    return new AutoValue_ResolvedBindings(
        bindingKey,
        owningComponent,
        ImmutableSetMultimap.<ComponentDescriptor, ContributionBinding>of(),
        ImmutableMap.of(owningComponent, ownedMembersInjectionBinding));
  }

  /**
   * Creates a {@link ResolvedBindings} appropriate for when there are no bindings for the key.
   */
  static ResolvedBindings noBindings(BindingKey bindingKey, ComponentDescriptor owningComponent) {
    return new AutoValue_ResolvedBindings(
        bindingKey,
        owningComponent,
        ImmutableSetMultimap.<ComponentDescriptor, ContributionBinding>of(),
        ImmutableMap.<ComponentDescriptor, MembersInjectionBinding>of());
  }

  /**
   * Returns a {@code ResolvedBindings} with the same {@link #bindingKey()} and {@link #bindings()}
   * as this one, but no {@link #ownedBindings()}.
   */
  ResolvedBindings asInheritedIn(ComponentDescriptor owningComponent) {
    return new AutoValue_ResolvedBindings(
        bindingKey(), owningComponent, allContributionBindings(), allMembersInjectionBindings());
  }

  /**
   * {@code true} if this is a multibindings contribution.
   */
  boolean isMultibindings() {
    return bindingKey().kind().equals(BindingKey.Kind.CONTRIBUTION)
        && !contributionBindings().isEmpty()
        && contributionTypeFor(contributionBindings()).isMultibinding();
  }

  /**
   * {@code true} if this is a unique contribution binding.
   */
  boolean isUniqueContribution() {
    return bindingKey().kind().equals(BindingKey.Kind.CONTRIBUTION)
        && !contributionBindings().isEmpty()
        && !contributionTypeFor(contributionBindings()).isMultibinding();
  }
}