/*
 * 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.UIMAFramework.getLogger;
import static org.apache.uima.UIMAFramework.newConfigurationManager;
import static org.apache.uima.UIMAFramework.newUimaContext;
import static org.apache.uima.fit.factory.ConfigurationParameterFactory.createConfigurationData;
import static org.apache.uima.fit.factory.ConfigurationParameterFactory.getConfigurationParameterName;
import static org.apache.uima.fit.factory.ConfigurationParameterFactory.getDefaultValue;
import static org.apache.uima.fit.factory.ConfigurationParameterFactory.getParameterSettings;
import static org.apache.uima.fit.factory.ConfigurationParameterFactory.isConfigurationParameterField;
import static org.apache.uima.fit.internal.ReflectionUtil.getAnnotation;
import static org.apache.uima.fit.internal.ReflectionUtil.getFields;
import static org.apache.uima.fit.internal.ResourceManagerFactory.newResourceManager;
import static org.apache.uima.fit.internal.propertyeditors.PropertyEditorUtil.registerUimaFITEditors;
import static org.springframework.beans.PropertyAccessorUtils.canonicalPropertyName;
import static org.springframework.util.ObjectUtils.isEmpty;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.uima.UimaContext;
import org.apache.uima.UimaContextAdmin;
import org.apache.uima.fit.descriptor.ConfigurationParameter;
import org.apache.uima.fit.factory.ConfigurationParameterFactory;
import org.apache.uima.fit.factory.ConfigurationParameterFactory.ConfigurationData;
import org.apache.uima.resource.ConfigurationManager;
import org.apache.uima.resource.CustomResourceSpecifier;
import org.apache.uima.resource.DataResource;
import org.apache.uima.resource.Parameter;
import org.apache.uima.resource.Resource;
import org.apache.uima.resource.ResourceInitializationException;
import org.apache.uima.resource.ResourceManager;
import org.apache.uima.resource.ResourceSpecifier;
import org.apache.uima.resource.metadata.ConfigurationParameterSettings;
import org.apache.uima.resource.metadata.NameValuePair;
import org.apache.uima.resource.metadata.ResourceMetaData;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.validation.DataBinder;
import org.springframework.validation.ObjectError;

/**
 * Initialize an instance of a class with fields that are annotated as
 * {@link ConfigurationParameter}s from the parameter values given in a {@link UimaContext}.
 */
public final class ConfigurationParameterInitializer {

  private ConfigurationParameterInitializer() {
    // Utility class
  }

  /**
   * Initialize a component from an {@link UimaContext} This code can be a little confusing because
   * the configuration parameter annotations are used in two contexts: in describing the component
   * and to initialize member variables from a {@link UimaContext}. Here we are performing the
   * latter task. It is important to remember that the {@link UimaContext} passed in to this method
   * may or may not have been derived using reflection of the annotations (i.e. using
   * {@link ConfigurationParameterFactory} via e.g. a call to a AnalysisEngineFactory.create
   * method). It is just as possible for the description of the component to come directly from an
   * XML descriptor file. So, for example, just because a configuration parameter specifies a
   * default value, this does not mean that the passed in context will have a value for that
   * configuration parameter. It should be possible for a descriptor file to specify its own value
   * or to not provide one at all. If the context does not have a configuration parameter, then the
   * default value provided by the developer as specified by the defaultValue element of the
   * {@link ConfigurationParameter} will be used. See comments in the code for additional details.
   *
   * @param component
   *          the component to initialize.
   * @param context
   *          a UIMA context with configuration parameters.
   * @throws ResourceInitializationException
   *           if a failure occurs during initialization.
   */
  public static void initialize(final Object component, final UimaContext context)
          throws ResourceInitializationException {
    MutablePropertyValues values = new MutablePropertyValues();
    List<String> mandatoryValues = new ArrayList<>();

    for (Field field : getFields(component)) { // component.getClass().getDeclaredFields())
      if (isConfigurationParameterField(field)) {
        org.apache.uima.fit.descriptor.ConfigurationParameter annotation = getAnnotation(field,
                org.apache.uima.fit.descriptor.ConfigurationParameter.class);

        Object parameterValue;
        String parameterName = getConfigurationParameterName(field);

        // Obtain either from the context - or - if the context does not provide the
        // parameter, check if there is a default value. Note there are three possibilities:
        // 1) Parameter present and set
        // 2) Parameter present and set to null (null value)
        // 3) Parameter not present (also provided as null value by UIMA)
        // Unfortunately we cannot make a difference between case 2 and 3 since UIMA does
        // not allow us to actually get a list of the parameters set in the context. We can
        // only get a list of the declared parameters. Thus we have to rely on the null
        // value.
        parameterValue = context.getConfigParameterValue(parameterName);
        if (parameterValue == null) {
          parameterValue = getDefaultValue(field);
        }

        if (parameterValue != null) {
          values.addPropertyValue(field.getName(), parameterValue);
        }

        // TODO does this check really belong here? It seems that
        // this check is already performed by UIMA
        if (annotation.mandatory()) {
          mandatoryValues.add(field.getName());
        }
      }
    }

    DataBinder binder = new DataBinder(component) {
      @Override
      protected void checkRequiredFields(MutablePropertyValues mpvs) {
        String[] requiredFields = getRequiredFields();
        if (!isEmpty(requiredFields)) {
          Map<String, PropertyValue> propertyValues = new HashMap<>();
          PropertyValue[] pvs = mpvs.getPropertyValues();
          for (PropertyValue pv : pvs) {
            propertyValues.put(canonicalPropertyName(pv.getName()), pv);
          }
          for (String field : requiredFields) {
            PropertyValue pv = propertyValues.get(field);
            boolean empty = (pv == null || pv.getValue() == null);
            // For our purposes, empty Strings or empty String arrays do not count as
            // empty. Empty is only "null".
            // if (!empty) {
            // if (pv.getValue() instanceof String) {
            // empty = !StringUtils.hasText((String) pv.getValue());
            // }
            // else if (pv.getValue() instanceof String[]) {
            // String[] values = (String[]) pv.getValue();
            // empty = (values.length == 0 || !StringUtils.hasText(values[0]));
            // }
            // }
            if (empty) {
              // Use bind error processor to create FieldError.
              getBindingErrorProcessor().processMissingFieldError(field,
                      getInternalBindingResult());
              // Remove property from property values to bind:
              // It has already caused a field error with a rejected value.
              if (pv != null) {
                mpvs.removePropertyValue(pv);
                propertyValues.remove(field);
              }
            }
          }
        }
      }
    };
    binder.initDirectFieldAccess();
    registerUimaFITEditors(binder);
    binder.setRequiredFields(mandatoryValues.toArray(new String[mandatoryValues.size()]));

    binder.bind(values);

    if (binder.getBindingResult().hasErrors()) {
      StringBuilder sb = new StringBuilder();
      sb.append("Errors initializing [" + component.getClass() + "]");
      for (ObjectError error : binder.getBindingResult().getAllErrors()) {
        if (sb.length() > 0) {
          sb.append("\n");
        }
        sb.append(error.getDefaultMessage());
      }
      throw new IllegalArgumentException(sb.toString());
    }
  }

  /**
   * Initialize a component from a map.
   *
   * @param component
   *          the component to initialize.
   * @param map
   *          a UIMA context with configuration parameters.
   * @see #initialize(Object, UimaContext)
   * @throws ResourceInitializationException
   *           if a failure occurs during initialization.
   */
  public static void initialize(final Object component, final Map<String, Object> map)
          throws ResourceInitializationException {
    // If there is already a ResourceManager then re-use that, otherwise create a new one.
    ResourceManager resMgr;
    if (component instanceof Resource) {
      resMgr = ((Resource) component).getResourceManager();
    } else {
      resMgr = newResourceManager();
    }

    UimaContextAdmin context = newUimaContext(getLogger(), resMgr, newConfigurationManager());

    ConfigurationManager cfgMgr = context.getConfigurationManager();
    cfgMgr.setSession(context.getSession());

    for (Entry<String, Object> e : map.entrySet()) {
      cfgMgr.setConfigParameterValue(context.getQualifiedContextName() + e.getKey(), e.getValue());
    }

    initialize(component, context);
  }

  /**
   * Initialize a component from a {@link CustomResourceSpecifier}.
   *
   * @param component
   *          the component to initialize.
   * @param spec
   *          a resource specifier.
   * @see #initialize(Object, UimaContext)
   * @throws ResourceInitializationException
   *           if a failure occurs during initialization.
   */
  public static void initialize(Object component, ResourceSpecifier spec)
          throws ResourceInitializationException {
    initialize(component, getParameterSettings(spec));
  }

  /**
   * Initialize a component from a {@link CustomResourceSpecifier}.
   *
   * @param component
   *          the component to initialize.
   * @param parameters
   *          a list of parameters.
   * @see #initialize(Object, UimaContext)
   * @throws ResourceInitializationException
   *           if a failure occurs during initialization.
   */
  public static void initialize(Object component, Parameter... parameters)
          throws ResourceInitializationException {
    Map<String, Object> params = new HashMap<>();
    for (Parameter p : parameters) {
      params.put(p.getName(), p.getValue());
    }
    initialize(component, params);
  }

  /**
   * Initialize a component from a {@link ResourceMetaData}.
   *
   * @param component
   *          the component to initialize.
   * @param parameters
   *          a list of parameters.
   * @see #initialize(Object, UimaContext)
   * @throws ResourceInitializationException
   *           if a failure occurs during initialization.
   */
  public static void initialize(Object component, NameValuePair... parameters)
          throws ResourceInitializationException {
    Map<String, Object> params = new HashMap<>();
    for (NameValuePair p : parameters) {
      params.put(p.getName(), p.getValue());
    }
    initialize(component, params);
  }

  /**
   * Initialize a component from a {@link ResourceMetaData}.
   *
   * @param component
   *          the component to initialize.
   * @param dataResource
   *          a data resource with configuration meta data.
   * @see #initialize(Object, UimaContext)
   * @throws ResourceInitializationException
   *           if a failure occurs during initialization.
   */
  public static void initialize(Object component, DataResource dataResource)
          throws ResourceInitializationException {
    ResourceMetaData metaData = dataResource.getMetaData();
    ConfigurationParameterSettings settings = metaData.getConfigurationParameterSettings();
    initialize(component, settings.getParameterSettings());
  }

  public static void initialize(Object component, Object... aParameters)
          throws ResourceInitializationException {
    ConfigurationData configurationData = createConfigurationData(aParameters);
    Map<String, Object> params = new HashMap<>();
    for (int i = 0; i < configurationData.configurationParameters.length; i++) {
      String name = configurationData.configurationParameters[i].getName();
      Object value = configurationData.configurationValues[i];
      params.put(name, value);
    }
    initialize(component, params);
  }
}
