package org.apache.lucene.util.fst;

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

import java.util.Arrays;

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.util.LuceneTestCase;
import org.apache.lucene.util.TestUtil;

public class TestBytesStore extends LuceneTestCase {

  public void testRandom() throws Exception {

    final int iters = atLeast(10);
    for(int iter=0;iter<iters;iter++) {
      final int numBytes = TestUtil.nextInt(random(), 1, 200000);
      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(in, numBytes, TestUtil.nextInt(random(), 256, Integer.MAX_VALUE));
        in.close();
        dir.close();
      } else {
        bytesToVerify = bytes;
      }

      verify(bytesToVerify, expected, numBytes);
    }
  }

  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)<<24;
            expectedInt |= (expected[expectedPos--]&0xFF)<<16;
            expectedInt |= (expected[expectedPos--]&0xFF)<<8;
            expectedInt |= (expected[expectedPos--]&0xFF);
          } else {
            expectedPos += skipBytes;
            expectedInt |= (expected[expectedPos++]&0xFF)<<24;
            expectedInt |= (expected[expectedPos++]&0xFF)<<16;
            expectedInt |= (expected[expectedPos++]&0xFF)<<8;
            expectedInt |= (expected[expectedPos++]&0xFF);
          }

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

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