/*
 * Copyright (c) 2013-2016 Cinchapi 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.cinchapi.concourse.server.model;

import java.nio.ByteBuffer;
import java.util.Objects;

import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

import com.cinchapi.concourse.server.io.Byteable;
import com.cinchapi.concourse.util.ByteBuffers;
import com.google.common.base.Preconditions;

/**
 * A Position is an abstraction for the association between a
 * relative location and a {@link PrimaryKey} that is used in a
 * {@link SearchRecord} to specify the location of a term in a record.
 * 
 * @author Jeff Nelson
 */
@Immutable
public final class Position implements Byteable, Comparable<Position> {

    /**
     * Return the Position encoded in {@code bytes} so long as those bytes
     * adhere to the format specified by the {@link #getBytes()} method. This
     * method assumes that all the bytes in the {@code bytes} belong to the
     * Position. In general, it is necessary to get the appropriate Position
     * slice from the parent ByteBuffer using
     * {@link ByteBuffers#slice(ByteBuffer, int, int)}.
     * 
     * @param bytes
     * @return the Position
     */
    public static Position fromByteBuffer(ByteBuffer bytes) {
        PrimaryKey primaryKey = PrimaryKey.fromByteBuffer(ByteBuffers.get(
                bytes, PrimaryKey.SIZE));
        int index = bytes.getInt();
        return new Position(primaryKey, index);
    }

    /**
     * Return a Position that is backed by {@code primaryKey} and {@code index}.
     * 
     * @param primaryKey
     * @param index
     * @return the Position
     */
    public static Position wrap(PrimaryKey primaryKey, int index) {
        return new Position(primaryKey, index);
    }

    /**
     * The total number of bytes used to store each Position
     */
    public static final int SIZE = PrimaryKey.SIZE + 4; // index

    /**
     * A cached copy of the binary representation that is returned from
     * {@link #getBytes()}.
     */
    private transient ByteBuffer bytes;

    /**
     * The index that this Position represents.
     */
    private final int index;

    /**
     * The PrimaryKey of the record that this Position represents.
     */
    private final PrimaryKey primaryKey;

    /**
     * Construct a new instance.
     * 
     * @param primaryKey
     * @param index
     */
    private Position(PrimaryKey primaryKey, int index) {
        this(primaryKey, index, null);
    }

    /**
     * Construct a new instance.
     * 
     * @param primaryKey
     * @param index
     * @param bytes;
     */
    private Position(PrimaryKey primaryKey, int index,
            @Nullable ByteBuffer bytes) {
        Preconditions
                .checkArgument(index >= 0, "Cannot have an negative index");
        this.primaryKey = primaryKey;
        this.index = index;
        this.bytes = bytes;
    }

    @Override
    public int compareTo(Position other) {
        int comparison;
        return (comparison = primaryKey.compareTo(other.primaryKey)) != 0 ? comparison
                : Integer.compare(index, other.index);
    }

    @Override
    public boolean equals(Object obj) {
        if(obj instanceof Position) {
            Position other = (Position) obj;
            return primaryKey.equals(other.primaryKey) && index == other.index;
        }
        return false;
    }

    /**
     * Return a byte buffer that represents this Value with the following order:
     * <ol>
     * <li><strong>primaryKey</strong> - position 0</li>
     * <li><strong>index</strong> - position 8</li>
     * </ol>
     * 
     * @return the ByteBuffer representation
     */
    @Override
    public ByteBuffer getBytes() {
        if(bytes == null) {
            bytes = ByteBuffer.allocate(size());
            copyTo(bytes);
            bytes.rewind();
        }
        return ByteBuffers.asReadOnlyBuffer(bytes);
    }

    /**
     * Return the associated {@code index}.
     * 
     * @return the index
     */
    public int getIndex() {
        return index;
    }

    /**
     * Return the associated {@code primaryKey}.
     * 
     * @return the primaryKey
     */
    public PrimaryKey getPrimaryKey() {
        return primaryKey;
    }

    @Override
    public int hashCode() {
        return Objects.hash(primaryKey, index);
    }

    @Override
    public int size() {
        return SIZE;
    }

    @Override
    public String toString() {
        return "Position " + index + " in Record " + primaryKey;
    }

    @Override
    public void copyTo(ByteBuffer buffer) {
        // NOTE: Storing the index as an int instead of some size aware
        // variable length is probably overkill since most indexes will be
        // smaller than Byte.MAX_SIZE or Short.MAX_SIZE, but having variable
        // size indexes means that the size of the entire Position (as an
        // int) must be stored before the Position for proper
        // deserialization. By storing the index as an int, the size of each
        // Position is constant so we won't need to store the overall size
        // prior to the Position to deserialize it, which is actually more
        // space efficient.
        primaryKey.copyTo(buffer);
        buffer.putInt(index);
    }

}
