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

import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Random;
import org.apache.lucene.store.ByteArrayDataInput;
import org.apache.lucene.store.ByteArrayDataOutput;
import org.apache.lucene.store.ByteBuffersDataInput;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.lucene.util.ArrayUtil;
import org.apache.lucene.util.BytesRef;

public abstract class AbstractTestCompressionMode extends LuceneTestCase {

  CompressionMode mode;

  static byte[] randomArray(Random random) {
    int bigsize = TEST_NIGHTLY ? 192 * 1024 : 33 * 1024;
    final int max = random.nextBoolean() ? random.nextInt(4) : random.nextInt(255);
    final int length = random.nextBoolean() ? random.nextInt(20) : random.nextInt(bigsize);
    return randomArray(random, length, max);
  }

  static byte[] randomArray(Random random, int length, int max) {
    final byte[] arr = new byte[length];
    for (int i = 0; i < arr.length; ++i) {
      arr[i] = (byte) RandomNumbers.randomIntBetween(random, 0, max);
    }
    return arr;
  }

  byte[] compress(byte[] decompressed, int off, int len) throws IOException {
    Compressor compressor = mode.newCompressor();
    return compress(compressor, decompressed, off, len);
  }

  static byte[] compress(Compressor compressor, byte[] decompressed, int off, int len)
      throws IOException {
    byte[] compressed = new byte[len * 3 + 16]; // should be enough
    ByteBuffer bb = ByteBuffer.wrap(decompressed);
    ByteBuffersDataInput input = new ByteBuffersDataInput(Arrays.asList(bb)).slice(off, len);

    ByteArrayDataOutput out = new ByteArrayDataOutput(compressed);

    compressor.compress(input, out);
    final int compressedLen = out.getPosition();
    return ArrayUtil.copyOfSubArray(compressed, 0, compressedLen);
  }

  byte[] decompress(byte[] compressed, int originalLength) throws IOException {
    Decompressor decompressor = mode.newDecompressor();
    return decompress(decompressor, compressed, originalLength);
  }

  static byte[] decompress(Decompressor decompressor, byte[] compressed, int originalLength)
      throws IOException {
    final BytesRef bytes = new BytesRef();
    decompressor.decompress(
        new ByteArrayDataInput(compressed), originalLength, 0, originalLength, bytes);
    return BytesRef.deepCopyOf(bytes).bytes;
  }

  byte[] decompress(byte[] compressed, int originalLength, int offset, int length)
      throws IOException {
    Decompressor decompressor = mode.newDecompressor();
    final BytesRef bytes = new BytesRef();
    decompressor.decompress(
        new ByteArrayDataInput(compressed), originalLength, offset, length, bytes);
    return BytesRef.deepCopyOf(bytes).bytes;
  }

  public void testDecompress() throws IOException {
    Random random = random();
    final int iterations = atLeast(random, 3);
    for (int i = 0; i < iterations; ++i) {
      final byte[] decompressed = randomArray(random);
      final int off = random.nextBoolean() ? 0 : TestUtil.nextInt(random, 0, decompressed.length);
      final int len =
          random.nextBoolean()
              ? decompressed.length - off
              : TestUtil.nextInt(random, 0, decompressed.length - off);
      final byte[] compressed = compress(decompressed, off, len);
      final byte[] restored = decompress(compressed, len);
      assertArrayEquals(ArrayUtil.copyOfSubArray(decompressed, off, off + len), restored);
    }
  }

  public void testPartialDecompress() throws IOException {
    Random random = random();
    final int iterations = atLeast(random, 3);
    for (int i = 0; i < iterations; ++i) {
      final byte[] decompressed = randomArray(random);
      final byte[] compressed = compress(decompressed, 0, decompressed.length);
      final int offset, length;
      if (decompressed.length == 0) {
        offset = length = 0;
      } else {
        offset = random.nextInt(decompressed.length);
        length = random.nextInt(decompressed.length - offset);
      }
      final byte[] restored = decompress(compressed, decompressed.length, offset, length);
      assertArrayEquals(ArrayUtil.copyOfSubArray(decompressed, offset, offset + length), restored);
    }
  }

  public byte[] test(byte[] decompressed) throws IOException {
    return test(decompressed, 0, decompressed.length);
  }

  public byte[] test(byte[] decompressed, int off, int len) throws IOException {
    final byte[] compressed = compress(decompressed, off, len);
    final byte[] restored = decompress(compressed, len);
    assertEquals(len, restored.length);
    return compressed;
  }

  public void testEmptySequence() throws IOException {
    test(new byte[0]);
  }

  public void testShortSequence() throws IOException {
    test(new byte[] {(byte) random().nextInt(256)});
  }

  public void testIncompressible() throws IOException {
    final byte[] decompressed = new byte[RandomNumbers.randomIntBetween(random(), 20, 256)];
    for (int i = 0; i < decompressed.length; ++i) {
      decompressed[i] = (byte) i;
    }
    test(decompressed);
  }

  public void testConstant() throws IOException {
    final byte[] decompressed = new byte[TestUtil.nextInt(random(), 1, 10000)];
    Arrays.fill(decompressed, (byte) random().nextInt());
    test(decompressed);
  }

  public void testExtremelyLargeInput() throws IOException {
    final byte[] decompressed = new byte[1 << 24]; // 16MB
    for (int i = 0; i < decompressed.length; ++i) {
      decompressed[i] = (byte) (i & 0x0F);
    }
    test(decompressed);
  }
}
