import java.io.RandomAccessFile;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.Channel;
import java.nio.channels.WritableByteChannel;
import java.nio.ByteBuffer;
import java.lang.reflect.Constructor;
import java.util.Random;

/**
 * @author yonik
 * @version $Id$
 */
public class FileReadTest {

  public static void main(String[] args) throws Exception {
    int argpos=0;
    final String filename = args[argpos++];
    final String implementation = args[argpos++];
    final boolean serial = Boolean.parseBoolean(args[argpos++]);
    final int nThreads = Integer.parseInt(args[argpos++]);
    final int iterations = Integer.parseInt(args[argpos++]);
    final int bufsize=argpos<args.length ? Integer.parseInt(args[argpos++]) : 1024;
    MyFile.BUFFER_SIZE = bufsize;

    final RandomAccessFile f = new RandomAccessFile(filename,"r");
    final long filelen = f.length();
    final int nPages = (int)((filelen-1) / bufsize)+1;

    Class clazz = Class.forName(implementation);
    Constructor con = clazz.getConstructor(new Class[]{RandomAccessFile.class});
    final MyFile myfile = (MyFile)con.newInstance(f);
    final int[] answer = new int[1];

    Thread[] threads = new Thread[nThreads];
    for (int i=0; i<threads.length; i++) {
      final int seed = i;
      threads[i] = new Thread() {
        Random r = new Random(seed);
        MyFile.MyReader reader = myfile.getReader(); // each thread gets one
        public void run() {
          int pg = r.nextInt(nPages);
          int val=0;
          for (int n=0; n<iterations; n++) {
            for (int j=0; j<nPages; j++) {
              int rn = r.nextInt(nPages);
              if (++pg>=nPages) pg-=nPages;
              long pos = (serial ? pg : rn) * bufsize;
              try {
                val += reader.read(pos);
              } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException(e);
              }
            }
          }
          synchronized (answer) { answer[0]+=val; }
        }
      };
    }

    long start = System.currentTimeMillis();
    for (Thread th : threads) th.start();
    for (Thread th : threads) th.join();
    long end = System.currentTimeMillis();

    long ms = end-start;
    int a;
    synchronized(answer) { a=answer[0]; }

    System.out.println("config:"
          + " impl=" + implementation
          + " serial="+serial + " nThreads="+nThreads +" iterations="+iterations
          + " bufsize="+bufsize + " filelen=" + filelen
    );
    System.out.println("answer=" + a
                    + ", ms=" + ms
                    + ", MB/sec="+(filelen*iterations*nThreads)/(((double)ms)/1000)/1000000
    );

  }
}


abstract class MyFile {
  public final RandomAccessFile f;
  long filepos=0;  // current position in the file, used by some impls
  public static int BUFFER_SIZE=1024;
  public MyFile(RandomAccessFile f) {
    this.f = f;  
  }

  abstract MyReader getReader();

  abstract class MyReader {
    public abstract int read(long pos) throws IOException;
  }
}

class ClassicFile extends MyFile {
  public ClassicFile(RandomAccessFile f) {
    super(f);
  }

  MyReader getReader() {
    return new MyReader() {
      byte[] b = new byte[BUFFER_SIZE];
      public int read(long pos) throws IOException {
        int len;
        synchronized(f) {
          if (pos != filepos) f.seek(pos);
          len = f.read(b,0,BUFFER_SIZE);
          filepos = pos + len;
        }
        return b[0]+b[len>>1];
      }
    };
  }
}


class ChannelFile extends MyFile {
  final FileChannel channel;
  public ChannelFile(RandomAccessFile f) {
    super(f);
    channel = f.getChannel();
  }

  MyReader getReader() {
    return new MyReader() {
      byte[] b = new byte[BUFFER_SIZE];
      ByteBuffer bb = ByteBuffer.wrap(b);

      public int read(long pos) throws IOException {
        bb.clear();
        int len;
        synchronized(channel) {
          if (filepos != pos) channel.position(pos);
          len = channel.read(bb);
          filepos = pos + len;
        }
        return b[0]+b[len>>1];
      }
    };
  }
}


class ChannelPread extends MyFile {
  final FileChannel channel;
  public ChannelPread(RandomAccessFile f) {
    super(f);
    channel = f.getChannel();
  }

  MyReader getReader() {
    return new MyReader() {
      byte[] b = new byte[BUFFER_SIZE];
      ByteBuffer bb = ByteBuffer.wrap(b);

      public int read(long pos) throws IOException {
        bb.clear();
        int len = channel.read(bb, pos);
        return b[0]+b[len>>1];
      }
    };
  }
}


class ChannelTransfer extends MyFile {
  final FileChannel channel;
  public ChannelTransfer(RandomAccessFile f) {
    super(f);
    channel = f.getChannel();
  }

  MyReader getReader() {
    return new MyReader() {
      final byte[] b = new byte[BUFFER_SIZE];
      int posInBuffer=0;
      final ByteBuffer bb = ByteBuffer.wrap(b);

      final WritableByteChannel sink = new WritableByteChannel() {
        public int write(ByteBuffer src) throws IOException {
          int remaining = src.remaining();
          src.get(b,posInBuffer,remaining);
          // may be called multiple times, so we need to keep track
          posInBuffer += remaining;
          return remaining;
        }
        public boolean isOpen() {return true;}
        public void close() throws IOException {}
      };

      public int read(long pos) throws IOException {
        posInBuffer=0;
        int len = (int)channel.transferTo(pos, BUFFER_SIZE, sink);
        return b[0]+b[len>>1];
      }
    };
  }
}

