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

import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.uima.analysis_engine.AnalysisEngineProcessException;
import org.apache.uima.analysis_engine.TypeOrFeature;
import org.apache.uima.analysis_engine.metadata.AnalysisEngineMetaData;
import org.apache.uima.cas.CAS;
import org.apache.uima.cas.Type;
import org.apache.uima.flow.CasFlowController_ImplBase;
import org.apache.uima.flow.CasFlow_ImplBase;
import org.apache.uima.flow.FinalStep;
import org.apache.uima.flow.Flow;
import org.apache.uima.flow.FlowControllerContext;
import org.apache.uima.flow.SimpleStep;
import org.apache.uima.flow.Step;
import org.apache.uima.resource.ResourceInitializationException;
import org.apache.uima.resource.metadata.Capability;
import org.apache.uima.util.Level;
import org.apache.uima.util.Logger;

/**
 * FlowController implementing a simple version of the "whiteboard" flow model. Each time a CAS is
 * received, it looks at the pool of available AEs that have not yet run on that CAS, and picks one
 * whose input requirements are satisfied.
 * <p>
 * Limitations: only looks at types, not features. Ignores languagesSupported. Does not handle
 * multiple Sofas or CasMultipliers.
 */
public class WhiteboardFlowController extends CasFlowController_ImplBase {

  /**
   * UIMA logger instance we will use to log messages when flow decisions are made.
   */
  private Logger mLogger;

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.uima.flow.FlowController_ImplBase#initialize(org.apache.uima.flow.
   * FlowControllerContext)
   */
  @Override
  public void initialize(FlowControllerContext aContext) throws ResourceInitializationException {
    super.initialize(aContext);
    mLogger = aContext.getLogger();
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.uima.flow.CasFlowController_ImplBase#computeFlow(org.apache.uima.cas.CAS)
   */
  @Override
  public Flow computeFlow(CAS aCAS) throws AnalysisEngineProcessException {
    WhiteboardFlow flow = new WhiteboardFlow();
    // No need to do the following as of release 2.3.0
    // because the framework automatically does this
    // flow.setCas(aCAS);
    return flow;
  }

  /**
   * A separate instance of WhiteboardFlow is created for each input CAS, and is responsible for
   * routing that CAS to all appropriate AnalysisEngines.
   */
  class WhiteboardFlow extends CasFlow_ImplBase {
    private Set mAlreadyCalled = new HashSet();

    /**
     * Get the next AnalyisEngine that should receive the CAS.
     */
    @Override
    public Step next() throws AnalysisEngineProcessException {
      // Get the CAS that this Flow object is responsible for routing.
      // Each Flow instance is responsible for a single CAS.
      CAS cas = getCas();

      // iterate over available AEs
      Iterator aeIter = getContext().getAnalysisEngineMetaDataMap().entrySet().iterator();
      while (aeIter.hasNext()) {
        Map.Entry entry = (Map.Entry) aeIter.next();
        // skip AEs that were already called on this CAS
        String aeKey = (String) entry.getKey();
        if (!mAlreadyCalled.contains(aeKey)) {
          // check for satisfied input capabilities (i.e. the CAS contains at least one instance
          // of each required input
          AnalysisEngineMetaData md = (AnalysisEngineMetaData) entry.getValue();
          Capability[] caps = md.getCapabilities();
          boolean satisfied = true;
          for (int i = 0; i < caps.length; i++) {
            satisfied = inputsSatisfied(caps[i].getInputs(), cas);
            if (satisfied)
              break;
          }
          if (satisfied) {
            mAlreadyCalled.add(aeKey);
            if (mLogger.isLoggable(Level.FINEST)) {
              getContext().getLogger().log(Level.FINEST, "Next AE is: " + aeKey);
            }
            return new SimpleStep(aeKey);
          }
        }
      }
      // no appropriate AEs to call - end of flow
      getContext().getLogger().log(Level.FINEST, "Flow Complete.");
      return new FinalStep();
    }

    /**
     * Checks if a set of input requirements are satisfied in the given CAS. This currently looks
     * only at types, not features. It returns true iff the default CAS view's indexes contain at
     * least one instance of each required input type.
     * 
     * @param aInputs
     *          input requirements
     * @param aCAS
     *          the CAS to check against
     * @return true iff the input requirements are satisfied
     */
    private boolean inputsSatisfied(TypeOrFeature[] aInputs, CAS aCAS) {
      for (int i = 0; i < aInputs.length; i++) {
        TypeOrFeature input = aInputs[i];
        if (input.isType()) {
          Type type = aCAS.getTypeSystem().getType(input.getName());
          if (type == null)
            return false;
          Iterator iter = aCAS.getIndexRepository().getAllIndexedFS(type);
          if (!iter.hasNext())
            return false;
        }
      }
      return true;
    }
  }
}
