/*
 * 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.beam.sdk.extensions.sql.zetasql;

import static com.google.zetasql.ZetaSQLType.TypeKind.TYPE_BOOL;
import static com.google.zetasql.ZetaSQLType.TypeKind.TYPE_BYTES;
import static com.google.zetasql.ZetaSQLType.TypeKind.TYPE_DATE;
import static com.google.zetasql.ZetaSQLType.TypeKind.TYPE_DOUBLE;
import static com.google.zetasql.ZetaSQLType.TypeKind.TYPE_INT64;
import static com.google.zetasql.ZetaSQLType.TypeKind.TYPE_STRING;
import static com.google.zetasql.ZetaSQLType.TypeKind.TYPE_TIME;
import static com.google.zetasql.ZetaSQLType.TypeKind.TYPE_TIMESTAMP;
import static java.util.stream.Collectors.toList;

import com.google.zetasql.ArrayType;
import com.google.zetasql.StructType;
import com.google.zetasql.StructType.StructField;
import com.google.zetasql.Type;
import com.google.zetasql.TypeFactory;
import com.google.zetasql.ZetaSQLType.TypeKind;
import java.util.List;
import java.util.function.Function;
import org.apache.beam.sdk.annotations.Internal;
import org.apache.beam.vendor.calcite.v1_20_0.org.apache.calcite.rel.type.RelDataType;
import org.apache.beam.vendor.calcite.v1_20_0.org.apache.calcite.rex.RexBuilder;
import org.apache.beam.vendor.calcite.v1_20_0.org.apache.calcite.sql.type.SqlTypeName;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ImmutableList;

/**
 * Utility methods for ZetaSQL <=> Calcite translation.
 *
 * <p>Unsupported ZetaSQL types: INT32, UINT32, UINT64, FLOAT, ENUM, PROTO, GEOGRAPHY
 * TODO[BEAM-10238]: support ZetaSQL types: TIME, DATETIME, NUMERIC
 */
@Internal
public final class ZetaSqlCalciteTranslationUtils {

  private ZetaSqlCalciteTranslationUtils() {}

  // Type conversion: Calcite => ZetaSQL
  public static Type toZetaType(RelDataType calciteType) {
    switch (calciteType.getSqlTypeName()) {
      case BIGINT:
        return TypeFactory.createSimpleType(TYPE_INT64);
      case DOUBLE:
        return TypeFactory.createSimpleType(TYPE_DOUBLE);
      case BOOLEAN:
        return TypeFactory.createSimpleType(TYPE_BOOL);
      case VARCHAR:
        return TypeFactory.createSimpleType(TYPE_STRING);
      case VARBINARY:
        return TypeFactory.createSimpleType(TYPE_BYTES);
      case DATE:
        return TypeFactory.createSimpleType(TYPE_DATE);
      case TIME:
        return TypeFactory.createSimpleType(TYPE_TIME);
      case TIMESTAMP:
        return TypeFactory.createSimpleType(TYPE_TIMESTAMP);
      case ARRAY:
        return TypeFactory.createArrayType(toZetaType(calciteType.getComponentType()));
      case ROW:
        List<StructField> structFields =
            calciteType.getFieldList().stream()
                .map(f -> new StructField(f.getName(), toZetaType(f.getType())))
                .collect(toList());

        return TypeFactory.createStructType(structFields);
      default:
        throw new UnsupportedOperationException("Unsupported RelDataType: " + calciteType);
    }
  }

  // Type conversion: ZetaSQL => Calcite
  public static SqlTypeName toCalciteTypeName(TypeKind type) {
    switch (type) {
      case TYPE_INT64:
        return SqlTypeName.BIGINT;
      case TYPE_DOUBLE:
        return SqlTypeName.DOUBLE;
      case TYPE_BOOL:
        return SqlTypeName.BOOLEAN;
      case TYPE_STRING:
        return SqlTypeName.VARCHAR;
      case TYPE_BYTES:
        return SqlTypeName.VARBINARY;
      case TYPE_DATE:
        return SqlTypeName.DATE;
      case TYPE_TIME:
        return SqlTypeName.TIME;
      case TYPE_TIMESTAMP:
        // TODO: handle timestamp with time zone.
        return SqlTypeName.TIMESTAMP;
        // TODO[BEAM-9179] Add conversion code for ARRAY and ROW types
      default:
        throw new UnsupportedOperationException("Unknown ZetaSQL type: " + type.name());
    }
  }

  public static RelDataType toRelDataType(RexBuilder rexBuilder, Type type, boolean isNullable) {
    if (type.getKind().equals(TypeKind.TYPE_ARRAY)) {
      return toArrayRelDataType(rexBuilder, type.asArray(), isNullable);
    } else if (type.getKind().equals(TypeKind.TYPE_STRUCT)) {
      return toStructRelDataType(rexBuilder, type.asStruct(), isNullable);
    } else {
      // TODO: Check type's nullability?
      return toSimpleRelDataType(type.getKind(), rexBuilder, isNullable);
    }
  }

  public static RelDataType toArrayRelDataType(
      RexBuilder rexBuilder, ArrayType arrayType, boolean isNullable) {
    // -1 cardinality means unlimited array size.
    // TODO: is unlimited array size right for general case?
    // TODO: whether isNullable should be ArrayType's nullablity (not its element type's?)
    return rexBuilder
        .getTypeFactory()
        .createArrayType(toRelDataType(rexBuilder, arrayType.getElementType(), isNullable), -1);
  }

  private static List<String> toNameList(List<StructField> fields) {
    ImmutableList.Builder<String> b = ImmutableList.builder();
    for (int i = 0; i < fields.size(); i++) {
      String name = fields.get(i).getName();
      if ("".equals(name)) {
        name = "$col" + i;
      }
      b.add(name);
    }
    return b.build();
  }

  public static RelDataType toStructRelDataType(
      RexBuilder rexBuilder, StructType structType, boolean isNullable) {

    List<StructField> fields = structType.getFieldList();
    List<String> fieldNames = toNameList(fields);
    List<RelDataType> fieldTypes =
        fields.stream()
            .map(f -> toRelDataType(rexBuilder, f.getType(), isNullable))
            .collect(toList());

    return rexBuilder.getTypeFactory().createStructType(fieldTypes, fieldNames);
  }

  // TODO: convert TIMESTAMP with/without TIMEZONE and DATETIME.
  public static RelDataType toSimpleRelDataType(TypeKind kind, RexBuilder rexBuilder) {
    return toSimpleRelDataType(kind, rexBuilder, true);
  }

  public static RelDataType toSimpleRelDataType(
      TypeKind kind, RexBuilder rexBuilder, boolean isNullable) {
    RelDataType relDataType = relDataTypeFactory(toCalciteTypeName(kind)).apply(rexBuilder);
    return nullable(rexBuilder, relDataType, isNullable);
  }

  private static RelDataType nullable(RexBuilder r, RelDataType relDataType, boolean isNullable) {
    return r.getTypeFactory().createTypeWithNullability(relDataType, isNullable);
  }

  private static Function<RexBuilder, RelDataType> relDataTypeFactory(SqlTypeName typeName) {
    return (RexBuilder r) -> r.getTypeFactory().createSqlType(typeName);
  }
}
