/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.cassandra.tcm.ownership;

import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.locator.Endpoints;
import org.apache.cassandra.locator.EndpointsForRange;
import org.apache.cassandra.locator.EndpointsForToken;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.Replica;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.tcm.MetadataValue;

/**
 * Wrapper classes for EndpointsFor{Token|Range}, adding an indication of the last epoch when the placements were last modified.
 */
public interface VersionedEndpoints<E extends Endpoints<E>> extends MetadataValue<VersionedEndpoints<E>>, Supplier<E>
{
    static ForRange forRange(Epoch lastModified, EndpointsForRange endpointsForRange)
    {
        return new ForRange(lastModified, endpointsForRange);
    }

    static ForToken forToken(Epoch lastModified, EndpointsForToken endpointsForToken)
    {
        return new ForToken(lastModified, endpointsForToken);
    }

    class ForRange implements VersionedEndpoints<EndpointsForRange>
    {
        private final Epoch lastModified;
        private final EndpointsForRange endpointsForRange;

        private ForRange(Epoch lastModified, EndpointsForRange endpointsForRange)
        {
            this.lastModified = lastModified;
            this.endpointsForRange = endpointsForRange;
        }

        public ForRange withLastModified(Epoch epoch)
        {
            return new ForRange(epoch, endpointsForRange);
        }

        public Epoch lastModified()
        {
            return lastModified;
        }

        public EndpointsForRange get()
        {
            return endpointsForRange;
        }

        public int size()
        {
            return endpointsForRange.size();
        }

        public final void forEach(Consumer<? super Replica> forEach)
        {
            endpointsForRange.forEach(forEach);
        }

        public Map<InetAddressAndPort, Replica> byEndpoint()
        {
            return endpointsForRange.byEndpoint();
        }

        public Range<Token> range()
        {
            return endpointsForRange.range();
        }

        public Set<InetAddressAndPort> endpoints()
        {
            return endpointsForRange.endpoints();
        }

        public ForRange map(Function<EndpointsForRange, EndpointsForRange> fn)
        {
            return new ForRange(lastModified, fn.apply(endpointsForRange));
        }

        public ForToken forToken(Token token)
        {
            return new ForToken(lastModified, endpointsForRange.forToken(token));
        }

        public boolean equals(Object o)
        {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            ForRange forRange = (ForRange) o;
            return lastModified.equals(forRange.lastModified) &&
                   Objects.equals(endpointsForRange.sorted(Replica::compareTo),
                                  forRange.endpointsForRange.sorted(Replica::compareTo));
        }

        public boolean isEmpty()
        {
            return endpointsForRange.isEmpty();
        }

        public int hashCode()
        {
            return Objects.hash(endpointsForRange);
        }

        public String toString()
        {
            return "ForRange{" +
                   "lastModified=" + lastModified +
                   ", endpointsForRange=" + endpointsForRange +
                   '}';
        }
    }

    class ForToken implements VersionedEndpoints<EndpointsForToken>
    {
        private final Epoch lastModified;
        private final EndpointsForToken endpointsForToken;

        private ForToken(Epoch lastModified, EndpointsForToken endpointsForRange)
        {
            this.lastModified = lastModified;
            this.endpointsForToken = endpointsForRange;
        }

        public ForToken withLastModified(Epoch epoch)
        {
            return new ForToken(epoch, endpointsForToken);
        }

        public ForToken map(Function<EndpointsForToken, EndpointsForToken> fn)
        {
            return new ForToken(lastModified, fn.apply(endpointsForToken));
        }

        public ForToken without(Set<InetAddressAndPort> remove)
        {
            return map(e -> e.without(remove));
        }

        public Epoch lastModified()
        {
            return lastModified;
        }

        public EndpointsForToken get()
        {
            return endpointsForToken;
        }

        public int size()
        {
            return endpointsForToken.size();
        }

        public boolean equals(Object o)
        {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            ForToken forToken = (ForToken) o;
            return lastModified.equals(forToken.lastModified) &&
                   Objects.equals(endpointsForToken, forToken.endpointsForToken);
        }

        public boolean isEmpty()
        {
            return endpointsForToken.isEmpty();
        }

        public int hashCode()
        {
            return Objects.hash(endpointsForToken);
        }

        public String toString()
        {
            return "ForToken{" +
                   "lastModified=" + lastModified +
                   ", endpointsForToken=" + endpointsForToken +
                   '}';
        }
    }
}
