/**
 * Copyright (C) 2011 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 com.google.inject.internal;

import com.google.common.collect.ImmutableSet;
import com.google.inject.AbstractModule;
import com.google.inject.Binder;
import com.google.inject.Binding;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.MembersInjector;
import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.Scope;
import com.google.inject.Stage;
import com.google.inject.TypeLiteral;
import com.google.inject.spi.DefaultBindingTargetVisitor;

import java.util.Set;

/**
 * Guarantees that processing of Binding elements happens in a sane way.
 * 
 * @author sameb@google.com (Sam Berlin)
 */
abstract class AbstractBindingProcessor extends AbstractProcessor {

//------------------------------------------------------------------------------
  private static final boolean DISABLE_MISPLACED_ANNOTATION_CHECK;
  static {
    boolean disableCheck;
    try {
      disableCheck = Boolean.parseBoolean(System.getProperty(
          "guice.disable.misplaced.annotation.check", "false"));
    } catch (Throwable e) {
      disableCheck = false;
    }
    DISABLE_MISPLACED_ANNOTATION_CHECK = disableCheck;
  }
//------------------------------------------------------------------------------

  // It's unfortunate that we have to maintain a blacklist of specific
  // classes, but we can't easily block the whole package because of
  // all our unit tests.
  private static final Set<Class<?>> FORBIDDEN_TYPES = ImmutableSet.<Class<?>>of(
      AbstractModule.class,
      Binder.class,
      Binding.class,
      Injector.class,
      Key.class,
      MembersInjector.class,
      Module.class,
      Provider.class,
      Scope.class,
      Stage.class,
      TypeLiteral.class);
  
  protected final ProcessedBindingData bindingData;
  
  AbstractBindingProcessor(Errors errors, ProcessedBindingData bindingData) {
    super(errors);
    this.bindingData = bindingData;
  }

  protected <T> UntargettedBindingImpl<T> invalidBinding(
      InjectorImpl injector, Key<T> key, Object source) {
    return new UntargettedBindingImpl<T>(injector, key, source);
  }
  
  protected void putBinding(BindingImpl<?> binding) {
    Key<?> key = binding.getKey();

    Class<?> rawType = key.getTypeLiteral().getRawType();
    if (FORBIDDEN_TYPES.contains(rawType)) {
      errors.cannotBindToGuiceType(rawType.getSimpleName());
      return;
    }

    BindingImpl<?> original = injector.getExistingBinding(key);
    if (original != null) {
      // If it failed because of an explicit duplicate binding...
      if (injector.state.getExplicitBinding(key) != null) {
        try {
          if(!isOkayDuplicate(original, binding, injector.state)) {
            errors.bindingAlreadySet(key, original.getSource());
            return;
          }
        } catch(Throwable t) {
          errors.errorCheckingDuplicateBinding(key, original.getSource(), t);
          return;
        }
      } else {
        // Otherwise, it failed because of a duplicate JIT binding
        // in the parent
        errors.jitBindingAlreadySet(key);
        return;
      }
    }

    // prevent the parent from creating a JIT binding for this key
    injector.state.parent().blacklist(key, injector.state, binding.getSource());
    injector.state.putBinding(key, binding);
  }

  /**
   * We tolerate duplicate bindings if one exposes the other or if the two bindings
   * are considered duplicates (see {@link Bindings#areDuplicates(BindingImpl, BindingImpl)}.
   *
   * @param original the binding in the parent injector (candidate for an exposing binding)
   * @param binding the binding to check (candidate for the exposed binding)
   */
  private boolean isOkayDuplicate(BindingImpl<?> original, BindingImpl<?> binding, State state) {
    if (original instanceof ExposedBindingImpl) {
      ExposedBindingImpl exposed = (ExposedBindingImpl) original;
      InjectorImpl exposedFrom = (InjectorImpl) exposed.getPrivateElements().getInjector();
      return (exposedFrom == binding.getInjector());
    } else {
      original = (BindingImpl<?>)state.getExplicitBindingsThisLevel().get(binding.getKey());
      // If no original at this level, the original was on a parent, and we don't
      // allow deduplication between parents & children.
      if(original == null) {
        return false;
      } else {
        return original.equals(binding);
      }
    }
  }
  
  private <T> void validateKey(Object source, Key<T> key) {
//------------------------------------------------------------------------------
if (!DISABLE_MISPLACED_ANNOTATION_CHECK) {
//------------------------------------------------------------------------------
    Annotations.checkForMisplacedScopeAnnotations(
        key.getTypeLiteral().getRawType(), source, errors);
//------------------------------------------------------------------------------
}
//------------------------------------------------------------------------------
  }
  
  /** 
   * Processor for visiting bindings.  Each overriden method that wants to
   * actually process the binding should call prepareBinding first.
   */
  abstract class Processor<T, V> extends DefaultBindingTargetVisitor<T, V> {
    final Object source;
    final Key<T> key;
    final Class<? super T> rawType;
    Scoping scoping;
    
    Processor(BindingImpl<T> binding) {
      source = binding.getSource();
      key = binding.getKey();
      rawType = key.getTypeLiteral().getRawType();
      scoping = binding.getScoping();
    }
    
    protected void prepareBinding() {      
      validateKey(source, key);
      scoping = Scoping.makeInjectable(scoping, injector, errors);
    }

    protected void scheduleInitialization(final BindingImpl<?> binding) {
      bindingData.addUninitializedBinding(new Runnable() {
        public void run() {
          try {
            binding.getInjector().initializeBinding(binding, errors.withSource(source));
          } catch (ErrorsException e) {
            errors.merge(e.getErrors());
          }
        }
      });
    }
  }
}
