/*
 * 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.jackrabbit.oak.commons.testing;

import org.jetbrains.annotations.NotNull;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * An input stream that returns pseudo-random bytes.
 * <p>
 * (copied from package org.apache.jackrabbit.core.data)
 */
public class RandomInputStream extends InputStream {

    private static final long MUL = 0x5DEECE66DL;
    private static final long ADD = 0xBL;
    private static final long MASK = (1L << 48) - 1;
    private static final int DEFAULT_MAX_READ_BLOCK_SIZE = 15;

    private final long initialSeed;
    private final long len;
    private long markedState;
    private long pos;
    private long markedPos;
    private long state;
    private final int maxReadBlockSize;

    public String toString() {
        return "new RandomInputStream(" + initialSeed + ", " + len + ")";
    }

    public RandomInputStream(long seed, long len) {
        this(seed, len, DEFAULT_MAX_READ_BLOCK_SIZE);
    }

    public static void compareStreams(InputStream a, InputStream b) throws IOException {
        a = new BufferedInputStream(a);
        b = new BufferedInputStream(b);
        long pos = 0;
        while (true) {
            int x = a.read();
            int y = b.read();
            if ((x == -1 || y == -1) && x == y) {
                break;
            }
            if (x != y) {
                throw new IOException("Incorrect byte at position " + pos + ": x=" + x + " y=" + y);
            }
        }
    }

    public RandomInputStream(long seed, long len, int maxReadBlockSize) {
        this.initialSeed = seed;
        this.len = len;
        this.maxReadBlockSize = maxReadBlockSize;
        setSeed(seed);
        reset();
    }

    @Override
    public long skip(long n) {
        n = getReadBlock(n);
        if (n == 0) {
            return -1;
        }
        pos += n;
        return n;
    }

    private int getReadBlock(long n) {
        if (n > (len - pos)) {
            n = (len - pos);
        }
        if (n > maxReadBlockSize) {
            n = maxReadBlockSize;
        } else if (n < 0) {
            n = 0;
        }
        return (int) n;
    }

    @Override
    public int read(byte @NotNull [] b, int off, int len) {
        if (pos >= this.len) {
            return -1;
        }
        len = getReadBlock(len);
        if (len == 0) {
            return -1;
        }
        for (int i = 0; i < len; i++) {
            b[off + i] = (byte) (next() & 255);
        }
        pos += len;
        return len;
    }

    @Override
    public int read(byte @NotNull [] b) {
        return read(b, 0, b.length);
    }

    @Override
    public void close() {
        pos = len;
    }

    private void setSeed(long seed) {
        markedState = (seed ^ MUL) & MASK;
    }

    private int next() {
        state = (state * MUL + ADD) & MASK;
        return (int) (state >>> (48 - 32));
    }

    @Override
    public void reset() {
        pos = markedPos;
        state = markedState;
    }

    @Override
    public int read() {
        if (pos >= len) {
            return -1;
        }
        pos++;
        return next() & 255;
    }

    @Override
    public boolean markSupported() {
        return true;
    }

    @Override
    public void mark(int readlimit) {
        markedPos = pos;
        markedState = state;
    }

}
