/**
 *    Copyright (C) 2015 Mesosphere, Inc.
 *
 * 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 io.mesosphere.mesos.util;

import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ListMultimap;
import io.mesosphere.mesos.frameworks.cassandra.CassandraFrameworkProtos;
import io.mesosphere.mesos.frameworks.cassandra.CassandraFrameworkProtos.*;

import org.apache.mesos.Protos;
import org.apache.mesos.Protos.Resource;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

import static com.google.common.collect.FluentIterable.from;
import static com.google.common.collect.Lists.newArrayList;
import static io.mesosphere.mesos.util.ProtoUtils.*;

public final class CassandraFrameworkProtosUtils {

    private CassandraFrameworkProtosUtils() {}

    @NotNull
    public static Function<CassandraNode, String> cassandraNodeToIp() {
        return CassandraNodeToIp.INSTANCE;
    }

    @NotNull
    public static Function<CassandraFrameworkProtos.CassandraNode, CassandraFrameworkProtos.CassandraNode.Builder> cassandraNodeToBuilder() {
        return CassandraNodeToBuilder.INSTANCE;
    }

    @NotNull
    public static Function<CassandraNode.Builder, CassandraNode> cassandraNodeBuilderToCassandraNode() {
        return CassandraNodeBuilderToCassandraNode.INSTANCE;
    }

    @NotNull
    public static Predicate<CassandraNode> cassandraNodeForTaskId(@NotNull final String taskId) {
        return new CassandraNodeTaskIdEq(taskId);
    }

    @NotNull
    public static Predicate<CassandraNode> cassandraNodeHasExecutor() {
        return new CassandraNodeHasExecutor();
    }

    @NotNull
    public static Function<CassandraNode, String> executorIdFromCassandraNode() {
        return ExecutorIdFromCassandraNode.INSTANCE;
    }

    @NotNull
    public static Predicate<CassandraNode> cassandraNodeHostnameEq(@NotNull final String hostname) {
        return new CassandraNodeHostnameEq(hostname);
    }

    @NotNull
    public static Predicate<ExecutorMetadata> executorMetadataExecutorIdEq(@NotNull final String executorId) {
        return new ExecutorMetadataExecutorIdEq(executorId);
    }

    @NotNull
    public static Predicate<CassandraNode> cassandraNodeExecutorIdEq(@NotNull final String executorId) {
        return new CassandraNodeExecutorIdEq(executorId);
    }

    @NotNull
    public static Predicate<HealthCheckHistoryEntry> healthCheckHistoryEntryExecutorIdEq(@NotNull final String executorId) {
        return new HealthCheckHistoryEntryExecutorIdEq(executorId);
    }

    @NotNull
    public static Predicate<Resource> resourceHasExpectedRole(@NotNull final String role) {
        return new ResourceHasExpectedRole(role);
    }

    @NotNull
    public static Function<HealthCheckHistoryEntry, Long> healthCheckHistoryEntryToTimestamp() {
        return HealthCheckHistoryEntryToTimestamp.INSTANCE;
    }

    @NotNull
    public static FileDownload resourceFileDownload(@NotNull final String urlForResource, final boolean extract) {
        return FileDownload.newBuilder().setDownloadUrl(urlForResource).setExtract(extract).build();
    }

    public static TaskConfig.Entry configValue(@NotNull final String name, @NotNull final Integer value) {
        return TaskConfig.Entry.newBuilder().setName(name).setLongValue(value).build();
    }

    public static TaskConfig.Entry configValue(@NotNull final String name, @NotNull final String value) {
        return TaskConfig.Entry.newBuilder().setName(name).setStringValue(value).build();
    }

    @NotNull
    public static TaskConfig.Entry configValue(@NotNull final String name, @NotNull final List<String> values) {
        return TaskConfig.Entry.newBuilder().setName(name).addAllStringValues(values).build();
    }

    public static List<String> getSeedNodeIps(@NotNull final List<CassandraNode> nodes) {
        return newArrayList(from(nodes)
            .filter(new Predicate<CassandraNode>() {
                @Override
                public boolean apply(final CassandraNode v) {
                    return v.getSeed();
                }
            })
            .transform(cassandraNodeToIp()));
    }

    public static void addTaskEnvEntry(@NotNull final TaskEnv.Builder taskEnv, final boolean replace, @NotNull final String name, @NotNull final String value) {
        for (int i = 0; i < taskEnv.getVariablesList().size(); i++) {
            final TaskEnv.Entry entry = taskEnv.getVariables(i);
            if (name.equals(entry.getName())) {
                if (replace)
                    taskEnv.setVariables(i, TaskEnv.Entry.newBuilder().setName(name).setValue(value).build());
                return;
            }
        }
        taskEnv.addVariables(TaskEnv.Entry.newBuilder().setName(name).setValue(value).build());
    }

    public static void setTaskConfig(@NotNull final TaskConfig.Builder taskConfig, @NotNull final TaskConfig.Entry entry) {
        for (int i = 0; i < taskConfig.getVariablesList().size(); i++) {
            final TaskConfig.Entry e = taskConfig.getVariables(i);
            if (entry.getName().equals(e.getName())) {
                taskConfig.setVariables(i, entry);
                return;
            }
        }
        taskConfig.addVariables(entry);
    }

    public static CassandraNodeTask getTaskForNode(@NotNull final CassandraNode cassandraNode, @NotNull final String taskId) {
        for (final CassandraNodeTask cassandraNodeTask : cassandraNode.getTasksList()) {
            if (cassandraNodeTask.getTaskId().equals(taskId))
                return cassandraNodeTask;
        }
        return null;
    }

    public static CassandraNodeTask getTaskForNode(@NotNull final CassandraNode cassandraNode, @NotNull final CassandraNodeTask.NodeTaskType taskType) {
        for (final CassandraNodeTask cassandraNodeTask : cassandraNode.getTasksList()) {
            if (cassandraNodeTask.getType() == taskType)
                return cassandraNodeTask;
        }
        return null;
    }

    public static CassandraNode.Builder removeTask(@NotNull final CassandraNode cassandraNode, @NotNull final CassandraNodeTask nodeTask) {
        final CassandraNode.Builder builder = CassandraNode.newBuilder(cassandraNode);
        for (int i = 0; i < builder.getTasksList().size(); i++) {
            final CassandraNodeTask t = builder.getTasks(i);
            if (t.getTaskId().equals(nodeTask.getTaskId())) {
                builder.removeTasks(i);
                break;
            }
        }
        return builder;
    }

    @NotNull
    public static Function<Resource, TreeSet<Long>> resourceToPortSet() {
        return ResourceToPortSet.INSTANCE;
    }

    @NotNull
    public static Predicate<Resource> containsPort(long port) {
        return new ContainsPort(port);
    }

    public static ImmutableListMultimap<String, Resource> resourcesForRoleAndOffer(@NotNull final String role, @NotNull final Protos.Offer offer) {
        return from(offer.getResourcesList())
                .filter(resourceHasExpectedRole(role))
                .index(resourceToName());
    }

    public static Predicate<Resource> scalarValueAtLeast(final long v) {
        return new ScalarValueAtLeast(v);
    }

    public static Function<Resource, Double> toDoubleResourceValue() {
        return ToDoubleResourceValue.INSTANCE;
    }

    public static Function<Resource, Long> toLongResourceValue() {
        return ToLongResourceValue.INSTANCE;
    }

    public static Optional<Double> maxResourceValueDouble(@NotNull final List<Resource> resource) {
        ImmutableList<Double> values = from(resource)
                .transform(toDoubleResourceValue())
                .toList();
        if (values.isEmpty()) {
            return Optional.absent();
        } else {
            return Optional.of(Collections.max(values));
        }
    }

    public static Optional<Long> maxResourceValueLong(@NotNull final List<Resource> resource) {
        ImmutableList<Long> values = from(resource)
                .transform(toLongResourceValue())
                .toList();
        if (values.isEmpty()) {
            return Optional.absent();
        } else {
            return Optional.of(Collections.max(values));
        }
    }

    public static String roleForPort(final long port, @NotNull final ListMultimap<String, Resource> index) {
        Optional<Resource> offeredPorts = from(index.get("ports"))
                .filter(containsPort(port))
                .first();

        if (offeredPorts.isPresent()) {
            return offeredPorts.get().getRole();
        } else {
            return "*";
        }
    }

    public static String attributeValue(@NotNull final Protos.Offer offer, @NotNull final String name) {
        for (final Protos.Attribute attribute : offer.getAttributesList()) {
            if (name.equals(attribute.getName())) {
                return attribute.hasText() ? attribute.getText().getValue() : null;
            }
        }

        return null;
    }

    public static Function<Map.Entry<String, Collection<Long>>, Resource> roleAndPortsToResource() {
        return RoleAndPortsToResource.INSTANCE;
    }

    public static Function<Long, String> byRole(@NotNull final ListMultimap<String, Resource> resourcesForRoleAndOffer) {
        return new ByRole(resourcesForRoleAndOffer);
    }

    private static final class CassandraNodeToIp implements Function<CassandraNode, String> {
        private static final CassandraNodeToIp INSTANCE = new CassandraNodeToIp();

        @Override
        public String apply(@NotNull final CassandraNode input) {
            return input.getIp();
        }
    }

    private static final class CassandraNodeToBuilder implements Function<CassandraFrameworkProtos.CassandraNode, CassandraFrameworkProtos.CassandraNode.Builder> {
        private static final CassandraNodeToBuilder INSTANCE = new CassandraNodeToBuilder();

        @Override
        public CassandraNode.Builder apply(@NotNull final CassandraNode input) {
            return CassandraNode.newBuilder(input);
        }
    }

    private static final class CassandraNodeBuilderToCassandraNode implements Function<CassandraNode.Builder, CassandraNode> {
        private static final CassandraNodeBuilderToCassandraNode INSTANCE = new CassandraNodeBuilderToCassandraNode();

        @Override
        public CassandraNode apply(@NotNull final CassandraNode.Builder input) {
            return input.build();
        }
    }

    private static final class CassandraNodeTaskIdEq implements Predicate<CassandraNode> {
        @NotNull
        private final String taskId;

        public CassandraNodeTaskIdEq(@NotNull final String taskId) {
            this.taskId = taskId;
        }

        @Override
        public boolean apply(@NotNull final CassandraNode item) {
            for (final CassandraNodeTask cassandraNodeTask : item.getTasksList()) {
                if (cassandraNodeTask.getTaskId().equals(taskId))
                    return true;
            }
            return false;
        }
    }

    private static final class ExecutorIdFromCassandraNode implements Function<CassandraNode, String> {
        private static final ExecutorIdFromCassandraNode INSTANCE = new ExecutorIdFromCassandraNode();

        @Override
        public String apply(@NotNull final CassandraNode input) {
            return input.getCassandraNodeExecutor().getExecutorId();
        }
    }

    private static final class CassandraNodeHasExecutor implements Predicate<CassandraNode> {
        @Override
        public boolean apply(@NotNull final CassandraNode item) {
            return item.hasCassandraNodeExecutor();
        }
    }

    private static final class CassandraNodeHostnameEq implements Predicate<CassandraNode> {
        @NotNull
        private final String hostname;

        public CassandraNodeHostnameEq(@NotNull final String hostname) {
            this.hostname = hostname;
        }

        @Override
        public boolean apply(@NotNull final CassandraNode item) {
            return item.hasHostname() && item.getHostname().equals(hostname);
        }
    }

    private static final class ExecutorMetadataExecutorIdEq implements Predicate<ExecutorMetadata> {
        @NotNull
        private final String executorId;

        public ExecutorMetadataExecutorIdEq(@NotNull final String executorId) {
            this.executorId = executorId;
        }

        @Override
        public boolean apply(@NotNull final ExecutorMetadata item) {
            return item.getExecutorId().equals(executorId);
        }
    }

    private static final class CassandraNodeExecutorIdEq implements Predicate<CassandraNode> {
        @NotNull
        private final String executorId;

        public CassandraNodeExecutorIdEq(@NotNull final String executorId) {
            this.executorId = executorId;
        }

        @Override
        public boolean apply(@NotNull final CassandraNode item) {
            return item.hasCassandraNodeExecutor() && item.getCassandraNodeExecutor().getExecutorId().equals(executorId);
        }
    }

    private static final class HealthCheckHistoryEntryExecutorIdEq implements Predicate<HealthCheckHistoryEntry> {
        @NotNull
        private final String executorId;

        public HealthCheckHistoryEntryExecutorIdEq(@NotNull final String executorId) {
            this.executorId = executorId;
        }

        @Override
        public boolean apply(@NotNull final HealthCheckHistoryEntry item) {
            return item.getExecutorId().equals(executorId);
        }
    }

    private static final class ResourceHasExpectedRole implements Predicate<Resource> {
        @NotNull
        private final String role;

        public ResourceHasExpectedRole(@NotNull final String role) {
            this.role = role;
        }

        @Override
        public boolean apply(final Resource item) {
            String givenRole = item.getRole();
            return givenRole.equals(role) || givenRole.equals("*");
        }
    }

    private static final class HealthCheckHistoryEntryToTimestamp implements Function<HealthCheckHistoryEntry, Long> {
        private static final HealthCheckHistoryEntryToTimestamp INSTANCE = new HealthCheckHistoryEntryToTimestamp();

        @Override
        public Long apply(@NotNull final HealthCheckHistoryEntry input) {
            return input.getTimestampEnd();
        }
    }

    private static class ResourceToPortSet implements Function<Resource, TreeSet<Long>> {
        private static final ResourceToPortSet INSTANCE = new ResourceToPortSet();

        @Override
        @NotNull
        public TreeSet<Long> apply(@Nullable final Resource resource) {
            return resourceValueRange(Optional.fromNullable(resource));
        }
    }

    private static class ContainsPort implements Predicate<Resource> {
        private final long port;

        public ContainsPort(final long port) {
            this.port = port;
        }

        @Override
        public boolean apply(@Nullable final Resource resource) {
            TreeSet<Long> portsInResource = resourceValueRange(Optional.fromNullable(resource));
            return portsInResource.contains(port);
        }
    }

    private static class ScalarValueAtLeast implements Predicate<Resource> {
        private final long v;

        public ScalarValueAtLeast(final long v) {
            this.v = v;
        }

        @Override
        public boolean apply(@NotNull final Resource resource) {
            return resource.getType() == Protos.Value.Type.SCALAR &&
                    resource.getScalar().getValue() > v;
        }
    }

    private static class ToDoubleResourceValue implements Function<Resource, Double> {
        private static final ToDoubleResourceValue INSTANCE = new ToDoubleResourceValue();

        @Override
        public Double apply(@Nullable final Resource resource) {
            return resourceValueDouble(Optional.fromNullable(resource)).or(0.0);
        }
    }

    private static class ToLongResourceValue implements Function<Resource, Long> {
        private static final ToLongResourceValue INSTANCE = new ToLongResourceValue();

        @Override
        public Long apply(@Nullable final Resource resource) {
            return resourceValueLong(Optional.fromNullable(resource)).or(0l);
        }
    }

    private static class RoleAndPortsToResource implements Function<Map.Entry<String,Collection<Long>>, Resource> {
        public static final RoleAndPortsToResource INSTANCE = new RoleAndPortsToResource();

        @Override
        public Resource apply(@NotNull final Map.Entry<String, Collection<Long>> roleAndPorts) {
            return ports(from(roleAndPorts.getValue()), roleAndPorts.getKey());
        }
    }

    private static class ByRole implements Function<Long, String> {
        private final ListMultimap<String, Resource> resourcesForRoleAndOffer;

        public ByRole(@NotNull final ListMultimap<String, Resource> resourcesForRoleAndOffer) {
            this.resourcesForRoleAndOffer = resourcesForRoleAndOffer;
        }

        @Override
        public String apply(@NotNull final Long port) {
            return roleForPort(port, resourcesForRoleAndOffer);
        }
    }
}