/**
 * 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 com.salesforce.phoenix.client;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.Arrays;

import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.util.Bytes;

/**
 * {@link KeyValue} that should only be used from the client side. Enables clients to be more
 * flexible with the byte arrays they use when building a {@link KeyValue}, but still wire
 * compatible.
 * <p>
 * All<tt> byte[]</tt> (or {@link ImmutableBytesWritable}) passed into the constructor are only ever
 * read once - when writing <tt>this</tt> onto the wire. They are never copied into another array or
 * reused. This has the advantage of being much more efficient than the usual {@link KeyValue}
 * <p>
 * The down side is that we no longer can support some of the usual methods like
 * {@link #getBuffer()} or {@link #getKey()} since its is backed with multiple <tt>byte[]</tt> and
 * <i>should only be used by the client to <b>send</b> information</i>
 * <p>
 * <b>WARNING:</b> should only be used by advanced users who know how to construct their own
 * KeyValues
 */
public class ClientKeyValue extends KeyValue {

  private static ImmutableBytesWritable NULL = new ImmutableBytesWritable(new byte[0]);
  private ImmutableBytesWritable row;
  private ImmutableBytesWritable family;
  private ImmutableBytesWritable qualifier;
  private Type type;
  private long ts;
  private ImmutableBytesWritable value;

  /**
   * @param row must not be <tt>null</tt>
   * @param type must not be <tt>null</tt>
   */
  public ClientKeyValue(ImmutableBytesWritable row, ImmutableBytesWritable family,
      ImmutableBytesWritable qualifier, long ts, Type type, ImmutableBytesWritable value) {
    this.row = row;
    this.family = family == null ? NULL : family;
    this.qualifier = qualifier == null ? NULL : qualifier;
    this.type = type;
    this.ts = ts;
    this.value = value == null ? NULL : value;
  }

  /**
   * Convenience constructor that just wraps all the bytes in {@link ImmutableBytesWritable}
   */
  public ClientKeyValue(byte[] row, byte[] family, byte[] qualifier, long ts, Type t, byte[] value) {
    this(wrap(row), wrap(family), wrap(qualifier), ts, t, wrap(value));
  }

  /**
   * Convenience constructor that just wraps all the bytes in {@link ImmutableBytesWritable}
   */
  public ClientKeyValue(byte[] row, byte[] family, byte[] qualifier, long ts, Type t) {
    this(wrap(row), wrap(family), wrap(qualifier), ts, t, null);
  }

  private static ImmutableBytesWritable wrap(byte[] b) {
    return b == null ? NULL : new ImmutableBytesWritable(b);
  }

  @Override
  public KeyValue clone() {
    return new ClientKeyValue(copy(row), copy(family), copy(qualifier), ts, type, copy(value));
  }

  private ImmutableBytesWritable copy(ImmutableBytesWritable bytes) {
    return new ImmutableBytesWritable(bytes.copyBytes());
  }

  private static byte[] copyIfNecessary(ImmutableBytesWritable bytes) {
    byte[] b = bytes.get();
    if (bytes.getLength() == b.length && bytes.getOffset() == 0) {
      return b;
    }
    return Arrays.copyOfRange(b, bytes.getOffset(), bytes.getOffset() + bytes.getLength());
  }

  @Override
  public KeyValue shallowCopy() {
    return new ClientKeyValue(row, family, qualifier, ts, type, value);
  }

  @Override
  public int getValueOffset() {
    return value.getOffset();
  }

  @Override
  public int getValueLength() {
    return value.getLength();
  }

  @Override
  public int getRowOffset() {
    return row.getOffset();
  }

  @Override
  public short getRowLength() {
    return (short) row.getLength();
  }

  @Override
  public int getFamilyOffset() {
    return family.getOffset();
  }

  @Override
  public byte getFamilyLength() {
    return (byte) family.getLength();
  }

  @Override
  public byte getFamilyLength(int foffset) {
    return this.getFamilyLength();
  }

  @Override
  public int getQualifierOffset() {
    return qualifier.getOffset();
  }

  @Override
  public int getQualifierLength() {
    return qualifier.getLength();
  }

  @Override
  public int getQualifierLength(int rlength, int flength) {
    return this.getQualifierLength();
  }

  @Override
  public int getTotalColumnLength(int rlength, int foffset) {
    return this.getFamilyLength() + this.getQualifierLength();
  }

  @Override
  public int getTotalColumnLength() {
    return qualifier.getLength() + family.getLength();
  }

  @Override
  public byte[] getValue() {
    return copyIfNecessary(value);
  }

  @Override
  public byte[] getRow() {
    return copyIfNecessary(row);
  }

  @Override
  public long getTimestamp() {
    return ts;
  }

  @Override
  public byte[] getFamily() {
    return copyIfNecessary(family);
  }

  @Override
  public byte[] getQualifier() {
    return copyIfNecessary(qualifier);
  }

  @Override
  public byte getType() {
    return this.type.getCode();
  }

  @Override
  public boolean matchingFamily(byte[] family) {
    if (family == null) {
      if (this.family.getLength() == 0) {
        return true;
      }
      return false;
    }
    return matchingFamily(family, 0, family.length);
  }

  @Override
  public boolean matchingFamily(byte[] family, int offset, int length) {
    if (family == null) {
      if (this.family.getLength() == 0) {
        return true;
      }
      return false;
    }
    return matches(family, offset, length, this.family);
  }

  @Override
  public boolean matchingFamily(KeyValue other) {
    if(other == null) {
      return false;
    }
    if(other instanceof ClientKeyValue) {
      ClientKeyValue kv = (ClientKeyValue)other;
      return this.family.compareTo(kv.family) == 0;
    }
    return matchingFamily(other.getBuffer(), other.getFamilyOffset(), other.getFamilyLength());
  }

  private boolean matches(byte[] b, int offset, int length, ImmutableBytesWritable bytes) {
    return Bytes.equals(b, offset, length, bytes.get(), bytes.getOffset(), bytes.getLength());
  }

  @Override
  public boolean matchingQualifier(byte[] qualifier) {
    if (qualifier == null) {
      if (this.qualifier.getLength() == 0) {
        return true;
      }
      return false;
    }
    return matchingQualifier(qualifier, 0, qualifier.length);
  }

  @Override
  public boolean matchingQualifier(byte[] qualifier, int offset, int length) {
    if (qualifier == null) {
      if (this.qualifier.getLength() == 0) {
        return true;
      }
      return false;
    }
    return matches(qualifier, offset, length, this.qualifier);
  }

  @Override
  public boolean matchingQualifier(KeyValue other) {
    if (other == null) {
      return false;
    }
    if (other instanceof ClientKeyValue) {
      ClientKeyValue kv = (ClientKeyValue) other;
      return this.row.compareTo(kv.row) == 0;
    }
    return matchingQualifier(other.getBuffer(), other.getQualifierOffset(),
      other.getQualifierLength());
  }

  @Override
  public boolean matchingRow(byte[] row){
    if (row == null) {
      return false;
    }
    return matches(row, 0, row.length, this.row);
  }

  @Override
  public boolean matchingRow(byte[] row, int offset, int length) {
    if (row == null) {
      return false;
    }
    return matches(row, offset, length, this.row);
  }

  @Override
  public boolean matchingRow(KeyValue other) {
    return matchingRow(other.getBuffer(), other.getRowOffset(), other.getRowLength());
  }

  @Override
  public boolean matchingColumnNoDelimiter(byte[] column) {
    // match both the family and qualifier
    if (matchingFamily(column, 0, this.family.getLength())) {
      return matchingQualifier(column, family.getLength(), column.length - family.getLength());
    }
    return false;
  }

  @Override
  public boolean matchingColumn(byte[] family, byte[] qualifier) {
    return this.matchingFamily(family) && matchingQualifier(qualifier);
  }

  @Override
  public boolean nonNullRowAndColumn() {
    return (this.row != null && row.getLength() > 0) && !isEmptyColumn();
  }

  @Override
  public boolean isEmptyColumn() {
    return this.qualifier != null && this.qualifier.getLength() > 0;
  }


  @Override
  public void write(DataOutput out) throws IOException {
    // we need to simulate the keyvalue writing, but actually step through each buffer.
    //start with keylength
    long longkeylength = KeyValue.KEY_INFRASTRUCTURE_SIZE + row.getLength() + family.getLength()
        + qualifier.getLength();
    if (longkeylength > Integer.MAX_VALUE) {
      throw new IllegalArgumentException("keylength " + longkeylength + " > " + Integer.MAX_VALUE);
    }
    // need to figure out the max length before we start
    int length = this.getLength();
    out.writeInt(length);

    // write the actual data
    int keylength = (int) longkeylength;
    out.writeInt(keylength);
    int vlength = value == null ? 0 : value.getLength();
    out.writeInt(vlength);
    out.writeShort((short) (row.getLength() & 0x0000ffff));
    out.write(this.row.get(), this.row.getOffset(), this.row.getLength());
    out.writeByte((byte) (family.getLength() & 0x0000ff));
    if (family.getLength() != 0) {
      out.write(this.family.get(), this.family.getOffset(), this.family.getLength());
    }
    if (qualifier != NULL) {
      out.write(this.qualifier.get(), this.qualifier.getOffset(), this.qualifier.getLength());
    }
    out.writeLong(ts);
    out.writeByte(this.type.getCode());
    if (this.value != NULL) {
      out.write(this.value.get(), this.value.getOffset(), this.value.getLength());
    }
  }

  @Override
  public int getLength() {
    return KEYVALUE_INFRASTRUCTURE_SIZE + KeyValue.ROW_LENGTH_SIZE + row.getLength()
        + KeyValue.FAMILY_LENGTH_SIZE + family.getLength() + qualifier.getLength()
        + KeyValue.TIMESTAMP_SIZE + KeyValue.TYPE_SIZE + value.getLength();
  }

  @Override
  public String toString() {
    return keyToString() + "/vlen=" + getValueLength() + "/ts=" + getMemstoreTS();
  }

  private String keyToString() {
    String row = Bytes.toStringBinary(this.row.get(), this.row.getOffset(), this.row.getLength());
    String family = this.family.getLength() == 0 ? "" : Bytes.toStringBinary(this.family.get(),
      this.family.getOffset(), this.family.getLength());
    String qualifier = this.qualifier.getLength() == 0 ? "" : Bytes.toStringBinary(
      this.qualifier.get(), this.qualifier.getOffset(), this.qualifier.getLength());
    String timestampStr = Long.toString(ts);
    byte type = this.type.getCode();
    return row + "/" + family + (family != null && family.length() > 0 ? ":" : "") + qualifier
        + "/" + timestampStr + "/" + Type.codeToType(type);
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!super.equals(obj)) return false;
    if (getClass() != obj.getClass()) return false;
    ClientKeyValue other = (ClientKeyValue) obj;
    if (family == null) {
      if (other.family != null) return false;
    } else if (!family.equals(other.family)) return false;
    if (qualifier == null) {
      if (other.qualifier != null) return false;
    } else if (!qualifier.equals(other.qualifier)) return false;
    if (row == null) {
      if (other.row != null) return false;
    } else if (!row.equals(other.row)) return false;
    if (ts != other.ts) return false;
    if (type != other.type) return false;
    if (value == null) {
      if (other.value != null) return false;
    } else if (!value.equals(other.value)) return false;
    return true;
  }

  @Override
  public int hashCode() {
    // TODO do we need to keep the same hashcode logic as KeyValue? Everywhere else we don't keep
    // them by reference, but presumably clients might hash them.
    final int prime = 31;
    int result = super.hashCode();
    result = prime * result + family.hashCode();
    result = prime * result + qualifier.hashCode();
    result = prime * result + row.hashCode();
    result = prime * result + (int) (ts ^ (ts >>> 32));
    result = prime * result + type.hashCode();
    result = prime * result + value.hashCode();
    return result;
  }

  @Override
  public void readFields(int length, DataInput in) throws IOException {
    throw new UnsupportedOperationException(ClientKeyValue.class.getSimpleName()
        + " should not be used for server-side operations");
  }

  @Override
  public void readFields(DataInput in) throws IOException {
    throw new UnsupportedOperationException(ClientKeyValue.class.getSimpleName()
        + " should not be used for server-side operations");
  }

  @Override
  public int getKeyOffset() {
    return 0;
  }


  @Override
  public int getFamilyOffset(int rlength) {
    return 0;
  }

  @Override
  public int getQualifierOffset(int foffset) {
    return 0;
  }

  @Override
  public int getTimestampOffset() {
    return 0;
  }

  @Override
  public int getTimestampOffset(int keylength) {
    return 0;
  }

  @Override
  public int getOffset() {
    return 0;
  }

  @Override
  public boolean updateLatestStamp(byte[] now) {
    if (this.isLatestTimestamp()) {
      // unfortunately, this is a bit slower than the usual kv, but we don't expect this to happen
      // all that often on the client (unless users are updating the ts this way), as it generally
      // happens on the server
      this.ts = Bytes.toLong(now);
      return true;
    }
    return false;
  }

  @Override
  public boolean isLatestTimestamp() {
    return this.ts == HConstants.LATEST_TIMESTAMP;
  }

  @Override
  public int getKeyLength() {
    return KEY_INFRASTRUCTURE_SIZE + getRowLength() + getFamilyLength() + getQualifierLength();
  }

  @Override
  public byte[] getKey() {
    throw new UnsupportedOperationException(ClientKeyValue.class.getSimpleName()
        + " does not support a single backing buffer.");
  }

  @Override
  public String getKeyString() {
    throw new UnsupportedOperationException(ClientKeyValue.class.getSimpleName()
        + " does not support a single backing buffer.");
  }

  @Override
  public SplitKeyValue split() {
    throw new UnsupportedOperationException(ClientKeyValue.class.getSimpleName()
        + " should not be used for server-side operations");
  }

  @Override
  public byte[] getBuffer() {
    throw new UnsupportedOperationException(ClientKeyValue.class.getSimpleName()
        + " does not support a single backing buffer.");
  }
}