/*
 * Copyright © 2015 Cask Data, 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 io.cdap.cdap.data2.dataset2.lib.table;

import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import io.cdap.cdap.api.common.Bytes;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;

/**
 * Metadata entry key
 */
public final class MDSKey implements Comparable<MDSKey> {

  private final byte[] key;

  /**
   * @param key bytearray as constructed by {@code MDSKey.Builder}
   */
  public MDSKey(byte[] key) {
    this.key = key;
  }

  /**
   * @return the underlying key
   */
  public byte[] getKey() {
    return key;
  }

  /**
   * Splits the keys into a {@link Splitter} which exposes the parts that comprise the key.
   */
  public Splitter split() {
    return new Splitter(key);
  }

  /**
   * Decodes the keys parts, as specified (int, long, byte[], String)
   */
  public static final class Splitter {

    private final ByteBuffer byteBuffer;

    private Splitter(byte[] bytes) {
      byteBuffer = ByteBuffer.wrap(bytes);
    }

    /**
     * @return the next int part in the splitter
     * @throws BufferUnderflowException if there is no int as expected
     */
    public int getInt() {
      return byteBuffer.getInt();
    }

    /**
     * @return the next long part in the splitter
     * @throws BufferUnderflowException if there is no long as expected
     */
    public long getLong() {
      return byteBuffer.getLong();
    }

    /**
     * @return the next byte[] part in the splitter
     * @throws BufferUnderflowException if there is no byte[] as expected
     */
    public byte[] getBytes() {
      int len = byteBuffer.getInt();
      if (byteBuffer.remaining() < len) {
        throw new BufferUnderflowException();
      }
      byte[] bytes = new byte[len];
      byteBuffer.get(bytes, 0, len);
      return bytes;
    }


    /**
     * @return the next String part in the splitter
     * @throws BufferUnderflowException if there is no String as expected
     */
    public String getString() {
      return Bytes.toString(getBytes());
    }

    /**
     * @return the next boolean part in the splitter
     * @throws BufferUnderflowException if there is no boolean as expected
     */
    public boolean getBoolean() {
      if (byteBuffer.remaining() < 1) {
        throw new BufferUnderflowException();
      }
      return Bytes.toBoolean(new byte[]{byteBuffer.get()});
    }

    /**
     * skips the next int part in the splitter
     *
     * @throws BufferUnderflowException if there is no int as expected
     */
    public void skipInt() {
      forward(Ints.BYTES);
    }

    /**
     * skips the next long part in the splitter
     *
     * @throws BufferUnderflowException if there is no long as expected
     */
    public void skipLong() {
      forward(Longs.BYTES);
    }

    /**
     * skips the next Boolean part in the splitter
     *
     * @throws BufferUnderflowException if there is no Boolean as expected
     */
    public void skipBoolean() {
      // skip a single byte
      forward(1);
    }

    /**
     * skips the next byte[] part in the splitter
     *
     * @throws BufferUnderflowException if there is no byte[] as expected
     */
    public void skipBytes() {
      int len = byteBuffer.getInt();
      forward(len);
    }

    /**
     * skips the next String part in the splitter
     *
     * @throws BufferUnderflowException if there is no String as expected
     */
    public void skipString() {
      skipBytes();
    }

    /**
     * @return true if there are any bytes remaining to be read
     */
    public boolean hasRemaining() {
      return byteBuffer.hasRemaining();
    }

    private void forward(int count) {
      int position = byteBuffer.position();
      byteBuffer.position(position + count);
    }
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }

    MDSKey that = (MDSKey) o;
    return Bytes.equals(this.key, that.key);
  }

  @Override
  public int hashCode() {
    return Bytes.hashCode(key);
  }

  @Override
  public int compareTo(MDSKey o) {
    return Bytes.compareTo(this.getKey(), o.getKey());
  }

  /**
   * Builds {@link MDSKey}s.
   */
  public static final class Builder {

    private byte[] key;

    public Builder() {
      key = new byte[0];
    }

    public Builder(MDSKey start) {
      this.key = start.getKey();
    }

    // Encodes parts of the key with segments of <length> <value>
    public Builder add(byte[] part) {
      key = Bytes.add(key, Bytes.toBytes(part.length), part);
      return this;
    }

    public Builder add(String part) {
      add(Bytes.toBytes(part));
      return this;
    }

    public Builder add(String... parts) {
      for (String part : parts) {
        add(part);
      }
      return this;
    }

    public Builder add(long part) {
      key = Bytes.add(key, Bytes.toBytes(part));
      return this;
    }

    public Builder add(boolean part) {
      key = Bytes.add(key, Bytes.toBytes(part));
      return this;
    }

    public Builder add(int part) {
      key = Bytes.add(key, Bytes.toBytes(part));
      return this;
    }

    public Builder append(MDSKey mdsKey) {
      key = Bytes.add(key, mdsKey.getKey());
      return this;
    }

    public MDSKey build() {
      return new MDSKey(key);
    }
  }
}
