/*
 * Licensed 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 com.facebook.presto.jdbc;

import com.facebook.presto.common.type.TypeSignature;
import com.facebook.presto.common.type.TypeSignatureParameter;
import com.facebook.presto.common.type.VarcharType;
import com.google.common.collect.ImmutableList;

import java.sql.Types;
import java.util.List;

import static java.util.Objects.requireNonNull;

class ColumnInfo
{
    private static final int VARCHAR_MAX = VarcharType.UNBOUNDED_LENGTH;
    private static final int VARBINARY_MAX = 1024 * 1024 * 1024;
    private static final int TIME_ZONE_MAX = 40; // current longest time zone is 32
    private static final int TIME_MAX = "HH:mm:ss.SSS".length();
    private static final int TIME_WITH_TIME_ZONE_MAX = TIME_MAX + TIME_ZONE_MAX;
    private static final int TIMESTAMP_MAX = "yyyy-MM-dd HH:mm:ss.SSS".length();
    private static final int TIMESTAMP_WITH_TIME_ZONE_MAX = TIMESTAMP_MAX + TIME_ZONE_MAX;
    private static final int DATE_MAX = "yyyy-MM-dd".length();

    private final int columnType;
    private final List<Integer> columnParameterTypes;
    private final TypeSignature columnTypeSignature;
    private final Nullable nullable;
    private final boolean currency;
    private final boolean signed;
    private final int precision;
    private final int scale;
    private final int columnDisplaySize;
    private final String columnLabel;
    private final String columnName;
    private final String tableName;
    private final String schemaName;
    private final String catalogName;

    public enum Nullable
    {
        NO_NULLS, NULLABLE, UNKNOWN
    }

    public ColumnInfo(
            int columnType,
            List<Integer> columnParameterTypes,
            TypeSignature columnTypeSignature,
            Nullable nullable,
            boolean currency,
            boolean signed,
            int precision,
            int scale,
            int columnDisplaySize,
            String columnLabel,
            String columnName,
            String tableName,
            String schemaName,
            String catalogName)
    {
        this.columnType = columnType;
        this.columnParameterTypes = ImmutableList.copyOf(requireNonNull(columnParameterTypes, "columnParameterTypes is null"));
        this.columnTypeSignature = requireNonNull(columnTypeSignature, "columnTypeName is null");
        this.nullable = requireNonNull(nullable, "nullable is null");
        this.currency = currency;
        this.signed = signed;
        this.precision = precision;
        this.scale = scale;
        this.columnDisplaySize = columnDisplaySize;
        this.columnLabel = requireNonNull(columnLabel, "columnLabel is null");
        this.columnName = requireNonNull(columnName, "columnName is null");
        this.tableName = requireNonNull(tableName, "tableName is null");
        this.schemaName = requireNonNull(schemaName, "schemaName is null");
        this.catalogName = requireNonNull(catalogName, "catalogName is null");
    }

    public static void setTypeInfo(Builder builder, TypeSignature type)
    {
        builder.setColumnType(getType(type));
        ImmutableList.Builder<Integer> parameterTypes = ImmutableList.builder();
        for (TypeSignatureParameter parameter : type.getParameters()) {
            parameterTypes.add(getType(parameter));
        }
        builder.setColumnParameterTypes(parameterTypes.build());
        switch (type.getBase()) {
            case "boolean":
                builder.setColumnDisplaySize(5);
                break;
            case "bigint":
                builder.setSigned(true);
                builder.setPrecision(19);
                builder.setScale(0);
                builder.setColumnDisplaySize(20);
                break;
            case "integer":
                builder.setSigned(true);
                builder.setPrecision(10);
                builder.setScale(0);
                builder.setColumnDisplaySize(11);
                break;
            case "smallint":
                builder.setSigned(true);
                builder.setPrecision(5);
                builder.setScale(0);
                builder.setColumnDisplaySize(6);
                break;
            case "tinyint":
                builder.setSigned(true);
                builder.setPrecision(3);
                builder.setScale(0);
                builder.setColumnDisplaySize(4);
                break;
            case "real":
                builder.setSigned(true);
                builder.setPrecision(9);
                builder.setScale(0);
                builder.setColumnDisplaySize(16);
                break;
            case "double":
                builder.setSigned(true);
                builder.setPrecision(17);
                builder.setScale(0);
                builder.setColumnDisplaySize(24);
                break;
            case "char":
                builder.setSigned(false);
                builder.setPrecision(type.getParameters().get(0).getLongLiteral().intValue());
                builder.setColumnDisplaySize(type.getParameters().get(0).getLongLiteral().intValue());
                builder.setScale(0);
                break;
            case "varchar":
                builder.setSigned(false);
                builder.setPrecision(Math.min(type.getParameters().get(0).getLongLiteral().intValue(), VARCHAR_MAX));
                builder.setColumnDisplaySize(Math.min(type.getParameters().get(0).getLongLiteral().intValue(), VARCHAR_MAX));
                builder.setScale(0);
                break;
            case "varbinary":
                builder.setSigned(true);
                builder.setPrecision(VARBINARY_MAX);
                builder.setScale(0);
                builder.setColumnDisplaySize(VARBINARY_MAX);
                break;
            case "time":
                builder.setSigned(true);
                builder.setPrecision(3);
                builder.setScale(0);
                builder.setColumnDisplaySize(TIME_MAX);
                break;
            case "time with time zone":
                builder.setSigned(true);
                builder.setPrecision(3);
                builder.setScale(0);
                builder.setColumnDisplaySize(TIME_WITH_TIME_ZONE_MAX);
                break;
            case "timestamp":
                builder.setSigned(true);
                builder.setPrecision(3);
                builder.setScale(0);
                builder.setColumnDisplaySize(TIMESTAMP_MAX);
                break;
            case "timestamp with time zone":
                builder.setSigned(true);
                builder.setPrecision(3);
                builder.setScale(0);
                builder.setColumnDisplaySize(TIMESTAMP_WITH_TIME_ZONE_MAX);
                break;
            case "date":
                builder.setSigned(true);
                builder.setScale(0);
                builder.setColumnDisplaySize(DATE_MAX);
                break;
            case "interval year to month":
                builder.setColumnDisplaySize(TIMESTAMP_MAX);
                break;
            case "interval day to second":
                builder.setColumnDisplaySize(TIMESTAMP_MAX);
                break;
            case "decimal":
                builder.setSigned(true);
                builder.setColumnDisplaySize(type.getParameters().get(0).getLongLiteral().intValue() + 2); // dot and sign
                builder.setPrecision(type.getParameters().get(0).getLongLiteral().intValue());
                builder.setScale(type.getParameters().get(1).getLongLiteral().intValue());
                break;
        }
    }

    private static int getType(TypeSignatureParameter typeParameter)
    {
        switch (typeParameter.getKind()) {
            case TYPE:
                return getType(typeParameter.getTypeSignature());
            default:
                return Types.JAVA_OBJECT;
        }
    }

    private static int getType(TypeSignature type)
    {
        switch (type.getBase()) {
            case "array":
                return Types.ARRAY;
            case "boolean":
                return Types.BOOLEAN;
            case "bigint":
                return Types.BIGINT;
            case "integer":
                return Types.INTEGER;
            case "smallint":
                return Types.SMALLINT;
            case "tinyint":
                return Types.TINYINT;
            case "real":
                return Types.REAL;
            case "double":
                return Types.DOUBLE;
            case "varchar":
                return Types.VARCHAR;
            case "char":
                return Types.CHAR;
            case "varbinary":
                return Types.VARBINARY;
            case "time":
                return Types.TIME;
            case "time with time zone":
                return Types.TIME;
            case "timestamp":
                return Types.TIMESTAMP;
            case "timestamp with time zone":
                return Types.TIMESTAMP;
            case "date":
                return Types.DATE;
            case "decimal":
                return Types.DECIMAL;
            case "unknown":
                return Types.NULL;
            default:
                return Types.JAVA_OBJECT;
        }
    }

    public int getColumnType()
    {
        return columnType;
    }

    public List<Integer> getColumnParameterTypes()
    {
        return columnParameterTypes;
    }

    public String getColumnTypeName()
    {
        return columnTypeSignature.toString();
    }

    public TypeSignature getColumnTypeSignature()
    {
        return columnTypeSignature;
    }

    public Nullable getNullable()
    {
        return nullable;
    }

    public boolean isCurrency()
    {
        return currency;
    }

    public boolean isSigned()
    {
        return signed;
    }

    public int getPrecision()
    {
        return precision;
    }

    public int getScale()
    {
        return scale;
    }

    public int getColumnDisplaySize()
    {
        return columnDisplaySize;
    }

    public String getColumnLabel()
    {
        return columnLabel;
    }

    public String getColumnName()
    {
        return columnName;
    }

    public String getTableName()
    {
        return tableName;
    }

    public String getSchemaName()
    {
        return schemaName;
    }

    public String getCatalogName()
    {
        return catalogName;
    }

    static class Builder
    {
        private int columnType;
        private List<Integer> columnParameterTypes;
        private TypeSignature columnTypeSignature;
        private Nullable nullable;
        private boolean currency;
        private boolean signed;
        private int precision;
        private int scale;
        private int columnDisplaySize;
        private String columnLabel;
        private String columnName;
        private String tableName;
        private String schemaName;
        private String catalogName;

        public Builder setColumnType(int columnType)
        {
            this.columnType = columnType;
            return this;
        }

        public void setColumnParameterTypes(List<Integer> columnParameterTypes)
        {
            this.columnParameterTypes = ImmutableList.copyOf(requireNonNull(columnParameterTypes, "columnParameterTypes is null"));
        }

        public Builder setColumnTypeSignature(TypeSignature columnTypeSignature)
        {
            this.columnTypeSignature = columnTypeSignature;
            return this;
        }

        public Builder setNullable(Nullable nullable)
        {
            this.nullable = nullable;
            return this;
        }

        public Builder setCurrency(boolean currency)
        {
            this.currency = currency;
            return this;
        }

        public Builder setSigned(boolean signed)
        {
            this.signed = signed;
            return this;
        }

        public Builder setPrecision(int precision)
        {
            this.precision = precision;
            return this;
        }

        public Builder setScale(int scale)
        {
            this.scale = scale;
            return this;
        }

        public Builder setColumnDisplaySize(int columnDisplaySize)
        {
            this.columnDisplaySize = columnDisplaySize;
            return this;
        }

        public Builder setColumnLabel(String columnLabel)
        {
            this.columnLabel = columnLabel;
            return this;
        }

        public Builder setColumnName(String columnName)
        {
            this.columnName = columnName;
            return this;
        }

        public Builder setTableName(String tableName)
        {
            this.tableName = tableName;
            return this;
        }

        public Builder setSchemaName(String schemaName)
        {
            this.schemaName = schemaName;
            return this;
        }

        public Builder setCatalogName(String catalogName)
        {
            this.catalogName = catalogName;
            return this;
        }

        public ColumnInfo build()
        {
            return new ColumnInfo(
                    columnType,
                    columnParameterTypes,
                    columnTypeSignature,
                    nullable,
                    currency,
                    signed,
                    precision,
                    scale,
                    columnDisplaySize,
                    columnLabel,
                    columnName,
                    tableName,
                    schemaName,
                    catalogName);
        }
    }
}
