/*
 * 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.factory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.uima.analysis_engine.AnalysisEngine;
import org.apache.uima.analysis_engine.AnalysisEngineDescription;
import org.apache.uima.analysis_engine.metadata.SofaMapping;
import org.apache.uima.flow.FlowControllerDescription;
import org.apache.uima.resource.ResourceInitializationException;
import org.apache.uima.resource.metadata.TypePriorities;
import org.apache.uima.resource.metadata.TypeSystemDescription;

/**
 * This builder makes it easier to create an aggregate analysis engine. A typical use-case would
 * involve initializing this builder with your preferred type system and type priorities (the latter
 * may be null). This is followed by adding analysis engine descriptions one at a time until done.
 * This makes it easy to have runtime decisions determine how the aggregate engine should be built.
 * Finally, one of the create methods are called and an AnalysisEngine or AnalysisEngineDescription
 * is returned.
 * 
 * <p>
 * This is an example taken from our test cases:
 * </p>
 * 
 * <pre>
 * <code>
 * import static org.apache.uima.fit.factory.AnalysisEngineFactory.createPrimitiveDescription;
 * 
 * AggregateBuilder builder = new AggregateBuilder();
 * builder.add(createPrimitiveDescription(Annotator1.class, typeSystemDescription),
 *     ViewNames.PARENTHESES_VIEW, "A");
 * builder.add(createPrimitiveDescription(Annotator2.class, typeSystemDescription),
 *     ViewNames.SORTED_VIEW, "B",
 *     ViewNames.SORTED_PARENTHESES_VIEW, "C",
 *     ViewNames.PARENTHESES_VIEW, "A");
 * builder.add(createPrimitiveDescription(Annotator3.class, typeSystemDescription),
 *     ViewNames.INITIAL_VIEW, "B");
 * AnalysisEngine aggregateEngine = builder.createAggregate();
 * </code>
 * </pre>
 */
public class AggregateBuilder {

  List<String> componentNames = new ArrayList<String>();

  List<SofaMapping> sofaMappings = new ArrayList<SofaMapping>();

  List<AnalysisEngineDescription> analysisEngineDescriptions = new ArrayList<AnalysisEngineDescription>();

  TypeSystemDescription typeSystemDescription;

  TypePriorities typePriorities;

  FlowControllerDescription flowControllerDescription;

  /**
   * The default no-args constructor calls
   * {@link AggregateBuilder#AggregateBuilder(TypeSystemDescription, TypePriorities, FlowControllerDescription)}
   * with null-valued args.
   */
  public AggregateBuilder() {
    this(null, null, null);
  }

  /**
   * Instantiate an AggregateBuilder with a given type system, type priorities, and flow controller.
   * Generally, speaking it suffices to use the no arguments constructor
   * 
   * @param typeSystemDescription
   *          this can be instantiated using {@link TypeSystemDescriptionFactory}
   * @param typePriorities
   *          this can be instantiated using {@link TypePrioritiesFactory}
   * @param flowControllerDescription
   *          this can be instantiated using {@link FlowControllerFactory}
   */
  public AggregateBuilder(TypeSystemDescription typeSystemDescription,
          TypePriorities typePriorities, FlowControllerDescription flowControllerDescription) {
    this.typeSystemDescription = typeSystemDescription;
    this.typePriorities = typePriorities;
    this.flowControllerDescription = flowControllerDescription;
  }

  /**
   * This method simply calls {@link #add(String, AnalysisEngineDescription, String...)} using the
   * result of {@link AnalysisEngineDescription#getAnnotatorImplementationName()} for the component
   * name
   * 
   * @param aed
   *          an analysis engine description to add to the aggregate analysis engine
   * @param viewNames
   *          pairs of view names corresponding to a componentSofaName followed by the
   *          aggregateSofaName that it is mapped to. An even number of names must be passed in or
   *          else an IllegalArgumentException will be thrown. See
   *          {@link SofaMappingFactory#createSofaMapping(String, String, String)}
   * 
   * @return the name of the component generated for the {@link AnalysisEngineDescription}
   */
  public String add(AnalysisEngineDescription aed, String... viewNames) {
    String componentName = AnalysisEngineFactory.generateDelegateKey(aed, componentNames.size());
    add(componentName, aed, viewNames);
    return componentName;
  }

  /**
   * @param componentName
   *          the name of the component to add
   * @param aed
   *          an analysis engine description to add to the aggregate analysis engine
   * @param viewNames
   *          pairs of view names corresponding to a componentSofaName followed by the
   *          aggregateSofaName that it is mapped to. An even number of names must be passed in or
   *          else an IllegalArgumentException will be thrown. See
   *          {@link SofaMappingFactory#createSofaMapping(String, String, String)}
   */
  public void add(String componentName, AnalysisEngineDescription aed, String... viewNames) {
    if (componentNames.contains(componentName)) {
      throw new IllegalArgumentException("the component name '" + componentName
              + "' has already been used for another added analysis engine description.");
    }
    if (viewNames != null && viewNames.length % 2 != 0) {
      throw new IllegalArgumentException("an even number of view names is required (as "
              + "component view name, aggregate view name pairs) for the AggregateBuilder.add "
              + "method. " + viewNames.length + " view names passed: " + Arrays.asList(viewNames));
    }

    analysisEngineDescriptions.add(aed);
    componentNames.add(componentName);

    if (viewNames != null) {
      for (int i = 0; i < viewNames.length; i += 2) {
        sofaMappings.add(SofaMappingFactory.createSofaMapping(componentName, viewNames[i],
                viewNames[i + 1]));
      }
    }
  }

  /**
   * Provide a sofa mapping for a component from the component's view to the aggregate view.
   * 
   * @param componentName
   *          the name of the component
   * @param componentViewName
   *          the name of the component view
   * @param aggregateViewName
   *          the name of the aggregate view to map the component view to.
   */
  public void addSofaMapping(String componentName, String componentViewName,
          String aggregateViewName) {
    if (componentNames.contains(componentName)) {
      sofaMappings.add(SofaMappingFactory.createSofaMapping(componentName, componentViewName,
              aggregateViewName));
    } else {
      throw new IllegalArgumentException("No component with the name '" + componentName
              + "' has been added to this builder.  Sofa mappings may only be added for "
              + "components that have been added to this builder. ");
    }
  }

  /**
   * Set the flow controller description of the aggregate engine created by this builder.
   * 
   * @param flowControllerDescription
   *          see {@link FlowControllerFactory}
   */
  public void setFlowControllerDescription(FlowControllerDescription flowControllerDescription) {
    this.flowControllerDescription = flowControllerDescription;
  }

  /**
   * This method simply delegates to
   * {@link AnalysisEngineFactory#createEngine(List, TypeSystemDescription, TypePriorities, SofaMapping[], Object...)}
   * with the data collected by this builder.
   * 
   * @return an aggregate analysis engine
   * @throws ResourceInitializationException
   *           if there is a problem during initialization
   */
  public AnalysisEngine createAggregate() throws ResourceInitializationException {
    return AnalysisEngineFactory.createEngine(analysisEngineDescriptions, componentNames,
            typePriorities, sofaMappings.toArray(new SofaMapping[sofaMappings.size()]),
            flowControllerDescription);
  }

  /**
   * This method simply delegates to
   * {@link AnalysisEngineFactory#createEngineDescription(List, TypeSystemDescription, TypePriorities, SofaMapping[], Object...)}
   * with the data collected by this builder.
   * 
   * @return a description of an aggregate analysis engine
   * @throws ResourceInitializationException
   *           if there is a problem during initialization
   */
  public AnalysisEngineDescription createAggregateDescription()
          throws ResourceInitializationException {
    return AnalysisEngineFactory.createEngineDescription(analysisEngineDescriptions, componentNames,
            typePriorities, sofaMappings.toArray(new SofaMapping[sofaMappings.size()]),
            flowControllerDescription);
  }
}
