/*
 * 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.jena.tdb.base.record;

import static java.lang.String.format ;
import java.nio.ByteBuffer;

/** Record creator */
final
public class RecordFactory
{
    private final int keyLength ;
    private final int valueLength ;
    private final int slotLen ;
    private final boolean checking = false ;

    public RecordFactory(int keyLength, int valueLength)
    {
        if ( keyLength <= 0 )
            throw new IllegalArgumentException("Bad key length: "+keyLength) ;
        if ( valueLength < 0 )
            throw new IllegalArgumentException("Bad value length: "+valueLength) ;

        this.keyLength = keyLength ;
        this.valueLength = valueLength ;
        this.slotLen = keyLength + (valueLength>0 ? valueLength : 0 ) ;
    }
    
    /** Return a RecordFactory that makes key-only records of the same key size */ 
    public RecordFactory keyFactory()
    {
        return new RecordFactory(keyLength, 0) ;
    }

    /** Create a key-only record, allocating blank space for the key  */
    public Record createKeyOnly()
    {
        return create(new byte[keyLength], null) ;
    }
    
    /** Create a key-only record */
    public Record createKeyOnly(Record record)
    {
        checkKey(record.getKey()) ;
        if ( record.getValue() == null )
            return record ;
        
        return create(record.getKey(), null) ;
    }
    
    /** Create a key and value record (value uninitialized) */
    public Record create(byte[] key)
    { 
        checkKey(key) ;
        byte[] v = null ;
        if ( valueLength > 0 )
            v = new byte[valueLength] ;
        return create(key, v) ;
    }
    
    /** Create a record, allocating space for the key and value (if any) */
    public Record create()
    { return create(new byte[keyLength], 
                    (valueLength > 0) ? new byte[valueLength] : null) ;
    }
    
    /** Create a key and value record */
    public Record create(byte[] key, byte[] value)
    {
        check(key, value) ;
        return new Record(key, value) ;
    }
    
    public void insertInto(Record record, ByteBuffer bb, int idx)
    {
        check(record) ;
        bb.position(idx*slotLen) ;
        bb.put(record.getKey(), 0, keyLength) ;
        if ( hasValue() && record.getValue() != null )
            bb.put(record.getValue(), 0, valueLength) ;
    }
    
    public Record buildFrom(ByteBuffer bb, int idx)
    {
        byte[] key = new byte[keyLength] ;
        byte[] value = (hasValue() ? new byte[valueLength] :null ) ;

        // Slightly slower. (2017)
        // Avoid using position() so we can avoid needing synchronized.
        // Inlined or copyInto -- both the same time.
        // Slower than the synchronized(bb) by ~10%.
//        int posnKey = idx*slotLen ;
//        copyInto(key, bb, posnKey, keyLength) ;
//        if ( value != null ) {
//            int posnValue = posnKey+keyLength ;
//            copyInto(value, bb, posnValue, valueLength) ;
//        }
        
        // Using bb.get(byte[],,) is slightly faster, including the required synchronized.
        // There's no absolute version.
        synchronized(bb)
        {
            bb.position(idx*slotLen) ;
            bb.get(key, 0, keyLength) ;
            if ( value != null )
                bb.get(value, 0, valueLength) ;
        }
        return create(key, value) ;
    }
    
    private final void copyInto(byte[] dst, ByteBuffer src, int start, int length)
    {
        // Thread safe.
        for ( int i = 0 ; i < length ; i++ )
            dst[i] = src.get(start+i) ;
    }
    
    public boolean hasValue()   { return valueLength > 0 ; }

    public int recordLength()   { return keyLength + valueLength ; }
    
    public int keyLength()      { return keyLength ; }

    public int valueLength()    { return valueLength ; }
    
    @Override
    public String toString()
    {
        return format("<RecordFactory k=%d v=%d>", keyLength, valueLength) ; 
    }
    
    private final void check(Record record)
    {
        if ( ! checking ) return ;
        check(record.getKey(), record.getValue()) ;
    }
    
    private final void checkKey(byte[] k)
    {
        if ( ! checking ) return ;
        if ( k == null )
            throw new RecordException("Null key byte[]") ;
        if ( keyLength != k.length ) 
            throw new RecordException(format("Key length error: This RecordFactory manages records of key length %d, not %d", keyLength, k.length)) ;
    }
    
    private final void check(byte[] k, byte[] v)
    {
        if ( ! checking ) return ;
        checkKey(k) ;
        if ( valueLength <= 0 )
        {
            if ( v != null ) 
                throw new RecordException("Value array error: This RecordFactory manages records that are all key") ;
        }
        else
        {
            // v == null for a key-only record from this factory.
            if ( v != null && v.length != valueLength )
                throw new RecordException(format("This RecordFactory manages record of value length %d, not (%d,-)", valueLength, v.length)) ;
        }
    }
}
