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

import java.io.IOException;
import java.util.Arrays;
import java.util.Random;
import org.apache.lucene.store.DataInput;
import org.apache.lucene.store.DataOutput;
import org.apache.lucene.store.IOContext;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.store.IndexOutput;
import org.apache.lucene.tests.store.BaseDirectoryWrapper;
import org.apache.lucene.tests.store.MockDirectoryWrapper;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.RamUsageTester;
import org.apache.lucene.tests.util.TestUtil;
import org.junit.Ignore;

public class TestPagedBytes extends LuceneTestCase {

  // Writes random byte/s to "normal" file in dir, then
  // copies into PagedBytes and verifies with
  // PagedBytes.Reader:
  public void testDataInputOutput() throws Exception {
    Random random = random();
    int numIters = atLeast(1);
    for (int iter = 0; iter < numIters; iter++) {
      BaseDirectoryWrapper dir = newFSDirectory(createTempDir("testOverflow"));
      if (dir instanceof MockDirectoryWrapper) {
        ((MockDirectoryWrapper) dir).setThrottling(MockDirectoryWrapper.Throttling.NEVER);
      }
      final int blockBits = TestUtil.nextInt(random, 1, 20);
      final int blockSize = 1 << blockBits;
      final PagedBytes p = new PagedBytes(blockBits);
      final IndexOutput out = dir.createOutput("foo", IOContext.DEFAULT);
      final int numBytes;
      if (TEST_NIGHTLY) {
        numBytes = TestUtil.nextInt(random(), 2, 10_000_000);
      } else {
        numBytes = TestUtil.nextInt(random(), 2, 1_000_000);
      }

      final byte[] answer = new byte[numBytes];
      random.nextBytes(answer);
      int written = 0;
      while (written < numBytes) {
        if (random.nextInt(100) == 7) {
          out.writeByte(answer[written++]);
        } else {
          int chunk = Math.min(random.nextInt(1000), numBytes - written);
          out.writeBytes(answer, written, chunk);
          written += chunk;
        }
      }

      out.close();
      final IndexInput input = dir.openInput("foo", IOContext.DEFAULT);
      final DataInput in = input.clone();

      p.copy(input, input.length());
      final PagedBytes.Reader reader = p.freeze(random.nextBoolean());

      final byte[] verify = new byte[numBytes];
      int read = 0;
      while (read < numBytes) {
        if (random.nextInt(100) == 7) {
          verify[read++] = in.readByte();
        } else {
          int chunk = Math.min(random.nextInt(1000), numBytes - read);
          in.readBytes(verify, read, chunk);
          read += chunk;
        }
      }
      assertTrue(Arrays.equals(answer, verify));

      final BytesRef slice = new BytesRef();
      for (int iter2 = 0; iter2 < 100; iter2++) {
        final int pos = random.nextInt(numBytes - 1);
        assertEquals(answer[pos], reader.getByte(pos));
        final int len = random.nextInt(Math.min(blockSize + 1, numBytes - pos));
        reader.fillSlice(slice, pos, len);
        for (int byteUpto = 0; byteUpto < len; byteUpto++) {
          assertEquals(answer[pos + byteUpto], slice.bytes[slice.offset + byteUpto]);
        }
      }
      input.close();
      dir.close();
    }
  }

  // Writes random byte/s into PagedBytes via
  // .getDataOutput(), then verifies with
  // PagedBytes.getDataInput():
  public void testDataInputOutput2() throws Exception {
    Random random = random();
    int numIters = atLeast(1);
    for (int iter = 0; iter < numIters; iter++) {
      final int blockBits = TestUtil.nextInt(random, 1, 20);
      final int blockSize = 1 << blockBits;
      final PagedBytes p = new PagedBytes(blockBits);
      final DataOutput out = p.getDataOutput();
      final int numBytes;
      if (TEST_NIGHTLY) {
        numBytes = random().nextInt(10_000_000);
      } else {
        numBytes = random().nextInt(1_000_000);
      }

      final byte[] answer = new byte[numBytes];
      random().nextBytes(answer);
      int written = 0;
      while (written < numBytes) {
        if (random().nextInt(10) == 7) {
          out.writeByte(answer[written++]);
        } else {
          int chunk = Math.min(random().nextInt(1000), numBytes - written);
          out.writeBytes(answer, written, chunk);
          written += chunk;
        }
      }

      final PagedBytes.Reader reader = p.freeze(random.nextBoolean());

      final DataInput in = p.getDataInput();

      final byte[] verify = new byte[numBytes];
      int read = 0;
      while (read < numBytes) {
        if (random().nextInt(10) == 7) {
          verify[read++] = in.readByte();
        } else {
          int chunk = Math.min(random().nextInt(1000), numBytes - read);
          in.readBytes(verify, read, chunk);
          read += chunk;
        }
      }
      assertTrue(Arrays.equals(answer, verify));

      final BytesRef slice = new BytesRef();
      for (int iter2 = 0; iter2 < 100; iter2++) {
        final int pos = random.nextInt(numBytes - 1);
        final int len = random.nextInt(Math.min(blockSize + 1, numBytes - pos));
        reader.fillSlice(slice, pos, len);
        for (int byteUpto = 0; byteUpto < len; byteUpto++) {
          assertEquals(answer[pos + byteUpto], slice.bytes[slice.offset + byteUpto]);
        }
      }

      // test skipping
      final DataInput in2 = p.getDataInput();
      int maxSkipTo = numBytes - 1;
      // skip chunks of bytes until exhausted
      for (int curr = 0; curr < maxSkipTo; ) {
        int skipTo = TestUtil.nextInt(random, curr, maxSkipTo);
        int step = skipTo - curr;
        in2.skipBytes(step);
        assertEquals(answer[skipTo], in2.readByte());
        curr = skipTo + 1; // +1 for read byte
      }
    }
  }

  @Ignore // memory hole
  public void testOverflow() throws IOException {
    BaseDirectoryWrapper dir = newFSDirectory(createTempDir("testOverflow"));
    if (dir instanceof MockDirectoryWrapper) {
      ((MockDirectoryWrapper) dir).setThrottling(MockDirectoryWrapper.Throttling.NEVER);
    }
    final int blockBits = TestUtil.nextInt(random(), 14, 28);
    final int blockSize = 1 << blockBits;
    byte[] arr = new byte[TestUtil.nextInt(random(), blockSize / 2, blockSize * 2)];
    for (int i = 0; i < arr.length; ++i) {
      arr[i] = (byte) i;
    }
    final long numBytes = (1L << 31) + TestUtil.nextInt(random(), 1, blockSize * 3);
    final PagedBytes p = new PagedBytes(blockBits);
    final IndexOutput out = dir.createOutput("foo", IOContext.DEFAULT);
    for (long i = 0; i < numBytes; ) {
      assertEquals(i, out.getFilePointer());
      final int len = (int) Math.min(arr.length, numBytes - i);
      out.writeBytes(arr, len);
      i += len;
    }
    assertEquals(numBytes, out.getFilePointer());
    out.close();
    final IndexInput in = dir.openInput("foo", IOContext.DEFAULT);
    p.copy(in, numBytes);
    final PagedBytes.Reader reader = p.freeze(random().nextBoolean());

    for (long offset :
        new long[] {
          0L, Integer.MAX_VALUE, numBytes - 1, TestUtil.nextLong(random(), 1, numBytes - 2)
        }) {
      BytesRef b = new BytesRef();
      reader.fillSlice(b, offset, 1);
      assertEquals(arr[(int) (offset % arr.length)], b.bytes[b.offset]);
    }
    in.close();
    dir.close();
  }

  public void testRamBytesUsed() {
    final int blockBits = TestUtil.nextInt(random(), 4, 22);
    PagedBytes b = new PagedBytes(blockBits);
    final int totalBytes = random().nextInt(10000);
    for (long pointer = 0; pointer < totalBytes; ) {
      BytesRef bytes = newBytesRef(TestUtil.randomSimpleString(random(), 10));
      pointer = b.copyUsingLengthPrefix(bytes);
    }
    assertEquals(RamUsageTester.ramUsed(b), b.ramBytesUsed());
    final PagedBytes.Reader reader = b.freeze(random().nextBoolean());
    assertEquals(RamUsageTester.ramUsed(b), b.ramBytesUsed());
    assertEquals(RamUsageTester.ramUsed(reader), reader.ramBytesUsed());
  }
}
