/*
 * 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.planner.sql.handlers;

import org.apache.calcite.schema.Table;
import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlCallBinding;
import org.apache.calcite.sql.SqlFunction;
import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.validate.SqlUserDefinedTableMacro;
import org.apache.calcite.util.Util;
import org.apache.drill.common.exceptions.UserException;
import org.apache.drill.exec.planner.logical.DrillTable;
import org.apache.drill.exec.planner.sql.SchemaUtilities;
import org.apache.drill.exec.store.AbstractSchema;
import com.google.common.base.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * Holder class for {@link DrillTable}, {@code tableName} and table {@code schemaPath} obtained from
 * {@code SqlNode tableRef}.
 */
public class DrillTableInfo {
  private static final Logger logger = LoggerFactory.getLogger(DrillTableInfo.class);

  private final DrillTable drillTable;
  private final String tableName;
  private final List<String> schemaPath;

  private DrillTableInfo(DrillTable drillTable, List<String> schemaPath, String tableName) {
    this.drillTable = drillTable;
    this.tableName = tableName;
    this.schemaPath = schemaPath;
  }

  public DrillTable drillTable() {
    return drillTable;
  }

  public String tableName() {
    return tableName;
  }

  public List<String> schemaPath() {
    return schemaPath;
  }

  /**
   * Returns {@link DrillTableInfo} instance which holds {@link DrillTable}, {@code drillTable},
   * {@code schemaPath} corresponding to specified {@code tableRef}.
   *
   * @param tableRef table ref
   * @param config   handler config
   * @return {@link DrillTableInfo} instance
   */
  public static DrillTableInfo getTableInfoHolder(SqlNode tableRef, SqlHandlerConfig config) {
    switch (tableRef.getKind()) {
      case COLLECTION_TABLE: {
        SqlCall call = (SqlCall) config.getConverter().validate(tableRef);
        assert call.getOperandList().size() == 1;
        SqlOperator operator = ((SqlCall) call.operand(0)).getOperator();
        assert operator instanceof SqlUserDefinedTableMacro;
        SqlUserDefinedTableMacro tableMacro = (SqlUserDefinedTableMacro) operator;
        SqlIdentifier tableIdentifier = tableMacro.getSqlIdentifier();

        AbstractSchema drillSchema = SchemaUtilities.resolveToDrillSchema(
            config.getConverter().getDefaultSchema(), SchemaUtilities.getSchemaPath(tableIdentifier));

        DrillTable table = (DrillTable) tableMacro.getTable(new SqlCallBinding(config.getConverter().getValidator(), null, call.operand(0)));
        return new DrillTableInfo(table, drillSchema.getSchemaPath(), Util.last(tableIdentifier.names));
      }
      case IDENTIFIER: {
        SqlIdentifier tableIdentifier = (SqlIdentifier) tableRef;
        AbstractSchema drillSchema = SchemaUtilities.resolveToDrillSchema(
            config.getConverter().getDefaultSchema(), SchemaUtilities.getSchemaPath(tableIdentifier));
        String tableName = Util.last(tableIdentifier.names);
        DrillTable table = getDrillTable(drillSchema, tableName);
        return new DrillTableInfo(table, drillSchema.getSchemaPath(), tableName);
      }
      default:
        throw new UnsupportedOperationException("Unsupported table ref kind: " + tableRef.getKind());
    }
  }

  /**
   * Returns list with operands for table function, obtained from specified call in the order
   * suitable to be used in table function and default values for absent arguments.
   * For example, for the following call:
   * <pre>
   *   `dfs`.`corrupted_dates`(`type` => 'parquet', `autoCorrectCorruptDates` => FALSE, `enableStringsSignedMinMax` => FALSE)
   * </pre>
   * will be returned the following list:
   * <pre>
   *   ['parquet', FALSE, FALSE, DEFAULT]
   * </pre>
   * whose elements correspond to the following parameters:
   * <pre>
   *   [type, autoCorrectCorruptDates, enableStringsSignedMinMax, schema]
   * </pre>
   *
   * @param call sql call whose arguments should be prepared
   * @return list with operands for table function
   */
  private static List<SqlNode> prepareTableMacroOperands(SqlCall call) {
    Function<String, SqlNode> convertOperand = paramName -> call.getOperandList().stream()
        .map(sqlNode -> (SqlCall) sqlNode)
        .filter(sqlCall -> ((SqlIdentifier) sqlCall.operand(1)).getSimple().equals(paramName))
        .peek(sqlCall -> Preconditions.checkState(sqlCall.getKind() == SqlKind.ARGUMENT_ASSIGNMENT))
        .findFirst()
        .map(sqlCall -> (SqlNode) sqlCall.operand(0))
        .orElse(SqlStdOperatorTable.DEFAULT.createCall(SqlParserPos.ZERO));

    SqlFunction operator = (SqlFunction) call.getOperator();

    return operator.getParamNames().stream()
        .map(convertOperand)
        .collect(Collectors.toList());
  }

  private static DrillTable getDrillTable(AbstractSchema drillSchema, String tableName) {
    Table tableFromSchema = SqlHandlerUtil.getTableFromSchema(drillSchema, tableName);

    if (tableFromSchema == null) {
      throw UserException.validationError()
          .message("No table with given name [%s] exists in schema [%s]", tableName, drillSchema.getFullSchemaName())
          .build(logger);
    }

    switch (tableFromSchema.getJdbcTableType()) {
      case TABLE:
      case SYSTEM_TABLE:
        if (tableFromSchema instanceof DrillTable) {
          return (DrillTable) tableFromSchema;
        } else {
          throw UserException.validationError()
              .message("[%s] table kind is not supported", tableFromSchema.getClass().getSimpleName())
              .build(logger);
        }
      default:
        throw UserException.validationError()
            .message("[%s] object type is not supported", tableFromSchema.getJdbcTableType())
            .build(logger);
    }
  }
}
