/*
 * 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.ignite.internal.client.impl;

import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.ignite.internal.client.GridClientCacheMode;
import org.apache.ignite.internal.client.GridClientNode;
import org.apache.ignite.internal.client.GridClientNodeMetrics;
import org.apache.ignite.internal.client.GridClientProtocol;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.jetbrains.annotations.Nullable;

import static org.apache.ignite.internal.IgniteNodeAttributes.ATTR_CLIENT_MODE;
import static org.apache.ignite.internal.IgniteNodeAttributes.ATTR_DAEMON;

/**
 * Client node implementation.
 */
public class GridClientNodeImpl implements GridClientNode {
    /** Node id. */
    private UUID nodeId;

    /** Consistent ID. */
    private Object consistentId;

    /** REST TCP server addresses. */
    private List<String> tcpAddrs = Collections.emptyList();

    /** REST TCP server host names. */
    private List<String> tcpHostNames = Collections.emptyList();

    /** Port for TCP rest binary protocol. */
    private int tcpPort;

    /** Node attributes. */
    private Map<String, Object> attrs = Collections.emptyMap();

    /** Node metrics. */
    private GridClientNodeMetrics metrics;

    /** Node caches. */
    private Map<String, GridClientCacheMode> caches = Collections.emptyMap();

    /** Connectable property. */
    private boolean connectable;

    /** Cache for REST TCP socket addresses. */
    private final AtomicReference<Collection<InetSocketAddress>> tcpSockAddrs = new AtomicReference<>();

    /** Node order within grid topology */
    private long order;

    /**
     * Default constructor (private).
     */
    private GridClientNodeImpl() {
        // No-op.
    }

    /**
     * Creates and returns a builder for a new instance
     * of this class.
     *
     * @return Builder for new instance.
     */
    public static Builder builder() {
        return new Builder(new GridClientNodeImpl());
    }

    /**
     * Creates and returns a builder for a new instance
     * of this class, copying data from an input instance.
     *
     * @param from Instance to copy data from.
     * @param skipAttrs Whether to skip attributes.
     * @param skipMetrics Whether to skip metrics.
     * @return Builder for new instance.
     */
    public static Builder builder(GridClientNode from, boolean skipAttrs, boolean skipMetrics) {
        Builder b = new Builder(new GridClientNodeImpl())
            .nodeId(from.nodeId())
            .consistentId(from.consistentId())
            .tcpAddresses(from.tcpAddresses())
            .tcpPort(from.tcpPort())
            .caches(from.caches())
            .connectable(from.connectable())
            .order(from.order());

        if (!skipAttrs)
            b.attributes(from.attributes());

        if (!skipMetrics)
            b.metrics(from.metrics());

        return b;
    }

    /** {@inheritDoc} */
    @Override public UUID nodeId() {
        return nodeId;
    }

    /** {@inheritDoc} */
    @Override public Object consistentId() {
        return consistentId;
    }

    /** {@inheritDoc} */
    @Override public List<String> tcpAddresses() {
        return tcpAddrs;
    }

    /** {@inheritDoc} */
    @Override public List<String> tcpHostNames() {
        return tcpHostNames;
    }

    /** {@inheritDoc} */
    @Override public int tcpPort() {
        return tcpPort;
    }

    /** {@inheritDoc} */
    @Override public Map<String, Object> attributes() {
        return Collections.unmodifiableMap(attrs);
    }

    /** {@inheritDoc} */
    @Nullable @Override public <T> T attribute(String name) {
        return (T)attrs.get(name);
    }

    /** {@inheritDoc} */
    @Override public GridClientNodeMetrics metrics() {
        return metrics;
    }

    /** {@inheritDoc} */
    @Override public Map<String, GridClientCacheMode> caches() {
        return caches;
    }

    /** {@inheritDoc} */
    @Override public Collection<InetSocketAddress> availableAddresses(GridClientProtocol proto,
        boolean filterResolved) {
        Collection<String> addrs;
        Collection<String> hostNames;
        AtomicReference<Collection<InetSocketAddress>> addrsCache;
        final int port;

        if (proto == GridClientProtocol.TCP) {
            addrsCache = tcpSockAddrs;
            addrs = tcpAddrs;
            hostNames = tcpHostNames;
            port = tcpPort;
        }
        else
            throw new AssertionError("Unknown protocol: " + proto);

        Collection<InetSocketAddress> addrs0 = addrsCache.get();

        if (addrs0 != null)
            return filterIfNecessary(addrs0, filterResolved);

        addrs0 = U.toSocketAddresses(addrs, hostNames, port);

        if (!addrsCache.compareAndSet(null, addrs0))
            return filterIfNecessary(addrsCache.get(), filterResolved);

        return filterIfNecessary(addrs0, filterResolved);
    }

    /** {@inheritDoc} */
    @Override public long order() {
        return order;
    }

    /**
     * Filters sockets with resolved addresses.
     *
     * @param addrs Addresses to filter.
     * @param filter Flag indicating whether filter should be applied or not.
     * @return Collection copy without unresolved addresses if flag is set and collection itself otherwise.
     */
    private Collection<InetSocketAddress> filterIfNecessary(Collection<InetSocketAddress> addrs, boolean filter) {
        if (!filter)
            return addrs;

        List<InetSocketAddress> res = new ArrayList<>(addrs.size());

        for (InetSocketAddress addr : addrs)
            if (!addr.isUnresolved())
                res.add(addr);

        return res;
    }

    /** {@inheritDoc} */
    @Override public boolean connectable() {
        return connectable;
    }

    /** {@inheritDoc} */
    @Override public boolean equals(Object o) {
        if (this == o) return true;

        if (!(o instanceof GridClientNodeImpl)) return false;

        GridClientNodeImpl that = (GridClientNodeImpl)o;

        return nodeId.equals(that.nodeId);
    }

    /** {@inheritDoc} */
    @Override public boolean isClient() {
        return Objects.equals(attribute(ATTR_CLIENT_MODE), true);
    }

    /** {@inheritDoc} */
    @Override public boolean isDaemon() {
        return "true".equals(attribute(ATTR_DAEMON));
    }

    /** {@inheritDoc} */
    @Override public int hashCode() {
        return nodeId.hashCode();
    }

    /** {@inheritDoc} */
    @Override public String toString() {
        return "GridClientNodeImpl [nodeId=" + nodeId +
            ", consistentId=" + consistentId +
            ", tcpAddrs=" + tcpAddrs +
            ", tcpHostNames=" + tcpHostNames +
            ", binaryPort=" + tcpPort +
            ']';
    }

    /**
     * Builder for instances of this class.
     */
    @SuppressWarnings("PublicInnerClass")
    public static final class Builder {
        /** */
        private GridClientNodeImpl impl;

        /** */
        private boolean built;

        /**
         * @param impl Implementation reference to build.
         */
        private Builder(GridClientNodeImpl impl) {
            this.impl = impl;
        }

        /**
         * Finishes instance construction and returns a
         * newly-built instance.
         *
         * @return A newly-built instance.
         */
        public GridClientNodeImpl build() {
            if (built)
                throw new AssertionError("Instance already built.");

            built = true;

            return impl;
        }

        /**
         * Sets node ID.
         *
         * @param nodeId Node ID.
         * @return This for chaining.
         */
        public Builder nodeId(UUID nodeId) {
            impl.nodeId = nodeId;

            return this;
        }

        /**
         * Sets node consistent ID.
         *
         * @param consistentId New consistent ID.
         * @return This for chaining.
         */
        public Builder consistentId(Object consistentId) {
            impl.consistentId = consistentId;

            return this;
        }

        /**
         * Sets list of REST TCP server addresses.
         *
         * @param tcpAddrs List of address strings.
         * @return This for chaining.
         */
        public Builder tcpAddresses(Collection<String> tcpAddrs) {
            impl.tcpAddrs = U.sealList(tcpAddrs);

            return this;
        }

        /**
         * Sets list of REST TCP server host names.
         *
         * @param tcpHostNames List of host names.
         * @return This for chaining.
         */
        public Builder tcpHostNames(Collection<String> tcpHostNames) {
            impl.tcpHostNames = U.sealList(tcpHostNames);

            return this;
        }

        /**
         * Sets remote TCP port value.
         *
         * @param tcpPort Sets remote port value.
         * @return This for chaining.
         */
        public Builder tcpPort(int tcpPort) {
            impl.tcpPort = tcpPort;

            return this;
        }

        /**
         * Sets node attributes.
         *
         * @param attrs Node attributes.
         * @return This for chaining.
         */
        public Builder attributes(Map<String, Object> attrs) {
            impl.attrs = U.sealMap(attrs);

            return this;
        }

        /**
         * Sets node metrics.
         *
         * @param metrics Metrics.
         * @return This for chaining.
         */
        public Builder metrics(GridClientNodeMetrics metrics) {
            impl.metrics = metrics;

            return this;
        }

        /**
         * Sets caches available on remote node.
         *
         * @param caches Cache map.
         * @return This for chaining.
         */
        public Builder caches(Map<String, GridClientCacheMode> caches) {
            impl.caches = U.sealMap(caches);

            return this;
        }

        /**
         * Sets connectable property.
         *
         * @param connectable Connectable value.
         * @return This for chaining.
         */
        public Builder connectable(boolean connectable) {
            impl.connectable = connectable;

            return this;
        }

        /**
         * Set node order within grid topology
         *
         * @param order Node order within grid topology
         */
        public Builder order(long order) {
            impl.order = order;

            return this;
        }
    }
}
