/*
 * 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 org.apache.drill;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.drill.common.exceptions.UserException;
import org.apache.drill.exec.proto.UserBitShared.QueryId;
import org.apache.drill.exec.proto.UserBitShared.QueryData;
import org.apache.drill.exec.proto.UserBitShared.QueryResult.QueryState;
import org.apache.drill.exec.proto.UserBitShared.DrillPBError;
import org.apache.drill.exec.rpc.ConnectionThrottle;
import org.apache.drill.exec.rpc.user.QueryDataBatch;
import org.apache.drill.exec.rpc.user.UserResultsListener;

/**
 * Result listener that is set up to receive a single row. Useful for queries
 * such with a count(*) or limit 1. The abstract method {@link #rowArrived(QueryDataBatch)} provides
 * the means for a derived class to get the expected record's data.
 */
public abstract class SingleRowListener implements UserResultsListener {
  private final CountDownLatch latch = new CountDownLatch(1); // used to wait for completion
  private final AtomicInteger nRows = new AtomicInteger(0); // counts rows received
  private QueryState queryState = null; // last received QueryState, if any
  private final List<DrillPBError> errorList = new LinkedList<>(); // all errors ever received
  private Exception exception = null; // the exception captured from a submission failure

  @Override
  public void queryIdArrived(final QueryId queryId) {
  }

  @Override
  public void submissionFailed(final UserException ex) {
    exception = ex;
    synchronized(errorList) {
      errorList.add(ex.getOrCreatePBError(false));
    }
    latch.countDown();
  }

  @Override
  public void queryCompleted(QueryState state) {
    queryState = state;
    try {
      cleanup();
    } finally {
      latch.countDown();
    }
  }

  @Override
  public void dataArrived(final QueryDataBatch result, final ConnectionThrottle throttle) {
    final QueryData queryData = result.getHeader();
    if (result.hasData()) {
      final int nRows = this.nRows.addAndGet(queryData.getRowCount());
      if (nRows > 1) {
        throw new IllegalStateException("Expected exactly one row, but got " + nRows);
      }

      rowArrived(result);
    }

    result.release();
  }

  /**
   * Get the last known QueryState.
   *
   * @return the query state; may be null if no query state has been received
   */
  public QueryState getQueryState() {
    return queryState;
  }

  /**
   * Get an immutable copy of the list of all errors received so far.
   *
   * @return list of errors received
   */
  public List<DrillPBError> getErrorList() {
    synchronized(errorList) {
      return Collections.unmodifiableList(errorList);
    }
  }

  /**
   * A record has arrived and is ready for access.
   *
   * <p>Derived classes provide whatever implementation they require here to access
   * the record's data.
   *
   * @param queryDataBatch result batch holding the row
   */
  protected abstract void rowArrived(QueryDataBatch queryDataBatch);

  /**
   * Wait for the completion of this query; receiving a record or an error will both cause the
   * query to be considered complete
   *
   * @throws Exception if there was any kind of problem
   */
  public void waitForCompletion() throws Exception {
    latch.await();
    if (exception != null) {
      throw new RuntimeException("Query submission failed", exception);
    }
  }

  /**
   * Clean up any resources used.
   *
   * <p>Derived classes may use this to free things like allocators or files that were used to
   * record data received in resultArrived().
   */
  public void cleanup() {
  }
}
