/*
 * 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.accumulo.model;

import com.facebook.presto.accumulo.Types;
import com.facebook.presto.accumulo.serializers.AccumuloRowSerializer;
import com.facebook.presto.spi.PrestoException;
import com.facebook.presto.spi.type.Type;
import com.facebook.presto.spi.type.VarcharType;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.lang.StringUtils;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;

import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT;
import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED;
import static com.facebook.presto.spi.type.BigintType.BIGINT;
import static com.facebook.presto.spi.type.BooleanType.BOOLEAN;
import static com.facebook.presto.spi.type.DateType.DATE;
import static com.facebook.presto.spi.type.DoubleType.DOUBLE;
import static com.facebook.presto.spi.type.IntegerType.INTEGER;
import static com.facebook.presto.spi.type.RealType.REAL;
import static com.facebook.presto.spi.type.SmallintType.SMALLINT;
import static com.facebook.presto.spi.type.TimeType.TIME;
import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP;
import static com.facebook.presto.spi.type.TinyintType.TINYINT;
import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY;
import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;

public class Row
{
    private static final DateTimeFormatter DATE_PARSER = ISODateTimeFormat.date();
    private static final DateTimeFormatter TIME_PARSER = DateTimeFormat.forPattern("HH:mm:ss");
    private static final DateTimeFormatter TIMESTAMP_PARSER = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS");

    private final List<Field> fields = new ArrayList<>();

    public Row() {}

    public Row(Row row)
    {
        requireNonNull(row, "row is null");
        fields.addAll(row.fields.stream().map(Field::new).collect(Collectors.toList()));
    }

    public Row addField(Field field)
    {
        requireNonNull(field, "field is null");
        fields.add(field);
        return this;
    }

    public Row addField(Object value, Type type)
    {
        requireNonNull(type, "type is null");
        fields.add(new Field(value, type));
        return this;
    }

    public Field getField(int i)
    {
        return fields.get(i);
    }

    /**
     * Gets a list of all internal fields. Any changes to this list will affect this row.
     *
     * @return List of fields
     */
    public List<Field> getFields()
    {
        return fields;
    }

    public int length()
    {
        return fields.size();
    }

    @Override
    public int hashCode()
    {
        return Arrays.hashCode(fields.toArray());
    }

    @Override
    public boolean equals(Object obj)
    {
        return obj instanceof Row && Objects.equals(this.fields, ((Row) obj).getFields());
    }

    @Override
    public String toString()
    {
        if (fields.isEmpty()) {
            return "()";
        }
        else {
            StringBuilder builder = new StringBuilder("(");
            for (Field f : fields) {
                builder.append(f).append(",");
            }
            builder.deleteCharAt(builder.length() - 1);
            return builder.append(')').toString();
        }
    }

    /**
     * Creates a new {@link Row} from the given delimited string based on the given {@link RowSchema}
     *
     * @param schema Row's schema
     * @param str String to parse
     * @param delimiter Delimiter of the string
     * @return A new Row
     * @throws PrestoException If the length of the split string is not equal to the length of the schema
     * @throws PrestoException If the schema contains an unsupported type
     */
    public static Row fromString(RowSchema schema, String str, char delimiter)
    {
        Row row = new Row();

        ImmutableList.Builder<String> builder = ImmutableList.builder();
        List<String> fields = builder.addAll(Splitter.on(delimiter).split(str)).build();

        if (fields.size() != schema.getLength()) {
            throw new PrestoException(INVALID_FUNCTION_ARGUMENT, format("Number of split tokens is not equal to schema length. Expected %s received %s. Schema: %s, fields {%s}, delimiter %s", schema.getLength(), fields.size(), schema, StringUtils.join(fields, ","), delimiter));
        }

        for (int i = 0; i < fields.size(); ++i) {
            Type type = schema.getColumn(i).getType();
            row.addField(valueFromString(fields.get(i), type), type);
        }

        return row;
    }

    /**
     * Converts the given String into a Java object based on the given Presto type
     *
     * @param str String to convert
     * @param type Presto Type
     * @return Java object
     * @throws PrestoException If the type is not supported by this function
     */
    public static Object valueFromString(String str, Type type)
    {
        if (str == null || str.isEmpty()) {
            return null;
        }
        else if (Types.isArrayType(type)) {
            Type elementType = Types.getElementType(type);
            ImmutableList.Builder<Object> listBuilder = ImmutableList.builder();
            for (String element : Splitter.on(',').split(str)) {
                listBuilder.add(valueFromString(element, elementType));
            }
            return AccumuloRowSerializer.getBlockFromArray(elementType, listBuilder.build());
        }
        else if (Types.isMapType(type)) {
            Type keyType = Types.getKeyType(type);
            Type valueType = Types.getValueType(type);
            ImmutableMap.Builder<Object, Object> mapBuilder = ImmutableMap.builder();
            for (String element : Splitter.on(',').split(str)) {
                ImmutableList.Builder<String> builder = ImmutableList.builder();
                List<String> keyValue = builder.addAll(Splitter.on("->").split(element)).build();
                checkArgument(keyValue.size() == 2, format("Map element %s has %d entries, not 2", element, keyValue.size()));

                mapBuilder.put(valueFromString(keyValue.get(0), keyType), valueFromString(keyValue.get(1), valueType));
            }
            return AccumuloRowSerializer.getBlockFromMap(type, mapBuilder.build());
        }
        else if (type.equals(BIGINT)) {
            return Long.parseLong(str);
        }
        else if (type.equals(BOOLEAN)) {
            return Boolean.parseBoolean(str);
        }
        else if (type.equals(DATE)) {
            return new Date(DATE_PARSER.parseDateTime(str).getMillis());
        }
        else if (type.equals(DOUBLE)) {
            return Double.parseDouble(str);
        }
        else if (type.equals(INTEGER)) {
            return Integer.parseInt(str);
        }
        else if (type.equals(REAL)) {
            return Float.parseFloat(str);
        }
        else if (type.equals(SMALLINT)) {
            return Short.parseShort(str);
        }
        else if (type.equals(TIME)) {
            return new Time(TIME_PARSER.parseDateTime(str).getMillis());
        }
        else if (type.equals(TIMESTAMP)) {
            return new Timestamp(TIMESTAMP_PARSER.parseDateTime(str).getMillis());
        }
        else if (type.equals(TINYINT)) {
            return Byte.valueOf(str);
        }
        else if (type.equals(VARBINARY)) {
            return str.getBytes(UTF_8);
        }
        else if (type instanceof VarcharType) {
            return str;
        }
        else {
            throw new PrestoException(NOT_SUPPORTED, "Unsupported type " + type);
        }
    }
}
