/*
 *  This file is part of the Jikes RVM project (http://jikesrvm.org).
 *
 *  This file is licensed to You under the Common Public License (CPL);
 *  You may not use this file except in compliance with the License. You
 *  may obtain a copy of the License at
 *
 *      http://www.opensource.org/licenses/cpl1.0.php
 *
 *  See the COPYRIGHT.txt file distributed with this work for information
 *  regarding copyright ownership.
 */
package org.jikesrvm.jni;

import org.jikesrvm.VM;
import static org.jikesrvm.SizeConstants.BYTES_IN_ADDRESS;
import org.jikesrvm.classloader.UTF8Convert;
import org.jikesrvm.runtime.Magic;
import org.jikesrvm.runtime.Memory;
import org.jikesrvm.util.StringUtilities;
import org.vmmagic.unboxed.Address;
import org.vmmagic.unboxed.Offset;
import org.vmmagic.unboxed.Word;

import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;

/**
 * Platform independent utility functions called from JNIFunctions
 * (cannot be placed in JNIFunctions because methods
 * there are specially compiled to be called from native).
 *
 * @see JNIFunctions
 */
public abstract class JNIGenericHelpers {

  /**
   * Compute the length of the given null-terminated string
   *
   * @param ptr address of string in memory
   * @return the length of the string in bytes
   */
  public static int strlen(Address ptr) {
    int length=0;
    // align address to size of machine
    while (!ptr.toWord().and(Word.fromIntZeroExtend(BYTES_IN_ADDRESS - 1)).isZero()) {
      byte bits = ptr.loadByte(Offset.fromIntZeroExtend(length));
      if (bits == 0) {
        return length;
      }
      length++;
    }
    // Ascii characters are normally in the range 1 to 128, if we subtract 1
    // from each byte and look if the top bit of the byte is set then if it is
    // the chances are the byte's value is 0. Loop over words doing this quick
    // test and then do byte by byte tests when we think we have the 0
    Word onesToSubtract;
    Word maskToTestHighBits;
    if (VM.BuildFor32Addr) {
      onesToSubtract     = Word.fromIntZeroExtend(0x01010101);
      maskToTestHighBits = Word.fromIntZeroExtend(0x80808080);
    } else {
      onesToSubtract     = Word.fromLong(0x0101010101010101L);
      maskToTestHighBits = Word.fromLong(0x8080808080808080L);
    }
    while (true) {
      Word bytes = ptr.loadWord(Offset.fromIntZeroExtend(length));
      if(!bytes.minus(onesToSubtract).and(maskToTestHighBits).isZero()) {
        if (VM.LittleEndian) {
          for(int byteOff=0; byteOff < BYTES_IN_ADDRESS; byteOff++) {
            if(bytes.and(Word.fromIntZeroExtend(0xFF)).isZero()) {
              return length + byteOff;
            }
            bytes = bytes.rshl(8);
          }
        } else {
          for(int byteOff=BYTES_IN_ADDRESS-1; byteOff >= 0; byteOff--) {
            if(bytes.rshl(byteOff*8).and(Word.fromIntZeroExtend(0xFF)).isZero()) {
              return length + (BYTES_IN_ADDRESS - 1 - byteOff);
            }
          }
        }
      }
      length += BYTES_IN_ADDRESS;
    }
  }
  /**
   * Given an address in C that points to a null-terminated string,
   * create a new Java byte[] with a copy of the string.
   *
   * @param stringAddress an address in C space for a string
   * @return a new Java byte[]
   */
  public static byte[] createByteArrayFromC(Address stringAddress) {

    int length = strlen(stringAddress);
    byte[] contents = new byte[length];
    Memory.memcopy(Magic.objectAsAddress(contents), stringAddress, length);

    return contents;
  }

  /**
   * Create a string from the given charset decoder and bytebuffer
   */
  private static String createString(CharsetDecoder csd, ByteBuffer bbuf) throws CharacterCodingException {
    char[] v;
    int o;
    int c;
    CharBuffer cbuf = csd.decode(bbuf);
    if(cbuf.hasArray()) {
      v = cbuf.array();
      o = cbuf.position();
      c = cbuf.remaining();
    } else {
      // Doubt this will happen. But just in case.
      v = new char[cbuf.remaining()];
      cbuf.get(v);
      o = 0;
      c = v.length;
    }
    return java.lang.JikesRVMSupport.newStringWithoutCopy(v, o, c);
  }
  /**
   * Given an address in C that points to a null-terminated string,
   * create a new Java String with a copy of the string.
   *
   * @param stringAddress an address in C space for a string
   * @return a new Java String
   */
  public static String createStringFromC(Address stringAddress) {
    if (VM.fullyBooted) {
      try {
        String encoding = System.getProperty("file.encoding");
        CharsetDecoder csd = Charset.forName(encoding).newDecoder();
        csd.onMalformedInput(CodingErrorAction.REPLACE);
        csd.onUnmappableCharacter(CodingErrorAction.REPLACE);
        ByteBuffer bbuf =
          java.nio.JikesRVMSupport.newDirectByteBuffer(stringAddress,
                                                       strlen(stringAddress));
        return createString(csd, bbuf);
      } catch(Exception ex){
        // Any problems fall through to default encoding
      }
    }
    // Can't do real Char encoding until VM is fully booted.
    // All Strings encountered during booting must be ascii
    byte[] tmp = createByteArrayFromC(stringAddress);
    return StringUtilities.asciiBytesToString(tmp);
  }
  /**
   * Given an address in C that points to a null-terminated string,
   * create a new UTF encoded Java String with a copy of the string.
   *
   * @param stringAddress an address in C space for a string
   * @return a new Java String
   */
  public static String createUTFStringFromC(Address stringAddress) {
    final boolean USE_LIBRARY_CODEC = false;
    byte[] tmp;
    ByteBuffer bbuf;
    if (VM.fullyBooted) {
      try {
        bbuf = java.nio.JikesRVMSupport.newDirectByteBuffer(stringAddress,
                                                            strlen(stringAddress));
        if (USE_LIBRARY_CODEC) {
          CharsetDecoder csd = Charset.forName("UTF8").newDecoder();
          return createString(csd, bbuf);
        } else {
          return UTF8Convert.fromUTF8(bbuf);
        }
      } catch(Exception ex){
        // Any problems fall through to default encoding
      }
    }
    // Can't do real Char encoding until VM is fully booted.
    // All Strings encountered during booting must be ascii
    tmp = createByteArrayFromC(stringAddress);
    return StringUtilities.asciiBytesToString(tmp);
  }


  /**
   * Convert a String into a a malloced region
   */
  public static void createUTFForCFromString(String str, Address copyBuffer, int len) {
    ByteBuffer bbuf =
      java.nio.JikesRVMSupport.newDirectByteBuffer(copyBuffer, len);

    final boolean USE_LIBRARY_CODEC = false;
    if (USE_LIBRARY_CODEC) {
      char[] strChars = java.lang.JikesRVMSupport.getBackingCharArray(str);
      int strOffset = java.lang.JikesRVMSupport.getStringOffset(str);
      int strLen = java.lang.JikesRVMSupport.getStringLength(str);
      CharBuffer cbuf = CharBuffer.wrap(strChars, strOffset, strLen);
      CharsetEncoder cse = Charset.forName("UTF8").newEncoder();
      cse.encode(cbuf, bbuf, true);
    } else {
      UTF8Convert.toUTF8(str, bbuf);
    }
    // store terminating zero
    copyBuffer.store((byte)0, Offset.fromIntZeroExtend(len-1));
  }

  /**
   * A JNI helper function, to set the value pointed to by a C pointer
   * of type (jboolean *).
   * @param boolPtr Native pointer to a jboolean variable to be set.   May be
   *            the NULL pointer, in which case we do nothing.
   * @param val Value to set it to (usually TRUE)
   *
   */
  static void setBoolStar(Address boolPtr, boolean val) {
    if (boolPtr.isZero()) {
      return;
    }
    if (val) {
      boolPtr.store((byte)1);
    } else {
      boolPtr.store((byte)0);
    }
  }
}
