/*
 * Copyright (C) 2021 The Dagger Authors.
 *
 * 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.writing;

import static dagger.internal.codegen.base.Util.reentrantComputeIfAbsent;
import static dagger.internal.codegen.binding.BindingRequest.bindingRequest;

import dagger.assisted.Assisted;
import dagger.assisted.AssistedFactory;
import dagger.assisted.AssistedInject;
import dagger.internal.codegen.binding.BindingGraph;
import dagger.internal.codegen.binding.BindingRequest;
import dagger.internal.codegen.binding.ComponentDescriptor.ComponentMethodDescriptor;
import dagger.internal.codegen.binding.ProvisionBinding;
import dagger.internal.codegen.model.RequestKind;
import dagger.internal.codegen.writing.ComponentImplementation.ShardImplementation;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/** Returns request representation based on a direct instance expression. */
final class DirectInstanceBindingRepresentation {
  private final ProvisionBinding binding;
  private final BindingGraph graph;
  private final ComponentImplementation componentImplementation;
  private final ComponentMethodRequestRepresentation.Factory
      componentMethodRequestRepresentationFactory;
  private final ImmediateFutureRequestRepresentation.Factory
      immediateFutureRequestRepresentationFactory;
  private final PrivateMethodRequestRepresentation.Factory
      privateMethodRequestRepresentationFactory;
  private final UnscopedDirectInstanceRequestRepresentationFactory
      unscopedDirectInstanceRequestRepresentationFactory;
  private final Map<BindingRequest, RequestRepresentation> requestRepresentations = new HashMap<>();

  @AssistedInject
  DirectInstanceBindingRepresentation(
      @Assisted ProvisionBinding binding,
      BindingGraph graph,
      ComponentImplementation componentImplementation,
      ComponentMethodRequestRepresentation.Factory componentMethodRequestRepresentationFactory,
      ImmediateFutureRequestRepresentation.Factory immediateFutureRequestRepresentationFactory,
      PrivateMethodRequestRepresentation.Factory privateMethodRequestRepresentationFactory,
      UnscopedDirectInstanceRequestRepresentationFactory
          unscopedDirectInstanceRequestRepresentationFactory) {
    this.binding = binding;
    this.graph = graph;
    this.componentImplementation = componentImplementation;
    this.componentMethodRequestRepresentationFactory = componentMethodRequestRepresentationFactory;
    this.immediateFutureRequestRepresentationFactory = immediateFutureRequestRepresentationFactory;
    this.privateMethodRequestRepresentationFactory = privateMethodRequestRepresentationFactory;
    this.unscopedDirectInstanceRequestRepresentationFactory =
        unscopedDirectInstanceRequestRepresentationFactory;
  }

  public RequestRepresentation getRequestRepresentation(BindingRequest request) {
    return reentrantComputeIfAbsent(
        requestRepresentations, request, this::getRequestRepresentationUncached);
  }

  private RequestRepresentation getRequestRepresentationUncached(BindingRequest request) {
    switch (request.requestKind()) {
      case INSTANCE:
        return requiresMethodEncapsulation(binding)
            ? wrapInMethod(unscopedDirectInstanceRequestRepresentationFactory.create(binding))
            : unscopedDirectInstanceRequestRepresentationFactory.create(binding);

      case FUTURE:
        return immediateFutureRequestRepresentationFactory.create(
            getRequestRepresentation(bindingRequest(binding.key(), RequestKind.INSTANCE)),
            binding.key().type().xprocessing());

      default:
        throw new AssertionError(
            String.format("Invalid binding request kind: %s", request.requestKind()));
    }
  }

  /**
   * Returns a binding expression that uses a given one as the body of a method that users call. If
   * a component provision method matches it, it will be the method implemented. If it does not
   * match a component provision method and the binding is modifiable, then a new public modifiable
   * binding method will be written. If the binding doesn't match a component method and is not
   * modifiable, then a new private method will be written.
   */
  RequestRepresentation wrapInMethod(RequestRepresentation bindingExpression) {
    // If we've already wrapped the expression, then use the delegate.
    if (bindingExpression instanceof MethodRequestRepresentation) {
      return bindingExpression;
    }

    BindingRequest request = bindingRequest(binding.key(), RequestKind.INSTANCE);
    Optional<ComponentMethodDescriptor> matchingComponentMethod =
        graph.componentDescriptor().firstMatchingComponentMethod(request);

    ShardImplementation shardImplementation = componentImplementation.shardImplementation(binding);

    // Consider the case of a request from a component method like:
    //
    //   DaggerMyComponent extends MyComponent {
    //     @Overrides
    //     Foo getFoo() {
    //       <FOO_BINDING_REQUEST>
    //     }
    //   }
    //
    // Normally, in this case we would return a ComponentMethodRequestRepresentation rather than a
    // PrivateMethodRequestRepresentation so that #getFoo() can inline the implementation rather
    // than
    // create an unnecessary private method and return that. However, with sharding we don't want to
    // inline the implementation because that would defeat some of the class pool savings if those
    // fields had to communicate across shards. Thus, when a key belongs to a separate shard use a
    // PrivateMethodRequestRepresentation and put the private method in the shard.
    if (matchingComponentMethod.isPresent() && shardImplementation.isComponentShard()) {
      ComponentMethodDescriptor componentMethod = matchingComponentMethod.get();
      return componentMethodRequestRepresentationFactory.create(bindingExpression, componentMethod);
    } else {
      return privateMethodRequestRepresentationFactory.create(request, binding, bindingExpression);
    }
  }

  private static boolean requiresMethodEncapsulation(ProvisionBinding binding) {
    switch (binding.kind()) {
      case COMPONENT:
      case COMPONENT_PROVISION:
      case SUBCOMPONENT_CREATOR:
      case COMPONENT_DEPENDENCY:
      case MULTIBOUND_SET:
      case MULTIBOUND_MAP:
      case BOUND_INSTANCE:
      case ASSISTED_FACTORY:
      case ASSISTED_INJECTION:
      case INJECTION:
      case PROVISION:
        // These binding kinds satify a binding request with a component method or a private
        // method when the requested binding has dependencies. The method will wrap the logic of
        // creating the binding instance. Without the encapsulation, we might see many levels of
        // nested instance creation code in a single statement to satisfy all dependencies of a
        // binding request.
        return !binding.dependencies().isEmpty();
      case MEMBERS_INJECTOR:
      case PRODUCTION:
      case COMPONENT_PRODUCTION:
      case OPTIONAL:
      case DELEGATE:
      case MEMBERS_INJECTION:
        return false;
    }
    throw new AssertionError(String.format("No such binding kind: %s", binding.kind()));
  }

  @AssistedFactory
  static interface Factory {
    DirectInstanceBindingRepresentation create(ProvisionBinding binding);
  }
}
