package com.thinkbiganalytics.feedmgr.service;

/*-
 * #%L
 * thinkbig-feed-manager-controller
 * %%
 * Copyright (C) 2017 ThinkBig Analytics
 * %%
 * 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.
 * #L%
 */

import com.google.common.base.MoreObjects;
import com.google.common.collect.Maps;
import com.thinkbiganalytics.feedmgr.rest.model.UserField;
import com.thinkbiganalytics.feedmgr.rest.model.UserProperty;
import com.thinkbiganalytics.metadata.api.extension.UserFieldDescriptor;

import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
 * Transforms user-defined properties between Feed Manager and Metadata formats.
 */
public final class UserPropertyTransform {

    /**
     * Instances of {@code UserPropertyTransform} should not be constructed.
     *
     * @throws UnsupportedOperationException always
     */
    private UserPropertyTransform() {
        throw new UnsupportedOperationException();
    }

    /**
     * Transforms the specified Metadata user-defined properties to Feed Manager user-defined properties.
     *
     * @param properties map of property names to values
     * @param userFields the user-defined field descriptors
     * @return the set of Feed Manager user-defined properties
     */
    @Nonnull
    @SafeVarargs
    public static Set<UserProperty> toUserProperties(@Nonnull final Map<String, String> properties, @Nonnull final Set<UserFieldDescriptor>... userFields) {
        // Map field names and order
        int order = 0;
        int size = Arrays.stream(userFields).collect(Collectors.summingInt(Set::size));
        final Map<String, UserFieldDescriptor> userFieldMap = Maps.newHashMapWithExpectedSize(size);
        final Map<String, Integer> userFieldOrder = Maps.newHashMapWithExpectedSize(size);

        for (final Set<UserFieldDescriptor> set : userFields) {
            final Iterator<UserFieldDescriptor> sorted = set.stream().sorted((f1, f2) -> Integer.compare(f1.getOrder(), f2.getOrder())).iterator();
            while (sorted.hasNext()) {
                final UserFieldDescriptor field = sorted.next();
                if (!userFieldMap.containsKey(field.getSystemName())) {
                    userFieldMap.put(field.getSystemName(), field);
                    userFieldOrder.put(field.getSystemName(), order++);
                }
            }
        }

        // Convert from Metadata to Feed Manager format
        final Stream<UserProperty> newProperties = userFieldMap.values().stream()
            .filter(field -> !properties.containsKey(field.getSystemName()))
            .map(field -> {
                final UserProperty property = new UserProperty();
                property.setDescription(field.getDescription());
                property.setDisplayName(field.getDisplayName());
                property.setLocked(true);
                property.setOrder(userFieldOrder.get(field.getSystemName()));
                property.setRequired(field.isRequired());
                property.setSystemName(field.getSystemName());
                return property;
            });

        final Stream<UserProperty> existingProperties = properties.entrySet().stream()
            .map(entry -> {
                // Create the Feed Manager property
                final UserProperty property = new UserProperty();
                property.setLocked(false);
                property.setSystemName(entry.getKey());
                property.setValue(entry.getValue());

                // Set additional Metadata attributes
                final UserFieldDescriptor field = userFieldMap.get(entry.getKey());
                if (field != null) {
                    property.setDescription(field.getDescription());
                    property.setDisplayName(field.getDisplayName());
                    property.setLocked(true);
                    property.setOrder(userFieldOrder.get(entry.getKey()));
                    property.setRequired(field.isRequired());
                }

                // Return the Feed Manager property
                return property;
            });

        return Stream.concat(newProperties, existingProperties).collect(Collectors.toCollection(LinkedHashSet::new));
    }

    /**
     * Transforms the specified Feed Manager user-defined properties to Metadata user-defined properties.
     *
     * @param userProperties the Feed Manager user-defined properties
     * @return a map of property names to values
     */
    @Nonnull
    public static Map<String, String> toMetadataProperties(@Nonnull final Set<UserProperty> userProperties) {
        return userProperties.stream()
            .filter(property -> property.getValue() != null)
            .collect(Collectors.toMap(UserProperty::getSystemName, UserProperty::getValue));
    }

    /**
     * Transforms the specified {@link UserField} objects into {@link UserFieldDescriptor} objects.
     *
     * @param userFields the user-defined fields
     * @return equivalent user-defined field descriptors
     */
    @Nonnull
    public static Set<UserFieldDescriptor> toUserFieldDescriptors(@Nonnull final Set<UserField> userFields) {
        return userFields.stream().map(FeedManagerUserFieldDescriptor::new).collect(Collectors.toSet());
    }

    /**
     * Transforms the specified {@link UserFieldDescriptor} objects into {@link UserField} objects.
     *
     * @param descriptors the user-defined field descriptors
     * @return equivalent user-defined fields
     */
    @Nonnull
    public static Set<UserField> toUserFields(@Nonnull final Set<UserFieldDescriptor> descriptors) {
        return descriptors.stream().map(descriptor -> {
            final UserField field = new UserField();
            field.setDescription(descriptor.getDescription());
            field.setDisplayName(descriptor.getDisplayName());
            field.setOrder(descriptor.getOrder());
            field.setRequired(descriptor.isRequired());
            field.setSystemName(descriptor.getSystemName());
            return field;
        }).collect(Collectors.toSet());
    }

    /**
     * A {@link UserFieldDescriptor} backed by a {@link UserField}.
     */
    private static class FeedManagerUserFieldDescriptor implements UserFieldDescriptor {

        /**
         * Delegate user-defined field
         */
        @Nonnull
        private final UserField userField;

        /**
         * Constructs a {@code FeedManagerUserFieldDescriptor} that delegates to the specified user-defined field.
         *
         * @param userField the user-defined field to delegate to
         */
        FeedManagerUserFieldDescriptor(@Nonnull final UserField userField) {
            this.userField = userField;
        }

        @Nullable
        @Override
        public String getDescription() {
            return userField.getDescription();
        }

        @Nullable
        @Override
        public String getDisplayName() {
            return userField.getDisplayName();
        }

        @Override
        public int getOrder() {
            return (userField.getOrder() != null) ? userField.getOrder() : 0;
        }

        @Override
        public boolean isRequired() {
            return (userField.isRequired() != null) ? userField.isRequired() : false;
        }

        @Nonnull
        @Override
        public String getSystemName() {
            return userField.getSystemName();
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(getSystemName());
        }

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(UserFieldDescriptor.class)
                .add("name", getSystemName())
                .toString();
        }
    }
}
