/*
 * 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.exec.physical.impl.join;

import javax.inject.Named;

import org.apache.drill.exec.exception.SchemaChangeException;
import org.apache.drill.exec.ops.FragmentContext;
import org.apache.drill.exec.physical.config.MergeJoinPOP;
import org.apache.drill.exec.record.VectorContainer;
import org.apache.calcite.rel.core.JoinRelType;

/**
 * Merge Join implementation using RecordIterator.
 */
public abstract class JoinTemplate implements JoinWorker {

  @Override
  public void setupJoin(FragmentContext context, JoinStatus status, VectorContainer outgoing) throws SchemaChangeException {
    doSetup(context, status, outgoing);
  }

  /**
   * Copy rows from the input record batches until the output record batch is full
   * @param status  State of the join operation (persists across multiple record batches/schema changes)
   * @return  true of join succeeded; false if the worker needs to be regenerated
   */
  public final boolean doJoin(final JoinStatus status) {
    final boolean isLeftJoin = (((MergeJoinPOP)status.outputBatch.getPopConfig()).getJoinType() == JoinRelType.LEFT);
    status.setHasMoreData(false);
    while (!status.isOutgoingBatchFull()) {
      if (status.right.finished()) {
        if (isLeftJoin) {
          while (!status.left.finished()) {
            if (status.isOutgoingBatchFull()) {
              status.setHasMoreData(true);
              return true;
            }
            doCopyLeft(status.left.getCurrentPosition(), status.getOutPosition());
            status.incOutputPos();
            status.left.next();
          }
        }
        return true;
      }
      if (status.left.finished()) {
        return true;
      }
      final int comparison = Integer.signum(doCompare(status.left.getCurrentPosition(), status.right.getCurrentPosition()));
      switch (comparison) {
        case -1:
          // left key < right key
          if (isLeftJoin) {
            doCopyLeft(status.left.getCurrentPosition(), status.getOutPosition());
            status.incOutputPos();
          }
          status.left.next();
          continue;

        case 0:
          // left key == right key
          // Mark current position in right iterator.
          // If we have set a mark in previous iteration but didn't finish the inner loop,
          // skip current right side as its already copied in earlier iteration.
          if (status.shouldMark()) {
            status.right.mark();
            // Copy all equal keys from right side to the output record batch.
            doCopyLeft(status.left.getCurrentPosition(), status.getOutPosition());
            doCopyRight(status.right.getCurrentPosition(), status.getOutPosition());
            status.incOutputPos();
          }
          if (status.isOutgoingBatchFull()) {
            // Leave iterators at their current positions and markers.
            // Don't mark on all subsequent doJoin iterations.
            status.setHasMoreData(true);
            status.disableMarking();
            return true;
          }
          // Move to next position in right iterator.
          status.right.next();
          while (!status.right.finished()) {
            if (doCompare(status.left.getCurrentPosition(), status.right.getCurrentPosition()) == 0) {
              doCopyLeft(status.left.getCurrentPosition(), status.getOutPosition());
              doCopyRight(status.right.getCurrentPosition(), status.getOutPosition());
              status.incOutputPos();
              if (status.isOutgoingBatchFull()) {
                status.setHasMoreData(true);
                status.disableMarking();
                return true;
              }
              status.right.next();
            } else {
              break;
            }
          }
          status.right.reset();
          // Enable marking only when we have consumed all equal keys on right side.
          status.enableMarking();
          status.left.next();
          continue;
        case 1:
          // left key > right key
          status.right.next();
          continue;

        default:
          throw new IllegalStateException();
      }
    }
    return true;
  }

  // Generated Methods

  public abstract void doSetup(@Named("context") FragmentContext context,
                               @Named("status") JoinStatus status,
                               @Named("outgoing") VectorContainer outgoing) throws SchemaChangeException;

  /**
   * Copy the data to the new record batch (if it fits).
   *
   * @param  leftIndex   position of batch (lower 16 bits) and record (upper 16 bits) in left SV4
   * @param  outIndex  position of the output record batch
   * @return Whether or not the data was copied.
   */
  public abstract void doCopyLeft(@Named("leftIndex") int leftIndex, @Named("outIndex") int outIndex);
  public abstract void doCopyRight(@Named("rightIndex") int rightIndex, @Named("outIndex") int outIndex);


  /**
   * Compare the values of the left and right join key to determine whether the left is less than, greater than
   * or equal to the right.
   *
   * @param leftIndex
   * @param rightIndex
   * @return  0 if both keys are equal
   *         -1 if left is < right
   *          1 if left is > right
   */
  protected abstract int doCompare(@Named("leftIndex") int leftIndex,
                                   @Named("rightIndex") int rightIndex);
}