/*
 * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved.
 */

package net.snowflake.client.core;

import net.snowflake.client.jdbc.ErrorCode;
import net.snowflake.client.jdbc.SnowflakeColumnMetadata;
import net.snowflake.client.jdbc.SnowflakeUtil;
import net.snowflake.client.log.SFLogger;
import net.snowflake.client.log.SFLoggerFactory;
import net.snowflake.common.core.SFTime;
import net.snowflake.common.core.SFTimestamp;
import net.snowflake.common.core.SnowflakeDateTimeFormat;

import java.sql.Date;
import java.sql.ResultSetMetaData;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

/**
 * Snowflake ResultSetMetaData
 */
public class SFResultSetMetaData
{
  static final SFLogger logger =
      SFLoggerFactory.getLogger(SFResultSetMetaData.class);

  private int columnCount = 0;

  private List<String> columnNames;

  private List<String> columnTypeNames;

  private List<Integer> columnTypes;

  private List<Integer> precisions;

  private List<Integer> scales;

  private List<Integer> nullables;

  private List<String> columnSrcTables;

  private List<String> columnSrcSchemas;

  private List<String> columnSrcDatabases;

  private List<Integer> columnDisplaySizes;

  private String queryId;

  private Map<String, Integer> columnNamePositionMap = new HashMap<>();

  private Map<String, Integer> columnNameUpperCasePositionMap = new HashMap<>();

  // For creating incidents
  private SFSession session;

  // Date time formatter for calculating the display size
  private SnowflakeDateTimeFormat timestampNTZFormatter;

  private SnowflakeDateTimeFormat timestampLTZFormatter;

  private SnowflakeDateTimeFormat timestampTZFormatter;

  private SnowflakeDateTimeFormat timeFormatter;

  private SnowflakeDateTimeFormat dateFormatter;

  // provide default display size for databasemetadata result set.
  // i.e. result set returned calling getTables etc
  private int timestampNTZStringLength = 30;

  private int timestampLTZStringLength = 30;

  private int timestampTZStringLength = 30;

  private int timeStringLength = 18;

  private int dateStringLength = 10;

  private boolean isResultColumnCaseInsensitive = false;

  public SFResultSetMetaData(int columnCount,
                             List<String> columnNames,
                             List<String> columnTypeNames,
                             List<Integer> columnTypes,
                             SFSession session)
  {
    this.columnCount = columnCount;
    this.columnNames = columnNames;
    this.columnTypeNames = columnTypeNames;
    this.columnTypes = columnTypes;
    this.session = session;
  }

  public SFResultSetMetaData(List<SnowflakeColumnMetadata> columnMetadata,
                             SFSession session,
                             SnowflakeDateTimeFormat timestampNTZFormatter,
                             SnowflakeDateTimeFormat timestampLTZFormatter,
                             SnowflakeDateTimeFormat timestampTZFormatter,
                             SnowflakeDateTimeFormat dateFormatter,
                             SnowflakeDateTimeFormat timeFormatter)
  {
    this(columnMetadata, "none", session,
         (session != null) && session.isResultColumnCaseInsensitive(),
         timestampNTZFormatter,
         timestampLTZFormatter,
         timestampTZFormatter,
         dateFormatter,
         timeFormatter);
  }

  public SFResultSetMetaData(List<SnowflakeColumnMetadata> columnMetadata,
                             String queryId,
                             SFSession session,
                             boolean isResultColumnCaseInsensitive,
                             SnowflakeDateTimeFormat timestampNTZFormatter,
                             SnowflakeDateTimeFormat timestampLTZFormatter,
                             SnowflakeDateTimeFormat timestampTZFormatter,
                             SnowflakeDateTimeFormat dateFormatter,
                             SnowflakeDateTimeFormat timeFormatter
  )
  {
    this.columnCount = columnMetadata.size();
    this.queryId = queryId;
    this.timestampNTZFormatter = timestampNTZFormatter;
    this.timestampLTZFormatter = timestampLTZFormatter;
    this.timestampTZFormatter = timestampTZFormatter;
    this.dateFormatter = dateFormatter;
    this.timeFormatter = timeFormatter;
    calculateDateTimeStringLength();

    this.columnNames = new ArrayList<>(this.columnCount);
    this.columnTypeNames = new ArrayList<>(this.columnCount);
    this.columnTypes = new ArrayList<>(this.columnCount);
    this.precisions = new ArrayList<>(this.columnCount);
    this.scales = new ArrayList<>(this.columnCount);
    this.nullables = new ArrayList<>(this.columnCount);
    this.columnSrcDatabases = new ArrayList<>(this.columnCount);
    this.columnSrcSchemas = new ArrayList<>(this.columnCount);
    this.columnSrcTables = new ArrayList<>(this.columnCount);
    this.columnDisplaySizes = new ArrayList<>(this.columnCount);
    this.isResultColumnCaseInsensitive = isResultColumnCaseInsensitive;

    for (int colIdx = 0; colIdx < columnCount; colIdx++)
    {
      columnNames.add(columnMetadata.get(colIdx).getName());
      columnTypeNames.add(columnMetadata.get(colIdx).getTypeName());
      precisions.add(calculatePrecision(columnMetadata.get(colIdx)));
      columnTypes.add(columnMetadata.get(colIdx).getType());
      scales.add(columnMetadata.get(colIdx).getScale());
      nullables.add(columnMetadata.get(colIdx).isNullable() ?
                    ResultSetMetaData.columnNullable : ResultSetMetaData.columnNoNulls);
      columnSrcDatabases.add(columnMetadata.get(colIdx).getColumnSrcDatabase());
      columnSrcSchemas.add(columnMetadata.get(colIdx).getColumnSrcSchema());
      columnSrcTables.add(columnMetadata.get(colIdx).getColumnSrcTable());
      columnDisplaySizes.add(calculateDisplaySize(columnMetadata.get(colIdx)));
    }

    this.session = session;
  }

  private Integer calculatePrecision(SnowflakeColumnMetadata columnMetadata)
  {
    int columnType = columnMetadata.getType();
    switch (columnType)
    {
      case Types.CHAR:
      case Types.VARCHAR:
      case Types.BINARY:
        return columnMetadata.getLength();
      case Types.INTEGER:
      case Types.DECIMAL:
      case Types.BIGINT:
        return columnMetadata.getPrecision();
      case Types.DATE:
        return dateStringLength;
      case Types.TIME:
        return timeStringLength;
      case SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_LTZ:
        return timestampLTZStringLength;
      case SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_TZ:
        return timestampTZStringLength;
      case Types.TIMESTAMP:
        return timestampNTZStringLength;
      // for double and boolean
      // Precision is not applicable hence return 0
      default:
        return 0;
    }
  }

  private Integer calculateDisplaySize(SnowflakeColumnMetadata columnMetadata)
  {
    int columnType = columnMetadata.getType();
    switch (columnType)
    {
      case Types.CHAR:
      case Types.VARCHAR:
      case Types.BINARY:
        return columnMetadata.getLength();
      case Types.INTEGER:
      case Types.BIGINT:
        // + 1 because number can be negative, it could be -20 for number(2,0)
        return columnMetadata.getPrecision() + 1;
      case Types.DECIMAL:
        // first + 1 because number can be negative, second + 1 because it always
        // include decimal point.
        // i.e. number(2, 1) could be -1.3
        return columnMetadata.getPrecision() + 1 + 1;
      case Types.DOUBLE:
        // Hard code as 24 since the longest float
        // represented in char is
        // -2.2250738585072020E−308
        return 24;
      case Types.DATE:
        return dateStringLength;
      case Types.TIME:
        return timeStringLength;
      case SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_LTZ:
        return timestampLTZStringLength;
      case SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_TZ:
        return timestampTZStringLength;
      case Types.TIMESTAMP:
        return timestampNTZStringLength;
      case Types.BOOLEAN:
        // Hard code as 5 since the longest char to represent
        // a boolean would be false, which is 5.
        return 5;
      default:
        return 25;
    }
  }

  private void calculateDateTimeStringLength()
  {
    SFTimestamp ts = SFTimestamp.fromMilliseconds(System.currentTimeMillis(), TimeZone.getDefault());
    try
    {
      if (timestampNTZFormatter != null)
      {
        String tsNTZStr = ResultUtil.getSFTimestampAsString(ts, Types.TIMESTAMP, 9, timestampNTZFormatter,
                                                            timestampLTZFormatter, timestampTZFormatter, session);
        timestampNTZStringLength = tsNTZStr.length();
      }
      if (timestampLTZFormatter != null)
      {
        String tsLTZStr = ResultUtil.getSFTimestampAsString(ts, SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_LTZ, 9,
                                                            timestampNTZFormatter, timestampLTZFormatter, timestampTZFormatter, session);
        timestampLTZStringLength = tsLTZStr.length();
      }
      if (timestampTZFormatter != null)
      {
        String tsTZStr = ResultUtil.getSFTimestampAsString(ts, SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_TZ, 9,
                                                           timestampNTZFormatter, timestampLTZFormatter, timestampTZFormatter, session);
        timestampTZStringLength = tsTZStr.length();
      }

      SFTime time = SFTime.fromTimestamp(ts);
      if (timeFormatter != null)
      {
        timeStringLength = ResultUtil.getSFTimeAsString(time, 9, timeFormatter).length();
      }
      if (dateFormatter != null)
      {
        final Calendar calendar = Calendar.getInstance();
        calendar.set(2015, Calendar.DECEMBER, 11);
        dateStringLength = ResultUtil.getDateAsString(new Date(calendar.getTimeInMillis()),
                                                      dateFormatter).length();
      }
    }
    catch (SFException e)
    {
      logger.debug("Failed to calculate the display size. Use default one.");
    }
  }

  /**
   * get the query id
   *
   * @return query id
   */
  public String getQueryId()
  {
    return queryId;
  }

  /**
   * Get the list of column names
   *
   * @return column names in list
   */
  public List<String> getColumnNames()
  {
    return columnNames;
  }

  /**
   * Get the index of the column by name
   *
   * @param columnName column name
   * @return index of the column that names matches the column name
   */
  public int getColumnIndex(String columnName)
  {
    columnName = isResultColumnCaseInsensitive ? columnName.toUpperCase() : columnName;
    Map<String, Integer> nameToIndexMap = isResultColumnCaseInsensitive ?
                                          columnNameUpperCasePositionMap : columnNamePositionMap;

    if (nameToIndexMap.get(columnName) != null)
    {
      return nameToIndexMap.get(columnName);
    }
    else
    {
      int columnIndex = isResultColumnCaseInsensitive ?
                        ResultUtil.listSearchCaseInsensitive(columnNames, columnName) :
                        columnNames.indexOf(columnName);
      nameToIndexMap.put(columnName, columnIndex);
      return columnIndex;
    }
  }

  /**
   * Get number of columns
   *
   * @return column count
   */
  public int getColumnCount()
  {
    return columnCount;
  }

  public int getColumnType(int column) throws SFException
  {
    int internalColumnType = getInternalColumnType(column);

    int externalColumnType = internalColumnType;

    if (internalColumnType == SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_LTZ ||
        internalColumnType == SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_TZ)
    {
      externalColumnType = Types.TIMESTAMP;
    }

    logger.debug("column type = {}", externalColumnType);

    return externalColumnType;
  }

  public int getInternalColumnType(int column) throws SFException
  {
    int columnIdx = column - 1;
    if (column < 1 || column > columnTypes.size())
    {
      throw new SFException(ErrorCode.COLUMN_DOES_NOT_EXIST, column);
    }

    if (columnTypes.get(columnIdx) == null)
    {
      throw (SFException) IncidentUtil.generateIncidentV2WithException(
          session,
          new SFException(ErrorCode.INTERNAL_ERROR,
                          "Missing column type for column " + column),
          queryId,
          null);
    }

    return columnTypes.get(columnIdx);
  }

  public String getColumnTypeName(int column) throws SFException
  {
    if (column < 1 || column > columnTypeNames.size())
    {
      throw new SFException(ErrorCode.COLUMN_DOES_NOT_EXIST, column);
    }

    if (columnTypeNames.get(column - 1) == null)
    {
      throw (SFException) IncidentUtil.generateIncidentV2WithException(
          session,
          new SFException(ErrorCode.INTERNAL_ERROR,
                          "Missing column type for column " + column),
          queryId,
          null);
    }

    return columnTypeNames.get(column - 1);
  }

  public int getScale(int column)
  {
    if (scales != null && scales.size() >= column)
    {
      return scales.get(column - 1);
    }
    else
    {
      // TODO: fix this later to use different defaults for number or timestamp
      return 9;
    }
  }

  public int getPrecision(int column)
  {
    if (precisions != null && precisions.size() >= column)
    {
      return precisions.get(column - 1);
    }
    else
    {
      // TODO: fix this later to use different defaults for number or timestamp
      return 9;
    }
  }

  public boolean isSigned(int column)
  {
    return (columnTypes.get(column - 1) == Types.INTEGER ||
            columnTypes.get(column - 1) == Types.DECIMAL ||
            columnTypes.get(column - 1) == Types.BIGINT ||
            columnTypes.get(column - 1) == Types.DOUBLE);
  }

  public String getColumnLabel(int column)
  {
    if (columnNames != null)
    {
      return columnNames.get(column - 1);
    }
    else
    {
      return "C" + Integer.toString(column - 1);
    }
  }

  public String getColumnName(int column)
  {
    if (columnNames != null)
    {
      return columnNames.get(column - 1);
    }
    else
    {
      return "C" + Integer.toString(column - 1);
    }
  }

  public int isNullable(int column)
  {
    if (nullables != null)
    {
      return nullables.get(column - 1);
    }
    else
    {
      return ResultSetMetaData.columnNullableUnknown;
    }
  }

  public String getCatalogName(int column)
  {
    if (columnSrcDatabases == null)
    {
      return "";
    }
    return columnSrcDatabases.get(column - 1);
  }

  public String getSchemaName(int column)
  {
    if (columnSrcDatabases == null)
    {
      return "";
    }
    return columnSrcSchemas.get(column - 1);
  }

  public String getTableName(int column)
  {
    if (columnSrcDatabases == null)
    {
      return "T";
    }
    return columnSrcTables.get(column - 1);
  }

  public Integer getColumnDisplaySize(int column)
  {
    if (columnDisplaySizes == null)
    {
      return 25;
    }
    return columnDisplaySizes.get(column - 1);
  }
}