/*
 * Copyright (C) 2014 Indeed Inc.
 *
 * Licensed 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 com.indeed.flamdex.simple;

import com.google.common.io.ByteStreams;
import org.apache.log4j.Logger;
import sun.misc.Unsafe;

import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;

/**
 * @author jplaisance
 */
public final class NativeDocIdBuffer implements Closeable {

    private static final Logger log = Logger.getLogger(NativeDocIdBuffer.class);

    private static final Unsafe UNSAFE;
    private static final long INT_ARRAY_BASE_OFFSET;

    private static final int BUFFER_LENGTH = 512;

    private static final boolean useSSSE3;

    static {
        useSSSE3 = "true".equalsIgnoreCase(System.getProperty("com.indeed.flamdex.simple.useSSSE3"));
        loadNativeLibrary();
        nativeInit();
        log.info("libvarint loaded");
        if (useSSSE3) log.info("using SSSE3! (if the processor in this computer doesn't support SSSE3 this process will fail with SIGILL)");
    }

    static {
        try {
            final Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe)theUnsafe.get(null);
            INT_ARRAY_BASE_OFFSET = UNSAFE.arrayBaseOffset(int[].class);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    static void loadNativeLibrary() {
        try {
            final String osName = System.getProperty("os.name");
            final String arch = System.getProperty("os.arch");
            final String resourcePath = "/native/" + osName + "-" + arch + "/libvarint.so.1.0.1";
            final InputStream is = NativeDocIdStream.class.getResourceAsStream(resourcePath);
            if (is == null) {
                throw new FileNotFoundException("unable to find libvarint.so.1.0.1 at resource path "+resourcePath);
            }
            final File tempFile = File.createTempFile("libvarint", ".so");
            final OutputStream os = new FileOutputStream(tempFile);
            ByteStreams.copy(is, os);
            os.close();
            is.close();
            System.load(tempFile.getAbsolutePath());
            // noinspection ResultOfMethodCallIgnored
            tempFile.delete();
        } catch (Throwable e) {
            log.warn("unable to load libvarint using class loader, looking in java.library.path", e);
            System.loadLibrary("varint"); // if this fails it throws UnsatisfiedLinkError
        }
    }

    private final long bufAddress;
    private int bufIndex;
    private int bufLen;

    private long position;

    private long docsRemaining;

    NativeDocIdBuffer() {
        bufAddress = UNSAFE.allocateMemory(4 * BUFFER_LENGTH);
    }

    public void reset(long newPosition, long newDocsRemaining) {
        position = newPosition;
        docsRemaining = newDocsRemaining;
        // to force a refill
        bufIndex = 0;
        bufLen = 0;
    }

    public int fillDocIdBuffer(int[] docIdBuffer, int limit) {
        int off = 0;
        do {
            if (bufLen == bufIndex){
                if (docsRemaining == 0) {
                    break;
                }
                readInts();
            }
            final int n = Math.min(limit-off, bufLen-bufIndex);
            UNSAFE.copyMemory(null, bufAddress + bufIndex * 4, docIdBuffer, INT_ARRAY_BASE_OFFSET + off * 4, n * 4);
            off += n;
            bufIndex += n;
        } while (off < limit);
        return off;
    }

    private void readInts() {

        final int length = (int)Math.min(docsRemaining, BUFFER_LENGTH);
        if (useSSSE3) {
            position += readInts(position, bufAddress, length);
        } else {
            position += readIntsSingle(position, bufAddress, length);
        }
        docsRemaining -= length;
        bufIndex = 0;
        bufLen = length;
    }

    @Override
    public void close() throws IOException {
        UNSAFE.freeMemory(bufAddress);
    }

    private static native long readInts(long bytesAddr, long intsAddr, int length);

    private static native long readIntsSingle(long bytesAddr, long intsAddr, int length);

    private static native void nativeInit();
}
