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

import org.apache.uima.UIMARuntimeException;
import org.apache.uima.analysis_engine.ResultSpecification;
import org.apache.uima.analysis_engine.TypeOrFeature;
import org.apache.uima.cas.CAS;
import org.apache.uima.cas.admin.CASMgr;
import org.apache.uima.cas.impl.CASImpl;
import org.apache.uima.cas.impl.CASMgrSerializer;
import org.apache.uima.cas.impl.CASSerializer;
import org.apache.uima.cas.impl.FeatureImpl;
import org.apache.uima.cas.impl.Serialization;
import org.apache.uima.cas.impl.TypeImpl;
import org.apache.uima.cas.impl.TypeSystemImpl;
import org.apache.uima.internal.util.IntVector;

public class UimacppEngine {

  // Those types can not be created with CAS.createFS().
  private static String[] compatibleTafJniVersions = { "2.0" };

  static {
    String uimacpp_lib = "uima";
    String debug = System.getProperty("DEBUG_UIMACPP");
    if (debug != null) {
      String osname = System.getProperty("os.name");
      if (osname.startsWith("Windows")) {
        uimacpp_lib = "uimaD";
      }
    }
    System.loadLibrary(uimacpp_lib);
    try {
      String jniVersion = getTafJNIVersion();
      boolean compatible = false;
      for (int i = 0; i < compatibleTafJniVersions.length; i++) {
        if (jniVersion.equals(compatibleTafJniVersions[i])) {
          compatible = true;
          break;
        }
      }
      if (compatible) {
        createResourceManager();
      } else {
        throw new UIMARuntimeException(UIMARuntimeException.INCOMPATIBLE_TAF_JNI_LIBRARY,
                new Object[] { jniVersion });
      }

    } catch (UimacppException exc) {
      throw new UIMARuntimeException(exc.getEmbeddedException());
    }
  }

  /**
   * this field is the physical pointer to the C++ TafClEngine object this TafWrapper object is
   * associated with. Do not use explicitly anywhere. It is set automatically by the
   * <code>constructorJNI()</code> method and set to 0 by the <code>destructorJNI()</code>
   * method.
   */
  long cppEnginePointer = 0;

  CAS cas = null;

  boolean hasNext = false;

  // creation of TAF resource manager
  private static native void createResourceManagerJNI() throws InternalTafException;

  // configuration of the TAF resource manager
  private static native void configureResourceManagerJNI(String workDir, String dataDir)
          throws InternalTafException;

  // constructor
  private native void constructorJNI() throws InternalTafException;

  // destructor
  private native void destructorJNI() throws InternalTafException;

  // TAF functions
  private native void initializeJNI(String configFile) throws InternalTafException;

  private native void typeSystemInitJNI(String[] typeNames, String[] featureNames,
          int[] typeInheritance, int[] featDecls, int topTypeCode, int[] featureOffsets,
          int[] typeOrder, int[] stringSubTypes, String[] stringSubTypeValues,
          int[] stringSubTypeValuePos, String[] indexNames, int[] nameToIndexMap,
          int[] indexingStrategy, int[] comparatorIndex, int[] comparators)
          throws InternalTafException;

  private native void destroyJNI() throws InternalTafException;

  private native void resetJNI() throws InternalTafException;

  /*
   * private native void fillCASJNI(int[] heapArray, int[] fsIndex, String[] stringTable);
   */
  private native void fillCASJNI(int[] heapArray, int[] fsIndex, String[] stringTable,
          byte[] byteArray, short[] shortArray, long[] longArray);

  private native void processJNI(int isTCas, String sofaName, int[] resultSpecTypes,
          int[] resultSpecFeatures) throws InternalTafException;

  // set the parameter to true if you want per document data to be serialized
  // false only for type system and index definitions serialization
  private native void serializeCASJNI(boolean bSerializeData) throws InternalTafException;

  private native Object getSerializedDataJNI(int what) throws InternalTafException;

  private native void batchProcessCompleteJNI() throws InternalTafException;

  private native void collectionProcessCompleteJNI() throws InternalTafException;

  // JNI calls to support segmenter
  private native boolean hasNextSegmentJNI() throws InternalTafException;

  private native void nextSegmentJNI() throws InternalTafException;

  private native void serializeSegmentJNI(boolean bSerializeData) throws InternalTafException;

  private native Object getSerializedSegmentDataJNI(int what) throws InternalTafException;

  private native void releaseSegmentJNI() throws InternalTafException;

  /*
   * serialized CAS data components - supported args to getSerializedDataJNI and
   * getSegmentSerializedDataJNI. Many of these are unread and have been commented out but left here
   * for documentation purposes.
   */
  // private static final int TYPE_INH = 0;
  //
  // private static final int FEATURE_DEF = 1;
  //
  // private static final int FEATURE_OFFSET = 2;
  //
  // private static final int TYPE_SYMBOLS = 3;
  //
  // private static final int FEATURE_SYMBOLS = 4;
  //
  // private static final int TOPTYPE = 5;
  //
  // private static final int TYPE_PRIORITIES = 6;
  private static final int FSHEAP = 10;

  private static final int STRINGSYMBOL = 11;

  // private static final int DOCUMENT = 20;

  private static final int INDEXEDFSS = 30;

  // private static final int INDEXID = 40;
  //
  // private static final int INDEXKIND = 41;
  //
  // private static final int COMPARATORDEF = 42;
  //
  // private static final int COMPARATORSTART = 43;

  private static final int BYTEHEAP = 12;

  private static final int SHORTHEAP = 13;

  private static final int LONGHEAP = 14;

  // helper functions
  private static native String getErrorMessageJNI(long errorId) throws InternalTafException;

  private static native String getVersionJNI() throws InternalTafException;

  /**
   * Create the TAF resource manager.
   */
  private static void createResourceManager() throws UimacppException {
    try {
      createResourceManagerJNI();
    } catch (Exception exc) {
      throwJTafException(exc);
    }
  }

  /**
   * Configure the TAF Resource Manager.
   * @param workDirectory the work directory
   * @param dataDirectory the data directory
   * @throws UimacppException wraps any exception
   */
  public static void configureResourceManager(String workDirectory, String dataDirectory)
          throws UimacppException {
    try {
      configureResourceManagerJNI(workDirectory, dataDirectory);
    } catch (Exception exc) {
      throwJTafException(exc);
    }
  }

  public UimacppEngine() throws UimacppException {
    try {
      constructorJNI();
    } catch (Exception exc) {
      throwJTafException(exc);
    }
  }

  // ////////////////////////////////////////////////////////
  // implementation of the AE interface
  // getAnalysisEngineMetaData, getView, process, reset, and destroy.

  /**
   * create a TAF engine with a config file
   * @param configFile the configuration file used for initialization
   * @throws UimacppException pass thru
   */
  public static UimacppEngine createJTafTAE(String configFile) throws UimacppException {
    UimacppEngine result = new UimacppEngine();
    result.initialize(configFile);

    return result;
  }

  static void printArray(String s, int[] ar) {
    System.out.println("int array " + s);
    for (int i = 0; i < ar.length; ++i) {
      System.out.println(i + ": " + ar[i]);
    }
    System.out.println();
  }

  static void printArray(String s, String[] ar) {
    System.out.println("String array " + s);
    for (int i = 0; i < ar.length; ++i) {
      System.out.println(i + ": " + ar[i]);
    }
    System.out.println();
  }

  // ////////////////////////////////////////////////////////

  /**
   * initialize Taf engine
   * 
   * @param configFile
   *          the configuration as a string (not a filename)
   */
  void initialize(String config) throws UimacppException {
    // for (int i = 0; i < casMgrSerializer.indexNames.length; i++) {
    // System.out.println(casMgrSerializer.indexNames[i]);
    // }
    try {
      initializeJNI(config);
    } catch (Exception exc) {
      throwJTafException(exc);
    }
  }

  // ////////////////////////////////////////////////////////

  /**
   * reinit Taf engine type system
   * 
   * @param serialized
   *          CAS definition
   */
  void typeSystemInit(CASMgrSerializer casMgrSerializer) throws UimacppException {
    // for (int i = 0; i < casMgrSerializer.indexNames.length; i++) {
    // System.out.println(casMgrSerializer.indexNames[i]);

    try {
      typeSystemInitJNI(casMgrSerializer.typeNames, casMgrSerializer.featureNames,
              casMgrSerializer.typeInheritance, casMgrSerializer.featDecls,
              casMgrSerializer.topTypeCode, casMgrSerializer.featureOffsets,
              casMgrSerializer.typeOrder, casMgrSerializer.stringSubtypes,
              casMgrSerializer.stringSubtypeValues, casMgrSerializer.stringSubtypeValuePos,
              casMgrSerializer.indexNames, casMgrSerializer.nameToIndexMap,
              casMgrSerializer.indexingStrategy, casMgrSerializer.comparatorIndex,
              casMgrSerializer.comparators);
    } catch (Exception exc) {
      throwJTafException(exc);
    }
  }

  /**
   * de-initializes the TAF engine.
   * @throws UimacppException wraps any exception
   */
  public void destroy() throws UimacppException {
    try {
      destroyJNI();
    } catch (Exception exc) {
      throwJTafException(exc);
    }
  }

  private static void serializeResultSpecification(ResultSpecification rs, CASImpl cas,
          IntVector resultSpecTypes, IntVector resultSpecFeatures) {
    TypeOrFeature[] tofs = rs.getResultTypesAndFeatures();
    TypeSystemImpl tsImpl = cas.getTypeSystemImpl();
    for (int i = 0; i < tofs.length; ++i) {
      if (tofs[i].isType()) {
        TypeImpl t = (TypeImpl) tsImpl.getType(tofs[i].getName());
        resultSpecTypes.add(t.getCode());
      } else {
        FeatureImpl f = (FeatureImpl) tsImpl.getFeatureByFullName(tofs[i].getName());
        resultSpecFeatures.add(f.getCode());
      }
    }
  }

  /**
   * process the document.
   * @param rs the result specification
   * @param aCas the CAS
   * @param casIsEmpty tbd
   * @throws UimacppException wraps any exception
   */
  public void process(ResultSpecification rs, CAS aCas, boolean casIsEmpty) throws UimacppException {

    int isTCas=0;
    String sofaName=aCas.getViewName();
    if (sofaName != null) {
      isTCas=1;
    }

    cas = aCas.getCurrentView();

    try {
      resetJNI();

      if (!casIsEmpty) {
        CASSerializer casSerializerIn = Serialization.serializeCAS(cas);
        /**
         * fillCASJNI(casSerializerIn.heapArray, casSerializerIn.fsIndex,
         * casSerializerIn.stringTable);
         */

        fillCASJNI(casSerializerIn.heapArray, casSerializerIn.fsIndex, casSerializerIn.stringTable,
                casSerializerIn.byteHeapArray, casSerializerIn.shortHeapArray,
                casSerializerIn.longHeapArray);
      }

      IntVector resultSpecTypes = new IntVector();
      IntVector resultSpecFeatures = new IntVector();
      if (rs != null) {

        serializeResultSpecification(rs, (CASImpl) cas, resultSpecTypes, resultSpecFeatures);
      }

      processJNI(isTCas, sofaName, resultSpecTypes.toArray(), resultSpecFeatures.toArray());

      // call hasNext() to see if this returns segments
      // if there are no segments this will get the
      // CAS data.
      if (hasNext()) {
        return;
      }

    } catch (Exception exc) {
      throwJTafException(exc);
    }
  }

  /**
   * hasNext
   * @return true if there's a next element
   * @throws UimacppException wraps any exception
   */
  public boolean hasNext() throws UimacppException {

    try {
      hasNext = hasNextSegmentJNI();
      // get the CAS data of the original input CAS.
      if (!hasNext) {
        CASSerializer casSerializerOut = new CASSerializer();
        // per document data
        serializeCASJNI(true);
        casSerializerOut.heapMetaData = null;
        casSerializerOut.heapArray = (int[]) getSerializedDataJNI(FSHEAP);
        casSerializerOut.fsIndex = (int[]) getSerializedDataJNI(INDEXEDFSS);
        casSerializerOut.stringTable = (String[]) getSerializedDataJNI(STRINGSYMBOL);

        casSerializerOut.byteHeapArray = (byte[]) getSerializedDataJNI(BYTEHEAP);
        casSerializerOut.shortHeapArray = (short[]) getSerializedDataJNI(SHORTHEAP);
        casSerializerOut.longHeapArray = (long[]) getSerializedDataJNI(LONGHEAP);

        CASMgr casMgr = (CASMgr) cas;
        CAS newCAS = Serialization.createCAS(casMgr, casSerializerOut);
        if (newCAS != casMgr) {
          throw new RuntimeException("CASMgr and CAS should be identical");
        }
      }
    } catch (Exception exc) {
      throwJTafException(exc);
    }
    return hasNext;
  }

  /**
   * next
   * @param segment tbd 
   * @throws UimacppException wraps any exception
   */
  public void next(CAS segment) throws UimacppException {

    try {

      // get the CAS data of CAS produce by segmenter component.
      if (hasNext) {

        nextSegmentJNI();
        CASSerializer casSerializerOut = new CASSerializer();
        // per document data
        serializeSegmentJNI(true);
        casSerializerOut.heapMetaData = null;
        casSerializerOut.heapArray = (int[]) getSerializedSegmentDataJNI(FSHEAP);
        casSerializerOut.fsIndex = (int[]) getSerializedSegmentDataJNI(INDEXEDFSS);
        casSerializerOut.stringTable = (String[]) getSerializedSegmentDataJNI(STRINGSYMBOL);

        casSerializerOut.byteHeapArray = (byte[]) getSerializedSegmentDataJNI(BYTEHEAP);
        casSerializerOut.shortHeapArray = (short[]) getSerializedSegmentDataJNI(SHORTHEAP);
        casSerializerOut.longHeapArray = (long[]) getSerializedSegmentDataJNI(LONGHEAP);
        releaseSegmentJNI();

        CASMgr casMgr = (CASMgr) segment;
        CAS newCAS = Serialization.createCAS(casMgr, casSerializerOut);
        if (newCAS != casMgr) {
          throw new RuntimeException("CASMgr and CAS should be identical");
        }
      } else {
        throw new RuntimeException("This analysis component has no CASs to return.");
      }
    } catch (Exception exc) {
      throwJTafException(exc);
    }

  }

  /**
   * batchProcessComplete
   * @throws UimacppException wraps any exception
   */
  public void batchProcessComplete() throws UimacppException {
    try {
      batchProcessCompleteJNI();
    } catch (Exception exc) {
      throwJTafException(exc);
    }
  }

  /**
   * CasConsumer collectionProcessComplete
   * @throws UimacppException wraps any exception
   */
  public void collectionProcessComplete() throws UimacppException {
    try {
      collectionProcessCompleteJNI();
    } catch (Exception exc) {
      throwJTafException(exc);
    }
  }

  /**
   * helper function to get the error message for some TAF error ID.
   * @param errorCode the code used as the key to look up the error message
   * @return the error message
   * @throws UimacppException wraps any exception
   */
  public static String getErrorMessage(long errorCode) throws UimacppException {
    try {
      return getErrorMessageJNI(errorCode);
    } catch (Exception exc) {
      throwJTafException(exc);
    }
    return null;
  }

  /**
   * helper function to get the TAF JNI version.
   * @throws UimacppException wraps any exception
   */
  public static String getTafJNIVersion() throws UimacppException {
    try {
      return getVersionJNI();
    } catch (Exception exc) {
      throwJTafException(exc);
    }
    return null;
  }

  static void throwJTafException(Exception exc) throws UimacppException {
    if (exc instanceof InternalTafException) {
      InternalTafException itExc = (InternalTafException) exc;

      // check special errors
      long l = itExc.getTafErrorCode();
      switch ((int) l) {
        case 1000:
        case 5000:
        case 10000:
          throw new OutOfMemoryError();
      }

      // TafException tafException = new TafException(
      // itExc.getTafErrorCode(), exc.getMessage() );
      throw new UimacppException(itExc);
    } else {
      throw new UimacppException(exc);
    }
  }

  // //////////////////////////////////////////////////
  protected void finalize() throws Throwable {
    synchronized (this) {
      if (cppEnginePointer != 0) {
        try {
          // sets cppEnginePointer to 0
          destructorJNI();
        } catch (Exception exc) {
          throw new UIMARuntimeException(exc);
        }
      }
      super.finalize();
    }
  }

}

class InternalTafException extends Exception {
  private static final long serialVersionUID = -6558646639254861394L;

  long errorCode = -1;

  public InternalTafException(String message, long error) {
    super(message + " (" + error + ")");
    errorCode = error;
  }

  public long getTafErrorCode() {
    return errorCode;
  }

  public String getMessage() {
    return super.getMessage();
  }

}
