/*
 * 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.datastax.driver.core;

import com.datastax.driver.core.exceptions.PagingStateException;
import com.datastax.driver.core.utils.Bytes;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

/**
 * The paging state of a query.
 *
 * <p>This object represents the next page to be fetched if the query is multi page. It can be saved
 * and reused later on the same statement.
 *
 * <p>The PagingState can be serialized and deserialized either as a String or as a byte array.
 *
 * @see Statement#setPagingState(PagingState)
 */
public class PagingState {

  private final byte[] pagingState;
  private final byte[] hash;
  private final ProtocolVersion protocolVersion;

  PagingState(
      ByteBuffer pagingState,
      Statement statement,
      ProtocolVersion protocolVersion,
      CodecRegistry codecRegistry) {
    this.pagingState = Bytes.getArray(pagingState);
    this.hash = hash(statement, protocolVersion, codecRegistry);
    this.protocolVersion = protocolVersion;
  }

  // The serialized form of the paging state is:
  //     size of raw state|size of hash|raw state|hash|protocol version
  //
  // The protocol version might be absent, in which case it defaults to V2 (this is for backward
  // compatibility with 2.0.10 where it is always absent).
  private PagingState(byte[] complete) {
    // Check the sizes in the beginning of the buffer, otherwise we cannot build the paging state
    // object
    ByteBuffer pagingStateBB = ByteBuffer.wrap(complete);
    int pagingSize = pagingStateBB.getShort();
    int hashSize = pagingStateBB.getShort();
    if (pagingSize + hashSize != pagingStateBB.remaining()
        && pagingSize + hashSize + 2 != pagingStateBB.remaining()) {
      throw new PagingStateException(
          "Cannot deserialize paging state, invalid format. "
              + "The serialized form was corrupted, or not initially generated from a PagingState object.");
    }
    this.pagingState = new byte[pagingSize];
    pagingStateBB.get(this.pagingState);
    this.hash = new byte[hashSize];
    pagingStateBB.get(this.hash);
    this.protocolVersion =
        (pagingStateBB.remaining() > 0)
            ? ProtocolVersion.fromInt(pagingStateBB.getShort())
            : ProtocolVersion.V2;
  }

  private byte[] hash(
      Statement statement, ProtocolVersion protocolVersion, CodecRegistry codecRegistry) {
    byte[] digest;
    ByteBuffer[] values;
    MessageDigest md;
    if (statement instanceof StatementWrapper) {
      statement = ((StatementWrapper) statement).getWrappedStatement();
    }
    assert !(statement instanceof BatchStatement);
    try {
      md = MessageDigest.getInstance("MD5");
      if (statement instanceof BoundStatement) {
        BoundStatement bs = ((BoundStatement) statement);
        md.update(bs.preparedStatement().getQueryString().getBytes());
        values = bs.wrapper.values;
      } else {
        // it is a RegularStatement since Batch statements are not allowed
        RegularStatement rs = (RegularStatement) statement;
        md.update(rs.getQueryString().getBytes());
        values = rs.getValues(protocolVersion, codecRegistry);
      }
      if (values != null) {
        for (ByteBuffer value : values) {
          md.update(value.duplicate());
        }
      }
      md.update(this.pagingState);
      digest = md.digest();

    } catch (NoSuchAlgorithmException e) {
      throw new RuntimeException("MD5 doesn't seem to be available on this JVM", e);
    }
    return digest;
  }

  boolean matches(Statement statement, CodecRegistry codecRegistry) {
    byte[] toTest = hash(statement, protocolVersion, codecRegistry);
    return Arrays.equals(toTest, this.hash);
  }

  private ByteBuffer generateCompleteOutput() {
    ByteBuffer res = ByteBuffer.allocate(pagingState.length + hash.length + 6);

    res.putShort((short) pagingState.length);
    res.putShort((short) hash.length);

    res.put(pagingState);
    res.put(hash);

    res.putShort((short) protocolVersion.toInt());

    res.rewind();

    return res;
  }

  ByteBuffer getRawState() {
    return ByteBuffer.wrap(this.pagingState);
  }

  @Override
  public String toString() {
    return Bytes.toRawHexString(generateCompleteOutput());
  }

  /**
   * Create a PagingState object from a string previously generated with {@link #toString()}.
   *
   * @param string the string value.
   * @return the PagingState object created.
   * @throws PagingStateException if the string does not have the correct format.
   */
  public static PagingState fromString(String string) {
    try {
      byte[] complete = Bytes.fromRawHexString(string, 0);
      return new PagingState(complete);
    } catch (Exception e) {
      throw new PagingStateException(
          "Cannot deserialize paging state, invalid format. "
              + "The serialized form was corrupted, or not initially generated from a PagingState object.",
          e);
    }
  }

  /**
   * Return a representation of the paging state object as a byte array.
   *
   * @return the paging state as a byte array.
   */
  public byte[] toBytes() {
    return generateCompleteOutput().array();
  }

  /**
   * Create a PagingState object from a byte array previously generated with {@link #toBytes()}.
   *
   * @param pagingState The byte array representation.
   * @return the PagingState object created.
   * @throws PagingStateException if the byte array does not have the correct format.
   */
  public static PagingState fromBytes(byte[] pagingState) {
    return new PagingState(pagingState);
  }
}
