/*
 * 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.thrift.api.datatypes;

import com.facebook.drift.annotations.ThriftConstructor;
import com.facebook.drift.annotations.ThriftField;
import com.facebook.drift.annotations.ThriftStruct;
import com.facebook.presto.common.block.Block;
import com.facebook.presto.common.block.BlockBuilder;
import com.facebook.presto.common.type.BigintType;
import com.facebook.presto.common.type.Type;
import com.facebook.presto.spi.RecordCursor;
import com.facebook.presto.spi.RecordSet;
import io.airlift.slice.Slice;

import javax.annotation.Nullable;

import java.util.Objects;

import static com.facebook.drift.annotations.ThriftField.Requiredness.OPTIONAL;
import static com.facebook.presto.common.type.StandardTypes.ARRAY;
import static com.facebook.presto.common.type.StandardTypes.BIGINT;
import static com.facebook.presto.common.type.StandardTypes.BOOLEAN;
import static com.facebook.presto.common.type.StandardTypes.DATE;
import static com.facebook.presto.common.type.StandardTypes.DOUBLE;
import static com.facebook.presto.common.type.StandardTypes.HYPER_LOG_LOG;
import static com.facebook.presto.common.type.StandardTypes.INTEGER;
import static com.facebook.presto.common.type.StandardTypes.JSON;
import static com.facebook.presto.common.type.StandardTypes.TIMESTAMP;
import static com.facebook.presto.common.type.StandardTypes.VARCHAR;
import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Iterables.getOnlyElement;

@ThriftStruct
public final class PrestoThriftBlock
{
    // number
    private final PrestoThriftInteger integerData;
    private final PrestoThriftBigint bigintData;
    private final PrestoThriftDouble doubleData;

    // variable width
    private final PrestoThriftVarchar varcharData;

    // boolean
    private final PrestoThriftBoolean booleanData;

    // temporal
    private final PrestoThriftDate dateData;
    private final PrestoThriftTimestamp timestampData;

    // special
    private final PrestoThriftJson jsonData;
    private final PrestoThriftHyperLogLog hyperLogLogData;

    // array
    private final PrestoThriftBigintArray bigintArrayData;

    // non-thrift field which points to non-null data item
    private final PrestoThriftColumnData dataReference;

    @ThriftConstructor
    public PrestoThriftBlock(
            @Nullable PrestoThriftInteger integerData,
            @Nullable PrestoThriftBigint bigintData,
            @Nullable PrestoThriftDouble doubleData,
            @Nullable PrestoThriftVarchar varcharData,
            @Nullable PrestoThriftBoolean booleanData,
            @Nullable PrestoThriftDate dateData,
            @Nullable PrestoThriftTimestamp timestampData,
            @Nullable PrestoThriftJson jsonData,
            @Nullable PrestoThriftHyperLogLog hyperLogLogData,
            @Nullable PrestoThriftBigintArray bigintArrayData)
    {
        this.integerData = integerData;
        this.bigintData = bigintData;
        this.doubleData = doubleData;
        this.varcharData = varcharData;
        this.booleanData = booleanData;
        this.dateData = dateData;
        this.timestampData = timestampData;
        this.jsonData = jsonData;
        this.hyperLogLogData = hyperLogLogData;
        this.bigintArrayData = bigintArrayData;
        this.dataReference = theOnlyNonNull(integerData, bigintData, doubleData, varcharData, booleanData, dateData, timestampData, jsonData, hyperLogLogData, bigintArrayData);
    }

    @Nullable
    @ThriftField(value = 1, requiredness = OPTIONAL)
    public PrestoThriftInteger getIntegerData()
    {
        return integerData;
    }

    @Nullable
    @ThriftField(value = 2, requiredness = OPTIONAL)
    public PrestoThriftBigint getBigintData()
    {
        return bigintData;
    }

    @Nullable
    @ThriftField(value = 3, requiredness = OPTIONAL)
    public PrestoThriftDouble getDoubleData()
    {
        return doubleData;
    }

    @Nullable
    @ThriftField(value = 4, requiredness = OPTIONAL)
    public PrestoThriftVarchar getVarcharData()
    {
        return varcharData;
    }

    @Nullable
    @ThriftField(value = 5, requiredness = OPTIONAL)
    public PrestoThriftBoolean getBooleanData()
    {
        return booleanData;
    }

    @Nullable
    @ThriftField(value = 6, requiredness = OPTIONAL)
    public PrestoThriftDate getDateData()
    {
        return dateData;
    }

    @Nullable
    @ThriftField(value = 7, requiredness = OPTIONAL)
    public PrestoThriftTimestamp getTimestampData()
    {
        return timestampData;
    }

    @Nullable
    @ThriftField(value = 8, requiredness = OPTIONAL)
    public PrestoThriftJson getJsonData()
    {
        return jsonData;
    }

    @Nullable
    @ThriftField(value = 9, requiredness = OPTIONAL)
    public PrestoThriftHyperLogLog getHyperLogLogData()
    {
        return hyperLogLogData;
    }

    @Nullable
    @ThriftField(value = 10, requiredness = OPTIONAL)
    public PrestoThriftBigintArray getBigintArrayData()
    {
        return bigintArrayData;
    }

    public Block toBlock(Type desiredType)
    {
        return dataReference.toBlock(desiredType);
    }

    public int numberOfRecords()
    {
        return dataReference.numberOfRecords();
    }

    @Override
    public boolean equals(Object obj)
    {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        PrestoThriftBlock other = (PrestoThriftBlock) obj;
        // remaining fields are guaranteed to be null by the constructor
        return Objects.equals(this.dataReference, other.dataReference);
    }

    @Override
    public int hashCode()
    {
        return Objects.hash(integerData, bigintData, doubleData, varcharData, booleanData, dateData, timestampData, jsonData, hyperLogLogData, bigintArrayData);
    }

    @Override
    public String toString()
    {
        return toStringHelper(this)
                .add("data", dataReference)
                .toString();
    }

    public static PrestoThriftBlock integerData(PrestoThriftInteger integerData)
    {
        return new PrestoThriftBlock(integerData, null, null, null, null, null, null, null, null, null);
    }

    public static PrestoThriftBlock bigintData(PrestoThriftBigint bigintData)
    {
        return new PrestoThriftBlock(null, bigintData, null, null, null, null, null, null, null, null);
    }

    public static PrestoThriftBlock doubleData(PrestoThriftDouble doubleData)
    {
        return new PrestoThriftBlock(null, null, doubleData, null, null, null, null, null, null, null);
    }

    public static PrestoThriftBlock varcharData(PrestoThriftVarchar varcharData)
    {
        return new PrestoThriftBlock(null, null, null, varcharData, null, null, null, null, null, null);
    }

    public static PrestoThriftBlock booleanData(PrestoThriftBoolean booleanData)
    {
        return new PrestoThriftBlock(null, null, null, null, booleanData, null, null, null, null, null);
    }

    public static PrestoThriftBlock dateData(PrestoThriftDate dateData)
    {
        return new PrestoThriftBlock(null, null, null, null, null, dateData, null, null, null, null);
    }

    public static PrestoThriftBlock timestampData(PrestoThriftTimestamp timestampData)
    {
        return new PrestoThriftBlock(null, null, null, null, null, null, timestampData, null, null, null);
    }

    public static PrestoThriftBlock jsonData(PrestoThriftJson jsonData)
    {
        return new PrestoThriftBlock(null, null, null, null, null, null, null, jsonData, null, null);
    }

    public static PrestoThriftBlock hyperLogLogData(PrestoThriftHyperLogLog hyperLogLogData)
    {
        return new PrestoThriftBlock(null, null, null, null, null, null, null, null, hyperLogLogData, null);
    }

    public static PrestoThriftBlock bigintArrayData(PrestoThriftBigintArray bigintArrayData)
    {
        return new PrestoThriftBlock(null, null, null, null, null, null, null, null, null, bigintArrayData);
    }

    public static PrestoThriftBlock fromBlock(Block block, Type type)
    {
        switch (type.getTypeSignature().getBase()) {
            case INTEGER:
                return PrestoThriftInteger.fromBlock(block);
            case BIGINT:
                return PrestoThriftBigint.fromBlock(block);
            case DOUBLE:
                return PrestoThriftDouble.fromBlock(block);
            case VARCHAR:
                return PrestoThriftVarchar.fromBlock(block, type);
            case BOOLEAN:
                return PrestoThriftBoolean.fromBlock(block);
            case DATE:
                return PrestoThriftDate.fromBlock(block);
            case TIMESTAMP:
                return PrestoThriftTimestamp.fromBlock(block);
            case JSON:
                return PrestoThriftJson.fromBlock(block, type);
            case HYPER_LOG_LOG:
                return PrestoThriftHyperLogLog.fromBlock(block);
            case ARRAY:
                Type elementType = getOnlyElement(type.getTypeParameters());
                if (BigintType.BIGINT.equals(elementType)) {
                    return PrestoThriftBigintArray.fromBlock(block);
                }
                else {
                    throw new IllegalArgumentException("Unsupported array block type: " + type);
                }
            default:
                throw new IllegalArgumentException("Unsupported block type: " + type);
        }
    }

    public static PrestoThriftBlock fromRecordSetColumn(RecordSet recordSet, int columnIndex, int totalRecords)
    {
        Type type = recordSet.getColumnTypes().get(columnIndex);
        switch (type.getTypeSignature().getBase()) {
            // use more efficient implementations for numeric types which are likely to be used in index join
            case INTEGER:
                return PrestoThriftInteger.fromRecordSetColumn(recordSet, columnIndex, totalRecords);
            case BIGINT:
                return PrestoThriftBigint.fromRecordSetColumn(recordSet, columnIndex, totalRecords);
            case DATE:
                return PrestoThriftDate.fromRecordSetColumn(recordSet, columnIndex, totalRecords);
            case TIMESTAMP:
                return PrestoThriftTimestamp.fromRecordSetColumn(recordSet, columnIndex, totalRecords);
            default:
                // less efficient implementation which converts to a block first
                return fromBlock(convertColumnToBlock(recordSet, columnIndex, totalRecords), type);
        }
    }

    private static Block convertColumnToBlock(RecordSet recordSet, int columnIndex, int positions)
    {
        Type type = recordSet.getColumnTypes().get(columnIndex);
        BlockBuilder output = type.createBlockBuilder(null, positions);
        Class<?> javaType = type.getJavaType();
        RecordCursor cursor = recordSet.cursor();
        for (int position = 0; position < positions; position++) {
            checkState(cursor.advanceNextPosition(), "cursor has less values than expected");
            if (cursor.isNull(columnIndex)) {
                output.appendNull();
            }
            else {
                if (javaType == boolean.class) {
                    type.writeBoolean(output, cursor.getBoolean(columnIndex));
                }
                else if (javaType == long.class) {
                    type.writeLong(output, cursor.getLong(columnIndex));
                }
                else if (javaType == double.class) {
                    type.writeDouble(output, cursor.getDouble(columnIndex));
                }
                else if (javaType == Slice.class) {
                    Slice slice = cursor.getSlice(columnIndex);
                    type.writeSlice(output, slice, 0, slice.length());
                }
                else {
                    type.writeObject(output, cursor.getObject(columnIndex));
                }
            }
        }
        checkState(!cursor.advanceNextPosition(), "cursor has more values than expected");
        return output.build();
    }

    private static PrestoThriftColumnData theOnlyNonNull(PrestoThriftColumnData... columnsData)
    {
        PrestoThriftColumnData result = null;
        for (PrestoThriftColumnData data : columnsData) {
            if (data != null) {
                checkArgument(result == null, "more than one type is present");
                result = data;
            }
        }
        checkArgument(result != null, "no types are present");
        return result;
    }
}
