/*
 * Copyright 2020, TeamDev. All rights reserved.
 *
 * Redistribution and use in source and/or binary forms, with or without
 * modification, must retain the above copyright notice and the following
 * disclaimer.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package io.spine.server.storage;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.protobuf.Message;
import io.spine.server.entity.storage.ColumnMapping;
import io.spine.server.entity.storage.ColumnName;
import io.spine.server.entity.storage.DefaultColumnMapping;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.Collections;
import java.util.Map;
import java.util.Objects;

import static com.google.common.base.Preconditions.checkNotNull;
import static io.spine.util.Exceptions.newIllegalStateException;

/**
 * A value of some message record along with the values of its {@linkplain Column columns}.
 *
 * @param <I>
 *         the type of the record identifiers
 * @param <R>
 *         the type of the stored records
 */
public class RecordWithColumns<I, R extends Message> {

    private final R record;
    private final I id;

    /**
     * A map of column names to the corresponding column values.
     */
    private final ImmutableMap<ColumnName, @Nullable Object> storageFields;

    protected RecordWithColumns(I identifier, R record, Map<ColumnName, Object> storageFields) {
        this.id = checkNotNull(identifier);
        this.record = checkNotNull(record);
        this.storageFields = ImmutableMap.copyOf(storageFields);
    }

    /**
     * Creates a new record extracting the column values from the passed entity.
     */
    public static <I, R extends Message>
    RecordWithColumns<I, R> create(I identifier, R record, Columns<R> columns) {
        checkNotNull(identifier);
        checkNotNull(record);
        checkNotNull(columns);
        Map<ColumnName, @Nullable Object> storageFields = columns.valuesIn(record);
        return of(identifier, record, storageFields);
    }

    public static <I, R extends Message>
    RecordWithColumns<I, R> create(I identifier,
                                   R record,
                                   Map<ColumnName, @Nullable Object> fields) {
        checkNotNull(identifier);
        checkNotNull(record);
        checkNotNull(fields);
        return of(identifier, record, fields);
    }

    /**
     * Wraps a passed entity record.
     *
     * <p>Such instance of {@code EntityRecordWithColumns} will contain no storage fields.
     */
    public static <I, R extends Message> RecordWithColumns of(I id, R record) {
        return new RecordWithColumns<>(id, record, Collections.emptyMap());
    }

    /**
     * Creates a new instance from the passed record and storage fields.
     */
    @VisibleForTesting
    public static <I, R extends Message>
    RecordWithColumns<I, R> of(I identifier, R record, Map<ColumnName, Object> storageFields) {
        return new RecordWithColumns<>(identifier, record, storageFields);
    }

    public I id() {
        return id;
    }

    /**
     * Returns the enclosed entity record.
     */
    public R record() {
        return record;
    }

    /**
     * Obtains the names of storage fields in the record.
     *
     * @return the storage field names
     */
    public ImmutableSet<ColumnName> columnNames() {
        return ImmutableSet.copyOf(storageFields.keySet());
    }

    /**
     * Obtains the value of the storage field by the specified column name.
     *
     * <p>The {@linkplain DefaultColumnMapping default column mapping} will be used. It is suitable
     * for storages that store values "as-is" or that are willing to do the manual column value
     * conversion.
     *
     * <p>In other cases consider implementing a custom {@link ColumnMapping} and using the
     * {@link #columnValue(ColumnName, ColumnMapping)} overload for convenient column value
     * conversion.
     *
     * @param columnName
     *         the column name
     * @return the storage field value
     * @throws IllegalStateException
     *         if there is no column with the specified name
     */
    public @Nullable Object columnValue(ColumnName columnName) {
        return columnValue(columnName, DefaultColumnMapping.INSTANCE);
    }

    /**
     * Obtains the value of the storage field by the specified column name.
     *
     * <p>The specified column mapping will be used to do the column value conversion.
     */
    public <V> V columnValue(ColumnName columnName, ColumnMapping<V> columnMapping) {
        checkNotNull(columnName);
        checkNotNull(columnMapping);
        if (!storageFields.containsKey(columnName)) {
            throw newIllegalStateException("Column with the name `%s` was not found.",
                                           columnName);
        }
        Object columnValue = storageFields.get(columnName);
        if (columnValue == null) {
            V result = columnMapping.ofNull()
                                    .apply(null);
            return result;
        }
        V result = columnMapping.of(columnValue.getClass())
                                .applyTo(columnValue);
        return result;
    }

    /**
     * Tells if there are any {@linkplain Column columns} associated with this record.
     */
    public boolean hasColumns() {
        return !storageFields.isEmpty();
    }

    /**
     * Determines if there is a column with the specified name among the storage fields.
     */
    public boolean hasColumn(ColumnName name) {
        boolean result = storageFields.containsKey(name);
        return result;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof RecordWithColumns)) {
            return false;
        }
        RecordWithColumns<?, ?> columns = (RecordWithColumns<?, ?>) o;
        return Objects.equals(record, columns.record) &&
                Objects.equals(id, columns.id) &&
                Objects.equals(storageFields, columns.storageFields);
    }

    @Override
    public int hashCode() {
        return Objects.hash(record, id, storageFields);
    }
}

@@ -1100,55 +1100,8 @@
 ng;%0A
-import com.google.common.collect.ImmutableMap;%0A
 impo
@@ -1433,16 +1433,42 @@
 ctions;%0A
+import java.util.HashMap;%0A
 import j
@@ -2040,16 +2040,183 @@
 values.%0A
+     *%0A     * @implNote It's impossible to use an %7B@link com.google.common.collect.ImmutableMap%0A     *         ImmutableMap%7D, as the values may contain %7B@code null%7Ds.%0A
      */%0A
@@ -2233,25 +2233,16 @@
 e final 
-Immutable
 Map%3CColu
@@ -2501,27 +2501,21 @@
 s = 
-ImmutableMap.copyOf
+new HashMap%3C%3E
 (sto

Revert back to `HashMap` from `ImmutableMap` due to the `null`-able values.