/*
 * Copyright 2009 the original author or 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 org.spockframework.tapestry;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.apache.tapestry5.ioc.AnnotationProvider;
import org.apache.tapestry5.ioc.Registry;
import org.apache.tapestry5.ioc.RegistryBuilder;
import org.apache.tapestry5.ioc.annotations.Autobuild;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.InjectService;

import org.spockframework.runtime.extension.AbstractMethodInterceptor;
import org.spockframework.runtime.extension.IMethodInvocation;
import org.spockframework.runtime.model.FieldInfo;
import org.spockframework.runtime.model.SpecInfo;
import org.spockframework.util.ReflectionUtil;

import spock.lang.Shared;
import spock.lang.Specification;

/**
 * Controls creation, startup and shutdown of the Tapestry container,
 * and injects specifications with Tapestry-provided objects.
 *
 * @author Peter Niederwieser
 */
public class TapestryInterceptor extends AbstractMethodInterceptor {
  private final SpecInfo spec;
  private final Set<Class<?>> modules;

  private Registry registry;

  public TapestryInterceptor(SpecInfo spec, Set<Class<?>> modules) {
    this.spec = spec;
    this.modules = modules;
  }

  @Override
  public void interceptSharedInitializerMethod(IMethodInvocation invocation) throws Throwable {
    runBeforeRegistryCreatedMethods((Specification) invocation.getSharedInstance());
    createAndStartupRegistry();
    injectServices(invocation.getSharedInstance(), true);
    invocation.proceed();
  }

  @Override
  public void interceptCleanupSpecMethod(IMethodInvocation invocation) throws Throwable {
    try {
      invocation.proceed();
    } finally {
      shutdownRegistry();
    }
  }

  @Override
  public void interceptInitializerMethod(IMethodInvocation invocation) throws Throwable {
    injectServices(invocation.getInstance(), false);
    invocation.proceed();
  }

  private void runBeforeRegistryCreatedMethods(Specification spec) {
    Object returnValue;

    for (Method method : findAllBeforeRegistryCreatedMethods()) {
      returnValue = ReflectionUtil.invokeMethod(spec, method);

      // Return values of a type other than Registry are silently ignored.
      // This avoids problems in case the method unintentionally returns a
      // value (which is common due to Groovy's implicit return).
      if (returnValue instanceof Registry)
        registry = (Registry) returnValue;
    }
  }

  private void createAndStartupRegistry() {
    if (registry != null) return;

    RegistryBuilder builder = new RegistryBuilder();
    builder.add(ExtensionModule.class);
    for (Class<?> module : modules) builder.add(module);
    registry = builder.build();
    registry.performRegistryStartup();
  }

  private List<Method> findAllBeforeRegistryCreatedMethods() {
    List<Method> methods = new ArrayList<Method>();

    for (SpecInfo curr : spec.getSpecsTopToBottom()) {
      Method method = ReflectionUtil.getDeclaredMethodByName(curr.getReflection(), "beforeRegistryCreated");
      if (method != null) {
        method.setAccessible(true);
        methods.add(method);
      }
    }

    return methods;
  }

  private void injectServices(Object target, boolean sharedFields) throws IllegalAccessException {
    for (final FieldInfo field : spec.getAllFields()) {
      Field rawField = field.getReflection();
      if ((rawField.isAnnotationPresent(Inject.class)
          || ReflectionUtil.isAnnotationPresent(rawField, "javax.inject.Inject")
          || rawField.isAnnotationPresent(Autobuild.class))
          && rawField.isAnnotationPresent(Shared.class) == sharedFields) {
        Object value = registry.getObject(rawField.getType(), createAnnotationProvider(field));
        rawField.setAccessible(true);
        rawField.set(target, value);
      } else if (rawField.isAnnotationPresent(InjectService.class)) {
        String serviceName = rawField.getAnnotation(InjectService.class).value();
        Object value = registry.getService(serviceName, rawField.getType());
        rawField.setAccessible(true);
        rawField.set(target, value);
      }
    }
  }

  private AnnotationProvider createAnnotationProvider(final FieldInfo field) {
    return new AnnotationProvider() {
      public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
        return field.getAnnotation(annotationClass);
      }
    };
  }

  private void shutdownRegistry() {
    if (registry != null)
      registry.shutdown();
  }
}
