/*
 * 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.scan.project;

import org.apache.drill.common.exceptions.CustomErrorContext;
import org.apache.drill.common.expression.SchemaPath;
import org.apache.drill.exec.physical.impl.scan.file.FileMetadataColumn;
import org.apache.drill.exec.physical.impl.scan.file.FileMetadataColumnDefn;
import org.apache.drill.exec.physical.impl.scan.project.NullColumnBuilder.NullBuilderBuilder;
import org.apache.drill.exec.physical.impl.scan.project.ResolvedTuple.ResolvedRow;
import org.apache.drill.exec.physical.resultSet.ResultSetLoader;
import org.apache.drill.exec.physical.resultSet.impl.ResultSetOptionBuilder;
import org.apache.drill.exec.physical.resultSet.impl.ResultSetLoaderImpl;
import org.apache.drill.exec.record.VectorContainer;
import org.apache.drill.exec.record.metadata.TupleMetadata;
import org.apache.drill.exec.store.ColumnExplorer.ImplicitInternalFileColumns;
import org.apache.drill.exec.vector.ValueVector;
import org.apache.drill.shaded.guava.com.google.common.annotations.VisibleForTesting;

import java.util.List;

/**
 * Orchestrates projection tasks for a single reader within the set that the
 * scan operator manages. Vectors are reused across readers, but via a vector
 * cache. All other state is distinct between readers.
 */
public class ReaderSchemaOrchestrator implements VectorSource {

  private final ScanSchemaOrchestrator scanOrchestrator;
  private long limit;
  private int readerBatchSize;
  private ResultSetLoaderImpl tableLoader;
  private int prevTableSchemaVersion = -1;

  /**
   * Assembles the table, metadata and null columns into the final output
   * batch to be sent downstream. The key goal of this class is to "smooth"
   * schema changes in this output batch by absorbing trivial schema changes
   * that occur across readers.
   */
  private ResolvedRow rootTuple;
  private VectorContainer tableContainer;

  public ReaderSchemaOrchestrator(
      ScanSchemaOrchestrator scanSchemaOrchestrator, long limit) {
    scanOrchestrator = scanSchemaOrchestrator;
    this.limit = limit;
    readerBatchSize = scanOrchestrator.options.scanBatchRecordLimit;
  }

  public void setBatchSize(int size) {
    if (size > 0) {
      readerBatchSize = size;
    }
  }

  @VisibleForTesting
  public ResultSetLoader makeTableLoader(TupleMetadata readerSchema) {
    return makeTableLoader(scanOrchestrator.scanProj.context(), readerSchema, -1);
  }

  public ResultSetLoader makeTableLoader(CustomErrorContext errorContext,
      TupleMetadata readerSchema, long localLimit) {
    ResultSetOptionBuilder options = new ResultSetOptionBuilder();
    options.rowCountLimit(Math.min(readerBatchSize, scanOrchestrator.options.scanBatchRecordLimit));
    options.vectorCache(scanOrchestrator.vectorCache);
    options.batchSizeLimit(scanOrchestrator.options.scanBatchByteLimit);
    options.errorContext(errorContext);

    // Set up a selection list if available and is a subset of
    // table columns. (Only needed for non-wildcard queries.)
    // The projection list includes all candidate table columns
    // whether or not they exist in the up-front schema. Handles
    // the odd case where the reader claims a fixed schema, but
    // adds a column later.
    options.projectionFilter(scanOrchestrator.scanProj.readerProjection);
    options.readerSchema(readerSchema);
    if (limit < 0) {
      limit = localLimit;
    } else if (localLimit >= 0) {
      limit = Math.min(localLimit, limit);
    }
    options.limit(limit);

    // Create the table loader
    tableLoader = new ResultSetLoaderImpl(scanOrchestrator.allocator, options.build());
    return tableLoader;
  }

  public boolean hasSchema() {
    return prevTableSchemaVersion >= 0;
  }

  public void defineSchema() {
    tableLoader.startEmptyBatch();
    endBatch();
  }

  public boolean startBatch() {
    return tableLoader.startBatch();
  }

  /**
   * Build the final output batch by projecting columns from the three input sources
   * to the output batch. First, build the metadata and/or null columns for the
   * table row count. Then, merge the sources.
   */
  public void endBatch() {
    endBatch(false);
  }

  /**
   * Build the final output batch by projecting columns from the three input sources
   * to the output batch. First, build the metadata and/or null columns for the
   * table row count. Then, merge the sources.
   *
   * @param eof is end of file
   */
  public boolean endBatch(boolean eof) {

    // Get the batch results in a container.
    tableContainer = tableLoader.harvest();

    boolean projected = resolveProjectingMetadata(eof);

    // If the schema changed, set up the final projection based on
    // the new (or first) schema.
    if (prevTableSchemaVersion < tableLoader.schemaVersion()) {
      reviseOutputProjection();
    } else {

      // Fill in the null and metadata columns.
      populateNonDataColumns();
    }
    if (projected) {
      projectMetadata(false);
    }
    int rowCount = tableContainer.getRecordCount();
    rootTuple.setRowCount(rowCount);
    scanOrchestrator.tallyBatch(rowCount);
    return eof || tableLoader.atLimit();
  }

  /**
   * Updates {@code PROJECT_METADATA} implicit column value to {@code "FALSE"} to handle current batch as
   * a batch with metadata information only for the case when this batch is first and empty.
   */
  private boolean resolveProjectingMetadata(boolean eof) {
    if (tableContainer.getRecordCount() == 0 && !hasSchema() && eof) {
      if (projectMetadata(true)) {
        tableContainer.setValueCount(tableContainer.getRecordCount() + 1);
        return true;
      }
    }
    return false;
  }

  /**
   * Sets {@code PROJECT_METADATA} implicit column value.
   *
   * @param projectMetadata whether {@code PROJECT_METADATA} should be replaced
   * @return {@code true} if {@code PROJECT_METADATA} implicit column
   * is present in scan projection and its value is updated
   */
  private boolean projectMetadata(boolean projectMetadata) {
    ImplicitInternalFileColumns original;
    ImplicitInternalFileColumns newColumn;
    if (projectMetadata) {
      original = ImplicitInternalFileColumns.USE_METADATA;
      newColumn = ImplicitInternalFileColumns.PROJECT_METADATA;
    } else {
      original = ImplicitInternalFileColumns.PROJECT_METADATA;
      newColumn = ImplicitInternalFileColumns.USE_METADATA;
    }
    List<ColumnProjection> outputColumns = scanOrchestrator.scanProj.columns();
    for (int i = 0; i < outputColumns.size(); i++) {
      ColumnProjection outputColumn = outputColumns.get(i);
      if (outputColumn instanceof FileMetadataColumn) {
        FileMetadataColumn metadataColumn = (FileMetadataColumn) outputColumn;
        if (metadataColumn.defn().defn == original) {

          projectMetadata = scanOrchestrator.scanProj.requestedCols().stream()
              .anyMatch(SchemaPath.getSimplePath(metadataColumn.name())::equals);

          if (projectMetadata) {
            outputColumns.set(i,
                new FileMetadataColumn(
                    metadataColumn.name(),
                    new FileMetadataColumnDefn(metadataColumn.defn().colName(), newColumn)));
          }
          return projectMetadata;
        }
      }
    }
    return false;
  }

  private void populateNonDataColumns() {
    int rowCount = tableContainer.getRecordCount();
    scanOrchestrator.metadataManager.load(rowCount);
    rootTuple.loadNulls(rowCount);
  }

  /**
   * Create the list of null columns by comparing the SELECT list against the
   * columns available in the batch schema. Create null columns for those that
   * are missing. This is done for the first batch, and any time the schema
   * changes. (For early-schema, the projection occurs once as the schema is set
   * up-front and does not change.) For a SELECT *, the null column check
   * only need be done if null columns were created when mapping from a prior
   * schema.
   */
  private void reviseOutputProjection() {

    // Do the table-schema level projection; the final matching
    // of projected columns to available columns.

    TupleMetadata readerSchema = tableLoader.outputSchema();
    if (scanOrchestrator.schemaSmoother != null) {
      doSmoothedProjection(readerSchema);
    } else {
      switch(scanOrchestrator.scanProj.projectionType()) {
        case EMPTY:
        case EXPLICIT:
          doExplicitProjection(readerSchema);
          break;
        case SCHEMA_WILDCARD:
        case STRICT_SCHEMA_WILDCARD:
          doStrictWildcardProjection(readerSchema);
          break;
        case WILDCARD:
          doWildcardProjection(readerSchema);
          break;
        default:
          throw new IllegalStateException(scanOrchestrator.scanProj.projectionType().toString());
      }
    }

    // Combine metadata, nulls and batch data to form the final
    // output container. Columns are created by the metadata and null
    // loaders only in response to a batch, so create the first batch.
    rootTuple.buildNulls(scanOrchestrator.vectorCache);
    scanOrchestrator.metadataManager.define();
    populateNonDataColumns();
    rootTuple.project(tableContainer, scanOrchestrator.outputContainer);
    prevTableSchemaVersion = tableLoader.schemaVersion();
  }

  private void doSmoothedProjection(TupleMetadata tableSchema) {
    rootTuple = newRootTuple();
    scanOrchestrator.schemaSmoother.resolve(tableSchema, rootTuple);
  }

  /**
   * Query contains a wildcard. The schema-level projection includes
   * all columns provided by the reader.
   */
  private void doWildcardProjection(TupleMetadata tableSchema) {
    rootTuple = newRootTuple();
    new WildcardProjection(scanOrchestrator.scanProj, tableSchema, rootTuple, scanOrchestrator.options.schemaResolvers);
  }

  private void doStrictWildcardProjection(TupleMetadata tableSchema) {
    rootTuple = newRootTuple();
    new WildcardSchemaProjection(scanOrchestrator.scanProj, tableSchema, rootTuple, scanOrchestrator.options.schemaResolvers);
  }

  private ResolvedRow newRootTuple() {
    return new ResolvedRow(new NullBuilderBuilder()
        .setNullType(scanOrchestrator.options.nullType)
        .allowRequiredNullColumns(scanOrchestrator.options.allowRequiredNullColumns)
        .setOutputSchema(scanOrchestrator.options.providedSchema())
        .build());
  }

  /**
   * Explicit projection: include only those columns actually
   * requested by the query, which may mean filling in null
   * columns for projected columns that don't actually exist
   * in the table.
   *
   * @param tableSchema newly arrived schema
   */
  private void doExplicitProjection(TupleMetadata tableSchema) {
    rootTuple = newRootTuple();
    new ExplicitSchemaProjection(scanOrchestrator.scanProj, tableSchema, rootTuple,
      scanOrchestrator.options.schemaResolvers);
  }

  @Override
  public ValueVector vector(int index) {
    return tableContainer.getValueVector(index).getValueVector();
  }

  public void close() {
    RuntimeException ex = null;
    try {
      if (tableLoader != null) {
        tableLoader.close();
        tableLoader = null;
      }
    }
    catch (RuntimeException e) {
      ex = e;
    }
    try {
      if (rootTuple != null) {
        rootTuple.close();
        rootTuple = null;
      }
    }
    catch (RuntimeException e) {
      ex = ex == null ? e : ex;
    }
    scanOrchestrator.metadataManager.endFile();
    if (ex != null) {
      throw ex;
    }
  }
}
