/*
 * Copyright © 2012-2014 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 co.cask.tephra.distributed;

import co.cask.tephra.InvalidTruncateTimeException;
import co.cask.tephra.Transaction;
import co.cask.tephra.TransactionCouldNotTakeSnapshotException;
import co.cask.tephra.TransactionNotInProgressException;
import co.cask.tephra.distributed.thrift.TInvalidTruncateTimeException;
import co.cask.tephra.distributed.thrift.TTransactionCouldNotTakeSnapshotException;
import co.cask.tephra.distributed.thrift.TTransactionNotInProgressException;
import co.cask.tephra.distributed.thrift.TTransactionServer;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TTransport;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * This class is a wrapper around the thrift tx service client, it takes
 * Operations, converts them into thrift objects, calls the thrift
 * client, and converts the results back to data fabric classes.
 * This class also instruments the thrift calls with metrics.
 */
public class TransactionServiceThriftClient {
  private static final Function<byte[], ByteBuffer> BYTES_WRAPPER = new Function<byte[], ByteBuffer>() {
    @Override
    public ByteBuffer apply(byte[] input) {
      return ByteBuffer.wrap(input);
    }
  };

  /**
   * The thrift transport layer. We need this when we close the connection.
   */
  TTransport transport;

  /**
   * The actual thrift client.
   */
  TTransactionServer.Client client;

  /**
   * Whether this client is valid for use.
   */
  private final AtomicBoolean isValid = new AtomicBoolean(true);

  /**
   * Constructor from an existing, connected thrift transport.
   *
   * @param transport the thrift transport layer. It must already be connected
   */
  public TransactionServiceThriftClient(TTransport transport) {
    this.transport = transport;
    // thrift protocol layer, we use binary because so does the service
    TProtocol protocol = new TBinaryProtocol(transport);
    // and create a thrift client
    this.client = new TTransactionServer.Client(protocol);
  }

  /**
   * close this client. may be called multiple times
   */
  public void close() {
    if (this.transport.isOpen()) {
      this.transport.close();
    }
  }

  public Transaction startLong() throws TException {
    try {
      return TransactionConverterUtils.unwrap(client.startLong());
    } catch (TException e) {
      isValid.set(false);
      throw e;
    }
  }

  public Transaction startShort() throws TException {
    try {
      return TransactionConverterUtils.unwrap(client.startShort());
    } catch (TException e) {
      isValid.set(false);
      throw e;
    }
  }

  public Transaction startShort(int timeout) throws TException {
    try {
      return TransactionConverterUtils.unwrap(client.startShortTimeout(timeout));
    } catch (TException e) {
      isValid.set(false);
      throw e;
    }
  }

  public boolean canCommit(Transaction tx, Collection<byte[]> changeIds)
    throws TException, TransactionNotInProgressException {
    try {
      return client.canCommitTx(TransactionConverterUtils.wrap(tx),
                                ImmutableSet.copyOf(Iterables.transform(changeIds, BYTES_WRAPPER))).isValue();
    } catch (TTransactionNotInProgressException e) {
      throw new TransactionNotInProgressException(e.getMessage());
    } catch (TException e) {
      isValid.set(false);
      throw e;
    }
  }



  public boolean commit(Transaction tx) throws TException, TransactionNotInProgressException {
    try {
      return client.commitTx(TransactionConverterUtils.wrap(tx)).isValue();
    } catch (TTransactionNotInProgressException e) {
      throw new TransactionNotInProgressException(e.getMessage());
    } catch (TException e) {
      isValid.set(false);
      throw e;
    }
  }

  public void abort(Transaction tx) throws TException {
    try {
      client.abortTx(TransactionConverterUtils.wrap(tx));
    } catch (TException e) {
      isValid.set(false);
      throw e;
    }
  }

  public boolean invalidate(long tx) throws TException {
    try {
      return client.invalidateTx(tx);
    } catch (TException e) {
      isValid.set(false);
      throw e;
    }
  }

  public Transaction checkpoint(Transaction tx) throws TException {
    try {
      return TransactionConverterUtils.unwrap(client.checkpoint(TransactionConverterUtils.wrap(tx)));
    } catch (TException e) {
      isValid.set(false);
      throw e;
    }
  }

  public InputStream getSnapshotStream() throws TException, TransactionCouldNotTakeSnapshotException {
    try {
      ByteBuffer buffer = client.getSnapshot();
      if (buffer.hasArray()) {
        return new ByteArrayInputStream(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
      }

      // The ByteBuffer is not backed by array. Read the content to a new byte array and return an InputStream of that.
      byte[] snapshot = new byte[buffer.remaining()];
      buffer.get(snapshot);
      return new ByteArrayInputStream(snapshot);
    } catch (TTransactionCouldNotTakeSnapshotException e) {
      throw new TransactionCouldNotTakeSnapshotException(e.getMessage());
    } catch (TException e) {
      isValid.set(false);
      throw e;
    }
  }

  public String status() throws TException {
    try {
      return client.status();
    } catch (TException e) {
      isValid.set(false);
      throw e;
    }
  }

  public void resetState() throws TException {
    try {
        client.resetState();
    } catch (TException e) {
      isValid.set(false);
      throw e;
    }
  }

  public boolean truncateInvalidTx(Set<Long> invalidTxIds) throws TException {
    try {
      return client.truncateInvalidTx(invalidTxIds).isValue();
    } catch (TException e) {
      isValid.set(false);
      throw e;
    }
  }
  
  public boolean truncateInvalidTxBefore(long time) throws TException, InvalidTruncateTimeException {
    try {
      return client.truncateInvalidTxBefore(time).isValue();
    } catch (TInvalidTruncateTimeException e) {
      throw new InvalidTruncateTimeException(e.getMessage());
    } catch (TException e) {
      isValid.set(false);
      throw e;
    }
  }
  
  public int getInvalidSize() throws TException {
    try {
      return client.invalidTxSize();
    } catch (TException e) {
      isValid.set(false);
      throw e;
    }
  }

  public boolean isValid() {
    return isValid.get();
  }
}
