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

import org.mmtk.plan.TraceLocal;
import org.mmtk.utility.Constants;
import org.mmtk.utility.Log;
import org.jikesrvm.VM;
import org.jikesrvm.runtime.BootRecord;
import org.jikesrvm.runtime.Magic;
import org.jikesrvm.scheduler.Scheduler;
import org.jikesrvm.mm.mminterface.CollectorThread;
import org.jikesrvm.mm.mminterface.MemoryManager;

import org.vmmagic.unboxed.*;
import org.vmmagic.pragma.*;

/**
 * Scan the boot image for references using the boot image reference map
 */
public class ScanBootImage implements Constants {

  private static final boolean DEBUG = false;
  private static final boolean FILTER = true;

  private static final int LOG_CHUNK_BYTES = 12;
  private static final int CHUNK_BYTES = 1<<LOG_CHUNK_BYTES;
  private static final int LONGENCODING_MASK = 0x1;
  private static final int RUN_MASK = 0x2;
  private static final int MAX_RUN = (1<<BITS_IN_BYTE)-1;
  private static final int LONGENCODING_OFFSET_BYTES = 4;
  private static final int GUARD_REGION = LONGENCODING_OFFSET_BYTES + 1; /* long offset + run encoding */

  /* statistics */
  static int roots = 0;
  static int refs = 0;

  /****************************************************************************
   *
   * GC-time decoding (multi-threaded)
   */

  /**
   * Scan the boot image for object references.  Executed by
   * all GC threads in parallel, with each doing a portion of the
   * boot image.
   *
   * @param trace The trace object to which the roots should be added
   */
  @Inline
  @Uninterruptible
  public static void scanBootImage(TraceLocal trace) {
    /* establish sentinals in map & image */
    Address mapStart = BootRecord.the_boot_record.bootImageRMapStart;
    Address mapEnd = BootRecord.the_boot_record.bootImageRMapEnd;
    Address imageStart = BootRecord.the_boot_record.bootImageDataStart;

    /* figure out striding */
    int stride = CollectorThread.numCollectors()<<LOG_CHUNK_BYTES;
    CollectorThread collector = Magic.threadAsCollectorThread(Scheduler.getCurrentThread());
    int start = (collector.getGCOrdinal() - 1)<<LOG_CHUNK_BYTES;
    Address cursor = mapStart.plus(start);

    /* statistics */
    roots = 0;
    refs = 0;

    /* process chunks in parallel till done */
    while (cursor.LT(mapEnd)) {
      processChunk(cursor, imageStart, mapStart, mapEnd, trace);
      cursor = cursor.plus(stride);
    }

    /* print some debugging stats */
    if (DEBUG) {
      Log.write("<boot image");
      Log.write(" roots: "); Log.write(roots);
      Log.write(" refs: "); Log.write(refs);
      Log.write(">");
    }
  }

  /**
   * Process a chunk of encoded reference data, enqueuing each
   * reference (optionally filtering them on whether they point
   * outside the boot image).
   *
   * @param chunkStart The address of the first byte of encoded data
   * @param imageStart The address of the start of the boot image
   * @param mapStart The address of the start of the encoded reference map
   * @param mapEnd The address of the end of the encoded reference map
   * @param trace The <code>TraceLocal</code> into which roots should
   * be enqueued.
   */
  @Inline
  @Uninterruptible
  private static void processChunk(Address chunkStart, Address imageStart,
      Address mapStart, Address mapEnd, TraceLocal trace) {
    int value;
    Offset offset = Offset.zero();
    Address cursor = chunkStart;
    while ((value = (cursor.loadByte() & 0xff)) != 0) {
      /* establish the offset */
      if ((value & LONGENCODING_MASK) != 0) {
        offset = decodeLongEncoding(cursor);
        cursor = cursor.plus(LONGENCODING_OFFSET_BYTES);
      } else {
        offset = offset.plus(value & 0xfc);
        cursor = cursor.plus(1);
      }
      /* figure out the length of the run, if any */
      int runlength = 0;
      if ((value & RUN_MASK) != 0) {
        runlength = cursor.loadByte() & 0xff;
        cursor = cursor.plus(1);
      }
      /* enqueue the specified slot or slots */
      if (VM.VerifyAssertions) VM._assert(isAddressAligned(offset));
      Address slot = imageStart.plus(offset);
      if (DEBUG) refs++;
      if (!FILTER || slot.loadAddress().GT(mapEnd)) {
        if (DEBUG) roots++;
        trace.processRootEdge(slot, false);
      }
      if (runlength != 0) {
        for (int i = 0; i < runlength; i++) {
          offset = offset.plus(BYTES_IN_ADDRESS);
          slot = imageStart.plus(offset);
          if (VM.VerifyAssertions) VM._assert(isAddressAligned(slot));
          if (DEBUG) refs++;
          if (!FILTER || slot.loadAddress().GT(mapEnd)) {
            if (DEBUG) roots++;
            if (ScanThread.VALIDATE_REFS) checkReference(slot);
            trace.processRootEdge(slot, false);
          }
        }
      }
    }
  }

  /**
   * Check that a reference encountered during scanning is valid.  If
   * the reference is invalid, dump stack and die.
   *
   * @param refaddr The address of the reference in question.
   */
  @Uninterruptible
  private static void checkReference(Address refaddr) {
    ObjectReference ref = org.mmtk.vm.VM.activePlan.collector().loadObjectReference(refaddr);
    if (!MemoryManager.validRef(ref)) {
      Log.writeln();
      Log.writeln("Invalid ref reported while scanning boot image");
      Log.writeln();
      Log.write(refaddr); Log.write(":"); Log.flush(); MemoryManager.dumpRef(ref);
      Log.writeln();
      Log.writeln("Dumping stack:");
      Scheduler.dumpStack();
      VM.sysFail("\n\nScanStack: Detected bad GC map; exiting RVM with fatal error");
    }
  }

  /**
   * Return true if the given offset is address-aligned
   * @param offset the offset to be check
   * @return true if the offset is address aligned.
   */
  @Uninterruptible
  private static boolean isAddressAligned(Offset offset) {
    return (offset.toLong()>>LOG_BYTES_IN_ADDRESS)<<LOG_BYTES_IN_ADDRESS == offset.toLong();
  }

  /**
   * Return true if the given address is address-aligned
   * @param address the address to be check
   * @return true if the address is address aligned.
   */
  @Uninterruptible
  private static boolean isAddressAligned(Address address) {
    return (address.toLong()>>LOG_BYTES_IN_ADDRESS)<<LOG_BYTES_IN_ADDRESS == address.toLong();
  }

  /****************************************************************************
   *
   * Build-time encoding (assumed to be single-threaded)
   */
  private static int lastOffset = 0;
  private static int oldIndex = 0;
  private static int codeIndex = 0;

  /* statistics */
  private static int shortRefs = 0;
  private static int runRefs = 0;
  private static int longRefs = 0;
  private static int startRefs = 0;

  /**
   * Take a bytemap encoding of all references in the boot image, and
   * produce an encoded byte array.  Return the total length of the
   * encoding.
   */
  public static int encodeRMap(byte[] bootImageRMap, byte[] referenceMap,
      int referenceMapLimit) {
    for (int index = 0; index <= referenceMapLimit; index++) {
      if (referenceMap[index] == 1) {
        addOffset(bootImageRMap, index<<LOG_BYTES_IN_ADDRESS);
      }
    }
    return codeIndex + 1;
  }

  /**
   * Print some basic statistics about the encoded references, for
   * debugging purposes.
   */
  public static void encodingStats() {
    if (DEBUG) {
      Log.write("refs: "); Log.writeln(startRefs + shortRefs + longRefs + runRefs);
      Log.write("start: "); Log.writeln(startRefs);
      Log.write("short: "); Log.writeln(shortRefs);
      Log.write("long: "); Log.writeln(longRefs);
      Log.write("run: "); Log.writeln(runRefs);
      Log.write("size: "); Log.writeln(codeIndex);
    }
  }

  /**
   * Encode a given offset (distance from the start of the boot image)
   * into the code array.
   *
   * @param code A byte array into which the value should be encoded
   * @param offset The offset value to be encoded
   */
  private static void addOffset(byte[] code, int offset) {
    if ((codeIndex ^ (codeIndex + GUARD_REGION)) >= CHUNK_BYTES) {
      codeIndex = (codeIndex + GUARD_REGION) & ~(CHUNK_BYTES - 1);
      oldIndex = codeIndex;
      codeIndex = encodeLongEncoding(code, codeIndex, offset);
      if (DEBUG) {
        startRefs++;
        Log.write("[chunk: "); Log.write(codeIndex);
        Log.write(" offset: "); Log.write(offset);
        Log.write(" last offset: "); Log.write(lastOffset);
        Log.writeln("]");
      }
    } else {
      int delta = offset - lastOffset;
      if (VM.VerifyAssertions) VM._assert((delta & 0x3) == 0);
      if (VM.VerifyAssertions) VM._assert(delta > 0);

      int currentrun = ((int) code[codeIndex]) & 0xff;
      if ((delta == BYTES_IN_ADDRESS) &&
          (currentrun < MAX_RUN)) {
        currentrun++;
        code[codeIndex] = (byte) (currentrun & 0xff);
        code[oldIndex] |= RUN_MASK;
        if (DEBUG) runRefs++;
      } else {
        if (currentrun != 0) codeIndex++;
        oldIndex = codeIndex;
        if (delta < 1<<BITS_IN_BYTE) {
          /* common case: single byte encoding */
          code[codeIndex++] = (byte) (delta & 0xff);
          if (DEBUG) shortRefs++;
        } else {
          /* else four byte encoding */
          codeIndex = encodeLongEncoding(code, codeIndex, offset);
          if (DEBUG) longRefs++;
        }
      }
    }
    if (offset != getOffset(code, oldIndex, lastOffset)) {
      Log.write("offset: "); Log.writeln(offset);
      Log.write("last offset: "); Log.writeln(lastOffset);
      Log.write("offset: "); Log.writeln(getOffset(code, oldIndex, lastOffset));
      Log.write("index: "); Log.writeln(oldIndex);
      Log.write("index: "); Log.writeln(oldIndex & (CHUNK_BYTES - 1));
      Log.writeln();
      Log.write("1: "); Log.writeln(code[oldIndex]);
      Log.write("2: "); Log.writeln(code[oldIndex+1]);
      Log.write("3: "); Log.writeln(code[oldIndex+2]);
      Log.write("4: "); Log.writeln(code[oldIndex+3]);
      Log.write("5: "); Log.writeln(code[oldIndex+4]);
      if (VM.VerifyAssertions)
        VM._assert(offset == getOffset(code, oldIndex, lastOffset));
    }
    lastOffset = offset;
  }

  /****************************************************************************
   *
   * Utility encoding and decoding methods
   */

  /**
   * Decode an encoded offset given the coded byte array, and index
   * into it, and the current (last) offset.
   *
   * @param code A byte array containing the encoded value
   * @param index The offset into the code array from which to
   * commence decoding
   * @param lastOffset The current (last) encoded offset
   * @return The next offset, which is either explicitly encoded in
   * the byte array or inferred from an encoded delta and the last
   * offset.
 */
  private static int getOffset(byte[] code, int index, int lastOffset) {
    if (((int) code[index] & RUN_MASK) == RUN_MASK) {
      return lastOffset + BYTES_IN_WORD;
    } else {
      if (((index & (CHUNK_BYTES - 1)) == 0) ||
          (((int) code[index] &LONGENCODING_MASK) == LONGENCODING_MASK)) {
        return decodeLongEncoding(code, index);
      } else {
        return lastOffset + (((int) code[index]) & 0xff);
      }
    }
  }

  /**
   * Decode a 4-byte encoding, taking a pointer to the first byte of
   * the encoding, and returning the encoded value as an <code>Offset</code>
   *
   * @param cursor A pointer to the first byte of encoded data
   * @return The encoded value as an <code>Offset</code>
   */
  @Inline
  @Uninterruptible
  private static Offset decodeLongEncoding(Address cursor) {
    int value;
    value  = ((int) cursor.loadByte())                                              & 0x000000fc;
    value |= ((int) cursor.loadByte(Offset.fromIntSignExtend(1))<<BITS_IN_BYTE)     & 0x0000ff00;
    value |= ((int) cursor.loadByte(Offset.fromIntSignExtend(2))<<(2*BITS_IN_BYTE)) & 0x00ff0000;
    value |= ((int) cursor.loadByte(Offset.fromIntSignExtend(3))<<(3*BITS_IN_BYTE)) & 0xff000000;
    return Offset.fromIntSignExtend(value);
  }

  /**
   * Decode a 4-byte encoding, taking a byte array and an index into
   * it and returning the encoded value as an integer
   *
   * @param code A byte array containing the encoded value
   * @param index The offset into the code array from which to
   * commence decoding
   * @return The encoded value as an integer
   */
  @Inline
  @Uninterruptible
  private static int decodeLongEncoding(byte[] code, int index) {
    int value;
    value  = ((int) code[index])                     & 0x000000fc;
    value |= ((int) code[index+1]<<BITS_IN_BYTE)     & 0x0000ff00;
    value |= ((int) code[index+2]<<(2*BITS_IN_BYTE)) & 0x00ff0000;
    value |= ((int) code[index+3]<<(3*BITS_IN_BYTE)) & 0xff000000;
    return value;
  }

  /**
   * Encode a 4-byte encoding, taking a byte array, the current index into
   * it, and the value to be encoded.
   *
   * @param code A byte array to containthe encoded value
   * @param index The current offset into the code array
   * @param value The value to be encoded
   * @return The updated index into the code array
   */
  private static int encodeLongEncoding(byte[] code, int index, int value) {
    code[index++] = (byte) ((value & 0xff) | LONGENCODING_MASK);
    value = value >>> BITS_IN_BYTE;
    code[index++] = (byte) (value & 0xff);
    value = value >>> BITS_IN_BYTE;
    code[index++] = (byte) (value & 0xff);
    value = value >>> BITS_IN_BYTE;
    code[index++] = (byte) (value & 0xff);
    return index;
  }

}
