/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.uima.fit.component.initialize;

import static org.apache.uima.fit.factory.ExternalResourceFactory.PREFIX_SEPARATOR;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

import org.apache.uima.UimaContext;
import org.apache.uima.UimaContextAdmin;
import org.apache.uima.fit.component.ExternalResourceAware;
import org.apache.uima.fit.descriptor.ExternalResource;
import org.apache.uima.fit.descriptor.ExternalResourceLocator;
import org.apache.uima.fit.internal.ReflectionUtil;
import org.apache.uima.fit.internal.ResourceList;
import org.apache.uima.resource.ResourceAccessException;
import org.apache.uima.resource.ResourceInitializationException;
import org.apache.uima.resource.ResourceManager;
import org.apache.uima.resource.impl.ResourceManager_impl;
import org.springframework.beans.SimpleTypeConverter;

/**
 * Configurator class for {@link ExternalResource} annotations.
 */
public final class ExternalResourceInitializer {

  private static final Object INITIALIZED = new Object();

  private static Map<Object, Object> initializedResources = new WeakHashMap<Object, Object>();

  private ExternalResourceInitializer() {
    // No instances
  }

  /**
   * Configure a component from the given context.
   * 
   * @param object
   *          the component.
   * @param context
   *          the UIMA context.
   * 
   * @param <T>
   *          the component type.
   * @throws ResourceInitializationException
   *           if the external resource cannot be configured.
   */
  public static <T> void initialize(T object, UimaContext context)
          throws ResourceInitializationException {
    configure(context, object.getClass(), object.getClass(), object);
  }

  /**
   * Helper method for recursively configuring super-classes.
   * 
   * @param <T>
   *          the component type.
   * @param context
   *          the context containing the resource bindings.
   * @param baseCls
   *          the class on which configuration started.
   * @param cls
   *          the class currently being configured.
   * @param object
   *          the object being configured.
   * @throws ResourceInitializationException
   *           if required resources could not be bound.
   */
  private static <T> void configure(UimaContext context, Class<?> baseCls, Class<?> cls, T object)
          throws ResourceInitializationException {
    if (cls.getSuperclass() != null) {
      configure(context, baseCls, cls.getSuperclass(), object);
    } else {
      // Try to initialize the external resources only once, not for each step of the
      // class hierarchy of a component.
      initializeNestedResources(context);
    }

    for (Field field : cls.getDeclaredFields()) {
      if (!ReflectionUtil.isAnnotationPresent(field, ExternalResource.class)) {
        continue;
      }

      ExternalResource era = ReflectionUtil.getAnnotation(field, ExternalResource.class);

      // Get the resource key. If it is a nested resource, also get the prefix.
      String key = era.key();
      if (key.length() == 0) {
        key = field.getName();
      }
      if (object instanceof ExternalResourceAware) {
        String prefix = ((ExternalResourceAware) object).getResourceName();
        if (prefix != null) {
          key = prefix + PREFIX_SEPARATOR + key;
        }
      }

      // Obtain the resource
      Object value = getResourceObject(context, key);
      if (value instanceof ExternalResourceLocator) {
        value = ((ExternalResourceLocator) value).getResource();
      }

      // Sanity checks
      if (value == null && era.mandatory()) {
        throw new ResourceInitializationException(new IllegalStateException(
                "Mandatory resource [" + key + "] is not set on [" + baseCls + "]"));
      }

      // Now record the setting and optionally apply it to the given
      // instance.
      if (value != null) {
        field.setAccessible(true);

        try {
          if (value instanceof ResourceList) {
            // Value is a multi-valued resource
            ResourceList resList = (ResourceList) value;

            // We cannot do this in ResourceList because the resource doesn't have access to
            // the UIMA context we use here. Resources are initialize with their own contexts
            // by the UIMA framework!
            List<Object> elements = new ArrayList<Object>();
            for (int i = 0; i < resList.getSize(); i++) {
              Object elementValue = getResourceObject(context, resList.getResourceName()
                      + PREFIX_SEPARATOR + ResourceList.ELEMENT_KEY + "[" + i + "]");
              elements.add(elementValue);
            }

            SimpleTypeConverter converter = new SimpleTypeConverter();
            value = converter.convertIfNecessary(elements, field.getType());
          }

          try {
            field.set(object, value);
          } catch (IllegalAccessException e) {
            throw new ResourceInitializationException(e);
          }
        } finally {
          field.setAccessible(false);
        }
      }
    }
  }

  private static Object getResourceObject(UimaContext aContext, String aKey)
          throws ResourceInitializationException {
    Object value;
    try {
      value = aContext.getResourceObject(aKey);
    } catch (ResourceAccessException e) {
      throw new ResourceInitializationException(e);
    }
    return value;
  }

  /**
   * Scan the context and initialize external resources injected into other external resources.
   * 
   * @param aContext
   *          the UIMA context.
   */
  private static void initializeNestedResources(UimaContext aContext)
          throws ResourceInitializationException {
    List<ExternalResourceAware> awareResources = new ArrayList<ExternalResourceAware>();

    // Initialize the resources - each resource must only be initialized once. We remember
    // if a resource has already been initialized in a weak hash map, so we automatically
    // forget about resources that are garbage collected.
    for (Object r : getResources(aContext)) {
      synchronized (initializedResources) {
        if (r instanceof ExternalResourceAware && !initializedResources.containsKey(r)) {
          // Already mark the resource as initialized so we do not run into an
          // endless recursive loop when initialize() is called again.
          initializedResources.put(r, INITIALIZED);
          initialize(r, aContext);
          awareResources.add((ExternalResourceAware) r);
        }
      }
    }

    // Notify the resources after everything has been configured
    for (ExternalResourceAware res : awareResources) {
      res.afterResourcesInitialized();
    }
  }

  /**
   * Get all resources declared in the context.
   */
  private static Collection<?> getResources(UimaContext aContext)
          throws ResourceInitializationException {
    if (!(aContext instanceof UimaContextAdmin)) {
      return Collections.emptyList();
    }

    ResourceManager resMgr = ((UimaContextAdmin) aContext).getResourceManager();
    if (!(resMgr instanceof ResourceManager_impl)) {
      // Unfortunately there is not official way to access the list of resources. Thus we
      // have to rely on the UIMA implementation details and access the internal resource
      // map via reflection. If the resource manager is not derived from the default
      // UIMA resource manager, then we cannot really do anything here.
      throw new IllegalStateException(
              "Unsupported resource manager implementation [" + resMgr.getClass() + "]");
    }

    return resMgr.getExternalResources();
  }
}
