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

import java.io.IOException;
import java.util.Arrays;
import org.apache.lucene.store.ByteArrayDataInput;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IOContext;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.store.IndexOutput;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.lucene.util.ArrayUtil;

public class TestBytesStore extends LuceneTestCase {

  public void testRandom() throws Exception {

    final int iters = atLeast(10);
    final int maxBytes = TEST_NIGHTLY ? 200000 : 20000;
    for (int iter = 0; iter < iters; iter++) {
      final int numBytes = TestUtil.nextInt(random(), 1, maxBytes);
      final byte[] expected = new byte[numBytes];
      final int blockBits = TestUtil.nextInt(random(), 8, 15);
      final BytesStore bytes = new BytesStore(blockBits);
      if (VERBOSE) {
        System.out.println(
            "TEST: iter=" + iter + " numBytes=" + numBytes + " blockBits=" + blockBits);
      }

      int pos = 0;
      while (pos < numBytes) {
        int op = random().nextInt(8);
        if (VERBOSE) {
          System.out.println("  cycle pos=" + pos);
        }
        switch (op) {
          case 0:
            {
              // write random byte
              byte b = (byte) random().nextInt(256);
              if (VERBOSE) {
                System.out.println("    writeByte b=" + b);
              }

              expected[pos++] = b;
              bytes.writeByte(b);
            }
            break;

          case 1:
            {
              // write random byte[]
              int len = random().nextInt(Math.min(numBytes - pos, 100));
              byte[] temp = new byte[len];
              random().nextBytes(temp);
              if (VERBOSE) {
                System.out.println("    writeBytes len=" + len + " bytes=" + Arrays.toString(temp));
              }
              System.arraycopy(temp, 0, expected, pos, temp.length);
              bytes.writeBytes(temp, 0, temp.length);
              pos += len;
            }
            break;

          case 2:
            {
              // write int @ absolute pos
              if (pos > 4) {
                int x = random().nextInt();
                int randomPos = random().nextInt(pos - 4);
                if (VERBOSE) {
                  System.out.println("    abs writeInt pos=" + randomPos + " x=" + x);
                }
                bytes.writeInt(randomPos, x);
                expected[randomPos++] = (byte) (x >> 24);
                expected[randomPos++] = (byte) (x >> 16);
                expected[randomPos++] = (byte) (x >> 8);
                expected[randomPos++] = (byte) x;
              }
            }
            break;

          case 3:
            {
              // reverse bytes
              if (pos > 1) {
                int len = TestUtil.nextInt(random(), 2, Math.min(100, pos));
                int start;
                if (len == pos) {
                  start = 0;
                } else {
                  start = random().nextInt(pos - len);
                }
                int end = start + len - 1;
                if (VERBOSE) {
                  System.out.println(
                      "    reverse start=" + start + " end=" + end + " len=" + len + " pos=" + pos);
                }
                bytes.reverse(start, end);

                while (start <= end) {
                  byte b = expected[end];
                  expected[end] = expected[start];
                  expected[start] = b;
                  start++;
                  end--;
                }
              }
            }
            break;

          case 4:
            {
              // abs write random byte[]
              if (pos > 2) {
                int randomPos = random().nextInt(pos - 1);
                int len = TestUtil.nextInt(random(), 1, Math.min(pos - randomPos - 1, 100));
                byte[] temp = new byte[len];
                random().nextBytes(temp);
                if (VERBOSE) {
                  System.out.println(
                      "    abs writeBytes pos="
                          + randomPos
                          + " len="
                          + len
                          + " bytes="
                          + Arrays.toString(temp));
                }
                System.arraycopy(temp, 0, expected, randomPos, temp.length);
                bytes.writeBytes(randomPos, temp, 0, temp.length);
              }
            }
            break;

          case 5:
            {
              // copyBytes
              if (pos > 1) {
                int src = random().nextInt(pos - 1);
                int dest = TestUtil.nextInt(random(), src + 1, pos - 1);
                int len = TestUtil.nextInt(random(), 1, Math.min(300, pos - dest));
                if (VERBOSE) {
                  System.out.println("    copyBytes src=" + src + " dest=" + dest + " len=" + len);
                }
                System.arraycopy(expected, src, expected, dest, len);
                bytes.copyBytes(src, dest, len);
              }
            }
            break;

          case 6:
            {
              // skip
              int len = random().nextInt(Math.min(100, numBytes - pos));

              if (VERBOSE) {
                System.out.println("    skip len=" + len);
              }

              pos += len;
              bytes.skipBytes(len);

              // NOTE: must fill in zeros in case truncate was
              // used, else we get false fails:
              if (len > 0) {
                byte[] zeros = new byte[len];
                bytes.writeBytes(pos - len, zeros, 0, len);
              }
            }
            break;

          case 7:
            {
              // absWriteByte
              if (pos > 0) {
                int dest = random().nextInt(pos);
                byte b = (byte) random().nextInt(256);
                expected[dest] = b;
                bytes.writeByte(dest, b);
              }
              break;
            }
        }

        assertEquals(pos, bytes.getPosition());

        if (pos > 0 && random().nextInt(50) == 17) {
          // truncate
          int len = TestUtil.nextInt(random(), 1, Math.min(pos, 100));
          bytes.truncate(pos - len);
          pos -= len;
          Arrays.fill(expected, pos, pos + len, (byte) 0);
          if (VERBOSE) {
            System.out.println("    truncate len=" + len + " newPos=" + pos);
          }
        }

        if ((pos > 0 && random().nextInt(200) == 17)) {
          verify(bytes, expected, pos);
        }
      }

      BytesStore bytesToVerify;

      if (random().nextBoolean()) {
        if (VERBOSE) {
          System.out.println("TEST: save/load final bytes");
        }
        Directory dir = newDirectory();
        IndexOutput out = dir.createOutput("bytes", IOContext.DEFAULT);
        bytes.writeTo(out);
        out.close();
        IndexInput in = dir.openInput("bytes", IOContext.DEFAULT);
        bytesToVerify = new BytesStore(TestUtil.nextInt(random(), 8, 20));
        bytesToVerify.copyBytes(in, numBytes);
        in.close();
        dir.close();
      } else {
        bytesToVerify = bytes;
      }

      verify(bytesToVerify, expected, numBytes);
    }
  }

  public void testCopyBytesOnByteStore() throws IOException {
    byte[] bytes = new byte[1024 * 8 + 10];
    byte[] bytesout = new byte[bytes.length];
    random().nextBytes(bytes);
    int offset = TestUtil.nextInt(random(), 0, 100);
    int len = bytes.length - offset;
    ByteArrayDataInput in = new ByteArrayDataInput(bytes, offset, len);
    final int blockBits = TestUtil.nextInt(random(), 8, 15);
    final BytesStore o = new BytesStore(blockBits);
    o.copyBytes(in, len);
    o.copyBytes(0, bytesout, 0, len);
    assertArrayEquals(
        ArrayUtil.copyOfSubArray(bytesout, 0, len),
        ArrayUtil.copyOfSubArray(bytes, offset, offset + len));
  }

  private void verify(BytesStore bytes, byte[] expected, int totalLength) throws Exception {
    assertEquals(totalLength, bytes.getPosition());
    if (totalLength == 0) {
      return;
    }
    if (VERBOSE) {
      System.out.println("  verify...");
    }

    // First verify whole thing in one blast:
    byte[] actual = new byte[totalLength];
    if (random().nextBoolean()) {
      if (VERBOSE) {
        System.out.println("    bulk: reversed");
      }
      // reversed
      FST.BytesReader r = bytes.getReverseReader();
      assertTrue(r.reversed());
      r.setPosition(totalLength - 1);
      r.readBytes(actual, 0, actual.length);
      int start = 0;
      int end = totalLength - 1;
      while (start < end) {
        byte b = actual[start];
        actual[start] = actual[end];
        actual[end] = b;
        start++;
        end--;
      }
    } else {
      // forward
      if (VERBOSE) {
        System.out.println("    bulk: forward");
      }
      FST.BytesReader r = bytes.getForwardReader();
      assertFalse(r.reversed());
      r.readBytes(actual, 0, actual.length);
    }

    for (int i = 0; i < totalLength; i++) {
      assertEquals("byte @ index=" + i, expected[i], actual[i]);
    }

    FST.BytesReader r;

    // Then verify ops:
    boolean reversed = random().nextBoolean();
    if (reversed) {
      if (VERBOSE) {
        System.out.println("    ops: reversed");
      }
      r = bytes.getReverseReader();
    } else {
      if (VERBOSE) {
        System.out.println("    ops: forward");
      }
      r = bytes.getForwardReader();
    }

    if (totalLength > 1) {
      int numOps = TestUtil.nextInt(random(), 100, 200);
      for (int op = 0; op < numOps; op++) {

        int numBytes = random().nextInt(Math.min(1000, totalLength - 1));
        int pos;
        if (reversed) {
          pos = TestUtil.nextInt(random(), numBytes, totalLength - 1);
        } else {
          pos = random().nextInt(totalLength - numBytes);
        }
        if (VERBOSE) {
          System.out.println(
              "    op iter="
                  + op
                  + " reversed="
                  + reversed
                  + " numBytes="
                  + numBytes
                  + " pos="
                  + pos);
        }
        byte[] temp = new byte[numBytes];
        r.setPosition(pos);
        assertEquals(pos, r.getPosition());
        r.readBytes(temp, 0, temp.length);
        for (int i = 0; i < numBytes; i++) {
          byte expectedByte;
          if (reversed) {
            expectedByte = expected[pos - i];
          } else {
            expectedByte = expected[pos + i];
          }
          assertEquals("byte @ index=" + i, expectedByte, temp[i]);
        }

        int left;
        int expectedPos;

        if (reversed) {
          expectedPos = pos - numBytes;
          left = (int) r.getPosition();
        } else {
          expectedPos = pos + numBytes;
          left = (int) (totalLength - r.getPosition());
        }
        assertEquals(expectedPos, r.getPosition());

        if (left > 4) {
          int skipBytes = random().nextInt(left - 4);

          int expectedInt = 0;
          if (reversed) {
            expectedPos -= skipBytes;
            expectedInt |= (expected[expectedPos--] & 0xFF);
            expectedInt |= (expected[expectedPos--] & 0xFF) << 8;
            expectedInt |= (expected[expectedPos--] & 0xFF) << 16;
            expectedInt |= (expected[expectedPos--] & 0xFF) << 24;
          } else {
            expectedPos += skipBytes;
            expectedInt |= (expected[expectedPos++] & 0xFF);
            expectedInt |= (expected[expectedPos++] & 0xFF) << 8;
            expectedInt |= (expected[expectedPos++] & 0xFF) << 16;
            expectedInt |= (expected[expectedPos++] & 0xFF) << 24;
          }

          if (VERBOSE) {
            System.out.println("    skip numBytes=" + skipBytes);
            System.out.println("    readInt");
          }

          r.skipBytes(skipBytes);
          assertEquals(expectedInt, r.readInt());
        }
      }
    }
  }
}
