/*
 * Copyright © 2017 <code@io7m.com> http://io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package com.io7m.jtensors.generators;

import java.util.Objects;
import com.io7m.jtensors.core.unparameterized.matrices.Matrix3x3D;
import net.java.quickcheck.Generator;
import net.java.quickcheck.generator.PrimitiveGenerators;

/**
 * A matrix generator.
 */

public final class Matrix3x3DGenerator implements Generator<Matrix3x3D>
{
  private final Generator<Double> gen;

  /**
   * Construct a generator.
   *
   * @param in_gen A component generator
   */

  public Matrix3x3DGenerator(
    final Generator<Double> in_gen)
  {
    this.gen = Objects.requireNonNull(in_gen, "Generator");
  }

  /**
   * Create a generator initialized with a default component generator.
   *
   * @return A generator
   */

  public static Generator<Matrix3x3D> create()
  {
    return new Matrix3x3DGenerator(PrimitiveGenerators.doubles(
      GeneratorConstants.BOUND_LARGE_DOUBLE_LOWER,
      GeneratorConstants.BOUND_LARGE_DOUBLE_UPPER
    ));
  }

  /**
   * Create a generator initialized with a default component generator that only
   * produces values in the range {@code [-1.0, 1.0]}.
   *
   * @return A generator
   */

  public static Generator<Matrix3x3D> createNormal()
  {
    return new Matrix3x3DGenerator(PrimitiveGenerators.doubles(
      GeneratorConstants.BOUND_NORMAL_DOUBLE_LOWER,
      GeneratorConstants.BOUND_NORMAL_DOUBLE_UPPER
    ));
  }

  @Override
  public Matrix3x3D next()
  {
    return Matrix3x3D.of(
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue());
  }
}


/*
 * Copyright © 2017  http://io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package com.io7m.jtensors.storage.bytebuffered;

import com.io7m.mutable.numbers.core.MutableLongType;

import java.nio.ByteBuffer;

/**
 *
A storage vector.

 *
Storage component type: {@code float}

 *
Storage component count: {@code 4}

 */

public final class VectorByteBufferedFloating4s32
  extends TensorByteBufferedFloating32
  implements VectorByteBufferedFloating4Type
{
  private VectorByteBufferedFloating4s32(
    final ByteBuffer in_buffer,
    final MutableLongType in_base,
    final int in_offset)
  {
    super(in_buffer, in_base, in_offset);
  }

  /**
   *
Return a new vector that is backed by the given byte buffer {@code
   * b}

   *
   *
The data for the instance will be taken from the data at the current
   * value of {@code base.get() + offset}, each time a field is requested or
   * set.

   *
   *
No initialization of the data is performed.

   *
   * @param b      The byte buffer
   * @param base   The base address
   * @param offset A constant offset
   *
   * @return A new buffered vector
   */

  public static VectorByteBufferedFloating4s32 createWithBase(
    final ByteBuffer b,
    final MutableLongType base,
    final int offset)
  {
    return new VectorByteBufferedFloating4s32(b, base, offset);
  }

  @Override
  protected int componentCount()
  {
    return 4;
  }

  @Override
  public double x()
  {
    return this.getValue(0);
  }

  @Override
  public double y()
  {
    return this.getValue(1);
  }

  @Override
  public double z()
  {
    return this.getValue(2);
  }

  @Override
  public double w()
  {
    return this.getValue(3);
  }

  @Override
  public void setX(final double x)
  {
    this.putValue(0, x);
  }

  @Override
  public void setY(final double y)
  {
    this.putValue(1, y);
  }

  @Override
  public void setZ(final double z)
  {
    this.putValue(2, z);
  }

  @Override
  public void setW(final double w)
  {
    this.putValue(3, w);
  }
}


/*
 * Copyright © 2017 <code@io7m.com> http://io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package com.io7m.jtensors.generators;

import java.util.Objects;
import com.io7m.jtensors.core.parameterized.matrices.PMatrix4x4D;
import net.java.quickcheck.Generator;
import net.java.quickcheck.generator.PrimitiveGenerators;

/**
 * A matrix generator.
 *
 * @param <A> A phantom type parameter (possibly representing a source
 *            coordinate system)
 * @param <B> A phantom type parameter (possibly representing a target
 *            coordinate system)
 */

public final class PMatrix4x4DGenerator<A, B> implements Generator<PMatrix4x4D<A, B>>
{
  private final Generator<Double> gen;

  /**
   * Construct a generator.
   *
   * @param in_gen A component generator
   */

  public PMatrix4x4DGenerator(
    final Generator<Double> in_gen)
  {
    this.gen = Objects.requireNonNull(in_gen, "Generator");
  }

  /**
   * Create a generator initialized with a default component generator.
   *
   * @param <A> A phantom type parameter (possibly representing a source
   *            coordinate system)
   * @param <B> A phantom type parameter (possibly representing a target
   *            coordinate system)
   *
   * @return A generator
   */

  public static <A, B> Generator<PMatrix4x4D<A, B>> create()
  {
    return new PMatrix4x4DGenerator<>(PrimitiveGenerators.doubles(
      GeneratorConstants.BOUND_LARGE_DOUBLE_LOWER,
      GeneratorConstants.BOUND_LARGE_DOUBLE_UPPER
    ));
  }

  /**
   * Create a generator initialized with a default component generator that only
   * produces values in the range {@code [-1.0, 1.0]}.
   *
   * @param <A> A phantom type parameter (possibly representing a source
   *            coordinate system)
   * @param <B> A phantom type parameter (possibly representing a target
   *            coordinate system)
   *
   * @return A generator
   */

  public static <A, B> Generator<PMatrix4x4D<A, B>> createNormal()
  {
    return new PMatrix4x4DGenerator<>(PrimitiveGenerators.doubles(
      GeneratorConstants.BOUND_NORMAL_DOUBLE_LOWER,
      GeneratorConstants.BOUND_NORMAL_DOUBLE_UPPER
    ));
  }

  @Override
  public PMatrix4x4D<A, B> next()
  {
    return PMatrix4x4D.of(
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue(),
      this.gen.next().doubleValue());
  }
}


/*
 * Copyright © 2017  http://io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package com.io7m.jtensors.storage.bytebuffered;

import com.io7m.mutable.numbers.core.MutableLongType;

import java.nio.ByteBuffer;

/**
 *
A storage vector.

 *
Storage component type: {@code byte}

 *
Storage component count: {@code 4}

 *
 * @param <T> A phantom type parameter
 */

public final class PVectorByteBufferedIntegral4s8<T>
  extends TensorByteBufferedIntegral8
  implements PVectorByteBufferedIntegral4Type<T>
{
  private PVectorByteBufferedIntegral4s8(
    final ByteBuffer in_buffer,
    final MutableLongType in_base,
    final int in_offset)
  {
    super(in_buffer, in_base, in_offset);
  }

  /**
   *
Return a new vector that is backed by the given byte buffer {@code
   * b}

   *
   *
The data for the instance will be taken from the data at the current
   * value of {@code base.get() + offset}, each time a field is requested or
   * set.

   *
   *
No initialization of the data is performed.

   *
   * @param <T>    A phantom type parameter
   * @param b      The byte buffer
   * @param base   The base address
   * @param offset A constant offset
   *
   * @return A new buffered vector
   */

  public static <T> PVectorByteBufferedIntegral4s8<T> createWithBase(
    final ByteBuffer b,
    final MutableLongType base,
    final int offset)
  {
    return new PVectorByteBufferedIntegral4s8<>(b, base, offset);
  }

  @Override
  protected int componentCount()
  {
    return 4;
  }

  @Override
  public long x()
  {
    return this.getValue(0);
  }

  @Override
  public long y()
  {
    return this.getValue(1);
  }

  @Override
  public long z()
  {
    return this.getValue(2);
  }

  @Override
  public long w()
  {
    return this.getValue(3);
  }

  @Override
  public void setX(final long x)
  {
    this.putValue(0, x);
  }

  @Override
  public void setY(final long y)
  {
    this.putValue(1, y);
  }

  @Override
  public void setZ(final long z)
  {
    this.putValue(2, z);
  }

  @Override
  public void setW(final long w)
  {
    this.putValue(3, w);
  }
}


/*
 * Copyright 2015 NAVER Corp.
 *
 * 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 com.navercorp.redis.cluster.gateway;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.navercorp.nbasearc.gcp.GatewayConnectionPool;
import com.navercorp.redis.cluster.util.DaemonThreadFactory;

/**
 *
 * @author seongminwoo
 * @author jaehong.kim
 */
public class Gateway implements GatewayServerData {

    /**
     * The log.
     */
    private final Logger log = LoggerFactory.getLogger(Gateway.class);

    /**
     * The servers.
     */
    private Map<String, GatewayServer> servers = new ConcurrentHashMap<String, GatewayServer>();
    /**
     * id : server
     **/
    private Map<Integer, GatewayServer> index = new HashMap<Integer, GatewayServer>();

    /**
     * The config.
     */
    private GatewayConfig config;

    /**
     * The selector.
     */
    private GatewayServerSelector selector;
    private NodeWatcher nodeWatcher;
    private GatewayAffinity affinity = new GatewayAffinity();

    private final GatewayConnectionPool gcp;
    private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(
            new DaemonThreadFactory("nbase-arc-gateway-closer-", true));

    /**
     * Instantiates a new gateway.
     *
     * @param config the config
     */
    public Gateway(GatewayConfig config) {
        this.config = config;
        log.info("[Gateway] Starting " + config);

        gcp = new GatewayConnectionPool(config.getEventLoopThreadCount(), config.isHealthCheckUsed());

        List<GatewayAddress> addresses = null;
        if (config.getDomainAddress() != null) {
            // domain
            addresses = GatewayAddress.asListFromDomain(config.getDomainAddress());
            init(addresses);
        } else if (config.getIpAddress() != null) {
            // IP
            addresses = GatewayAddress.asList(config.getIpAddress());
            init(addresses);
        } else if (config.isZkUsed()) {
            // zookeeper
            try {
                nodeWatcher = new NodeWatcher(this);
                nodeWatcher.setConfig(config);
                nodeWatcher.start();
                addresses = nodeWatcher.getGatewayAddress();
                affinity = nodeWatcher.getGatewayAffinity();
            } catch (Exception e) {
                log.error("[Gateway] Failed to connect ZK. " + config.getZkAddress() + " - " + config.getClusterName(),
                        e);
            }
        }

        if (servers.size() == 0) {
            destroy();
            throw new IllegalArgumentException("not found address " + config);
        }

        this.selector = new GatewayServerSelector(config.getGatewaySelectorMethod());
    }

    public void reload(List<GatewayAddress> addresses) {
        log.info("[Gateway] Reloading {}", addresses);

        synchronized (this.servers) {
            addNewServers(addresses);
            delOutdatedServers(addresses);
            buildIndex();
        }

        log.info("[Gateway] Reloaded {}", servers);
    }

    private void addNewServers(List<GatewayAddress> addresses) {
        for (GatewayAddress address : addresses) {
            try {
                if (this.servers.containsKey(address.getName())) {
                    final GatewayServer gatewayServer = this.servers.get(address.getName());
                    if (gatewayServer.getAddress().getId() != address.getId()) {
                        gatewayServer.getAddress().setId(address.getId());
                        this.gcp.changeGwId(gatewayServer.getAddress().getId(), address.getId());
                    }
                    gatewayServer.setExist(true);
                    log.info("[Gateway] Reuse gateway server " + gatewayServer);
                } else {
                    final GatewayServer gatewayServer = new GatewayServer(address, config.getPoolConfig(),
                            config.getTimeoutMillisec(), config.getKeyspace(), gcp);
                    final int count = gatewayServer.preload(config.getClientSyncTimeUnitMillis(),
                            config.getConnectPerDelayMillis(), config.getPoolConfig());
                    this.servers.put(address.getName(), gatewayServer);
                    gatewayServer.setExist(true);
                    log.info("[Gateway] Add gateway server " + gatewayServer);

                    this.gcp.addGw(address.getId(), address.getHost(), address.getPort(),
                            config.getPhysicalConnectionCount(), config.getPhysicalConnectionReconnectInterval())
                            .get(config.getTimeoutMillisec(), TimeUnit.MILLISECONDS);
                    gatewayServer.setValid(true);
                }
            } catch (Exception ex) {
                log.error("[Gateway] Failed to reload " + address, ex);
            }
        }
    }

    private void delOutdatedServers(List<GatewayAddress> addresses) {
        final List<GatewayServer> deletedServers = new ArrayList<GatewayServer>();
        for (final GatewayServer server : servers.values()) {
            String currentGatewayName = server.getAddress().getName();
            boolean isRemoved = true;

            for (GatewayAddress address : addresses) {
                if (currentGatewayName.equals(address.getName())) {
                    isRemoved = false;
                }
            }

            if (isRemoved) {
                deletedServers.add(server);
            }
        }

        for (final GatewayServer server : deletedServers) {
            server.setExist(false);
            server.setValid(false); // set valid flag to false if gateway is removed or not valid
            executor.schedule(new Runnable() {
                @Override
                public void run() {
                    server.close();
                }
            }, 3, TimeUnit.MINUTES);

            if (this.servers.remove(server.getAddress().getName()) == null) {
                log.error("[Gateway] Not found deleted gateway server " + server + ", list=" + this.servers);
            }
            gcp.delGw(server.getAddress().getId(), server.getAddress().getHost(),
                    server.getAddress().getPort());
            log.info("[Gateway] Delete gateway server " + server);
        }
    }

    @Override
    public void reload(GatewayAffinity affinity) {
        this.affinity = affinity;
        this.gcp.updateAffinity(affinity.getTable());
    }

    /**
     * Inits the.
     *
     * @param redisGatewayList the redis gateway list
     * @param addressValidMap
     */
    private void init(List<GatewayAddress> redisGatewayList) {
        for (GatewayAddress redisGateway : redisGatewayList) {
            final GatewayServer server = new GatewayServer(redisGateway, config.getPoolConfig(),
                    config.getTimeoutMillisec(), config.getKeyspace(), gcp);
            final int count = server.preload(0, config.getConnectPerDelayMillis(), config.getPoolConfig());
            server.setValid(count > 0);

            this.servers.put(redisGateway.getName(), server);
            try {
                this.gcp.addGw(redisGateway.getId(), redisGateway.getHost(), redisGateway.getPort(),
                        config.getPhysicalConnectionCount(), config.getPhysicalConnectionReconnectInterval())
                        .get(config.getTimeoutMillisec(), TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
                log.error("[GatewayServer] Failed to connect ", e);
            } catch (ExecutionException e) {
                log.error("[GatewayServer] Failed to connect ", e);
            } catch (TimeoutException e) {
                log.error("[GatewayServer] Failed to connect ", e);
            }
        }
        buildIndex();

        log.info("[Gateway] Initialized " + servers);
    }

    private void buildIndex() {
        final Map<Integer, GatewayServer> index = new HashMap<Integer, GatewayServer>();
        for (GatewayServer server : servers.values()) {
            index.put(server.getAddress().getId(), server);
        }
        this.index = index;
    }

    public void destroy() {
        log.info("[Gateway] Destroying " + servers);

        if (nodeWatcher != null) {
            nodeWatcher.stop();
        }

        executor.shutdown();

        for (GatewayServer server : this.servers.values()) {
            server.destroy();
        }
        this.servers.clear();

        try {
            this.gcp.close().get();
        } catch (InterruptedException e) {
            throw new GatewayException("Failed to gracefully close gateway connection pool.", e);
        } catch (ExecutionException e) {
            throw new GatewayException("Failed to gracefully close gateway connection pool.", e);
        }
    }

    /**
     *
     * @return the server
     * @throws GatewayException the gateway exception
     */
    public GatewayServer getServer(final int partitionNumber, final AffinityState state) throws GatewayException {
        if (servers.size() == 0) {
            throw new GatewayException("not found gateway");
        }

        List<GatewayServer> list = new ArrayList<GatewayServer>();
        if (config.isAffinityUsed() && partitionNumber > 0 && state != null) {
            list = getAffinityServers(partitionNumber, state);
            log.debug("[Gateway] Find affinity servers {}", list);
        }

        if (!config.isAffinityUsed() || list.size() == 0) {
            // filtering.
            for (GatewayServer server : servers.values()) {
                if (server.isExist() && server.isValid()) {
                    list.add(server);
                }
            }
        }

        if (list.size() == 0) {
            // reset state.
            for (GatewayServer server : servers.values()) {
                if (server.isExist()) {
                    server.setValid(true);
                    list.add(server);
                }
            }
        }

        return selector.get(list);
    }

    /*
     * @see GatewayServerData#getServers()
     */
    public Collection<GatewayServer> getServers() {
        return servers.values();
    }

    private List<GatewayServer> getAffinityServers(final int partitionNumber, final AffinityState state) {
        final List<GatewayServer> list = new ArrayList<GatewayServer>();
        final List<Integer> ids = affinity.get(partitionNumber, state);

        if (ids.size() == 0) {
            log.debug("Not found affinity server. {partitionNumber={}, state={}}", partitionNumber, state);
        }

        for (Integer id : ids) {
            GatewayServer server = index.get(id);
            if (server == null || !server.isExist() || !server.isValid() || server.isFullConnection()) {
                if (log.isDebugEnabled()) {
                    log.debug("Not matched affinity server. {id={}, server={}}", id, server == null ? "server is null"
                            : server);
                }
                continue;
            }
            list.add(server);
        }

        return list;
    }
}


/**
 * Copyright (c) 2011 Jonathan Leibiusky
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.navercorp.redis.cluster.connection;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ExecutionException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.navercorp.nbasearc.gcp.StatusCode;
import com.navercorp.nbasearc.gcp.RequestCallback;
import com.navercorp.nbasearc.gcp.VirtualConnection;
import com.navercorp.redis.cluster.connection.RedisProtocol.Command;
import com.navercorp.redis.cluster.gateway.AffinityState;
import com.navercorp.redis.cluster.gateway.GatewayException;
import com.navercorp.redis.cluster.pipeline.BuilderFactory;
import com.navercorp.redis.cluster.util.ByteHashMap;
import com.navercorp.redis.cluster.util.RedisInputStream;
import com.navercorp.redis.cluster.util.RedisOutputStream;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.PooledByteBufAllocator;
import redis.clients.jedis.Tuple;
import redis.clients.jedis.exceptions.JedisConnectionException;
import redis.clients.jedis.exceptions.JedisDataException;
import redis.clients.util.JedisByteHashMap;
import redis.clients.util.SafeEncoder;

/**
 * The Class RedisConnection.
 *
 * @author seunghoo.han
 */
class RedisConnectionAsync implements RedisConnectionImpl {
    private final Logger log = LoggerFactory.getLogger(RedisConnectionAsync.class);
    private static final ByteBufAllocator ALLOCATOR = PooledByteBufAllocator.DEFAULT;

    private VirtualConnection vc;
    private RedisOutputStream outputStream;
    private ByteBuf outputBuf;

    /**
     * The host.
     */
    private String host;

    /**
     * The port.
     */
    private int port = RedisProtocol.DEFAULT_PORT;

    /**
     * The timeout.
     */
    private int timeout = RedisProtocol.DEFAULT_TIMEOUT;
    private int activeTimeout = RedisProtocol.DEFAULT_TIMEOUT;

    private Queue<ListenableFuture<byte[]>> pipelinedFutures = new ArrayDeque<ListenableFuture<byte[]>>();
    private long connectedTime = 0;
    private long disconnectedTime = 0;

    RedisConnectionAsync() {
        outputBuf = ALLOCATOR.buffer();
        outputStream = new RedisOutputStream(new ByteBufOutputStream(outputBuf));
    }

    RedisConnectionAsync(String host, int port) {
        this();
        this.host = host;
        this.port = port;
    }

    public void setVc(VirtualConnection vc) {
        this.vc = vc;
    }

    public void allocPc(int hash, AffinityState affinity, boolean pipelineMode) {
        vc.allocPc(hash, affinity, pipelineMode);
    }

    /**
     * Gets the timeout.
     *
     * @return the timeout
     */
    public int getTimeout() {
        return timeout;
    }

    /**
     * Sets the timeout.
     *
     * @param timeoutMillisec the new timeout
     */
    public void setTimeout(final int timeoutMillisec) {
        this.activeTimeout = this.timeout = timeoutMillisec;
    }

    public void commitActiveTimeout(final int timeoutMillisec) {
        this.activeTimeout = timeoutMillisec;
    }

    public void rollbackActiveTimeout() {
        this.activeTimeout = this.timeout;
    }

    /**
     * Send command.
     *
     * @param cmd  the cmd
     * @param args the args
     * @return the redis connection
     */
    public void sendCommand(final Command cmd, final String... args) {
        final byte[][] bargs = new byte[args.length][];
        for (int i = 0; i < args.length; i++) {
            bargs[i] = SafeEncoder.encode(args[i]);
        }
        sendCommand(cmd, bargs);
    }

    /**
     * Send command.
     *
     * @param cmd  the cmd
     * @param args the args
     * @return the redis connection
     */
    public void sendCommand(final Command cmd, final byte[]... args) {
        log.trace("[RedisConnection] Command={} {}", cmd.name(), toStringArgs(args));
        RedisProtocol.sendCommand(outputStream, cmd, args);
        try {
            outputStream.flush();
        } catch (IOException e) {
            throw new JedisConnectionException(e);
        }

        final SettableFuture<byte[]> future = SettableFuture.create();

        byte [] cmdBytes = new byte[outputBuf.readableBytes()];
        outputBuf.readBytes(cmdBytes);
        outputBuf.discardSomeReadBytes();

        vc.request(cmdBytes, activeTimeout, new RequestCallback() {
            @Override
            public void onResponse(byte[] response, StatusCode statusCode) {
                switch (statusCode) {
                case OK:
                    future.set(response);
                    break;

                case CONNECTION_ERROR:
                    future.setException(new GatewayException("Connection error"));
                    break;
                case INTERNAL_ERROR:
                    future.setException(new GatewayException("Internal error"));
                    break;
                case NO_AVAILABLE_CONNECTION:
                    future.setException(new GatewayException("no available connection"));
                    break;
                case SHUTDOWN:
                    future.setException(new GatewayException("already closed"));
                    break;
                case TIMEOUT:
                    future.setException(new GatewayException("Timeout"));
                    break;
                }
            }
        });

        pipelinedFutures.add(future);
    }

    public String toStringArgs(final byte[]... args) {
        final StringBuilder sb = new StringBuilder();
        for (byte[] arg : args) {
            if (sb.length() != 0) {
                sb.append(" ");
            }
            sb.append(SafeEncoder.encode(arg));
        }

        return sb.toString();
    }

    /**
     * Send command.
     *
     * @param cmd the cmd
     * @return the redis connection
     */
    public void sendCommand(final Command cmd) {
        log.trace("[RedisConnectionAsync] Command={}", cmd.name());
        sendCommand(cmd, new byte[0][]);
    }

    /**
     * Gets the host.
     *
     * @return the host
     */
    public String getHost() {
        return host;
    }

    /**
     * Sets the host.
     *
     * @param host the new host
     */
    public void setHost(final String host) {
        this.host = host;
    }

    /**
     * Gets the port.
     *
     * @return the port
     */
    public int getPort() {
        return port;
    }

    /**
     * Sets the port.
     *
     * @param port the new port
     */
    public void setPort(final int port) {
        this.port = port;
    }

    /**
     * Connect.
     */
    public void connect() {
        // do nothing
    }

    /**
     * Disconnect.
     */
    public void disconnect() {
        if (vc != null) {
            vc.close();
        }
        outputBuf.release();
    }

    @Override
    public void passivate() {
        vc.freePc();
    }

    /**
     * Checks if is connected.
     *
     * @return true, if is connected
     */
    public boolean isConnected() {
        return vc.isConnected();
    }

    private byte[] getResponse() {
        ListenableFuture<byte[]> f = pipelinedFutures.poll();
        try {
            return f.get();
        } catch (InterruptedException e) {
            throw new JedisConnectionException(e);
        } catch (ExecutionException e) {
            throw new JedisConnectionException(e);
        }
    }

    /**
     * Gets the status code reply.
     *
     * @return the status code reply
     */
    public String getStatusCodeReply() {
        return getStatusCodeReply(getResponse());
    }

    /**
     * Gets the bulk reply.
     *
     * @return the bulk reply
     */
    public String getBulkReply() {
        return getBulkReply(getResponse());
    }

    /**
     * Gets the binary bulk reply.
     *
     * @return the binary bulk reply
     */
    public byte[] getBinaryBulkReply() {
        return getBinaryBulkReply(getResponse());
    }

    /**
     * Gets the integer reply.
     *
     * @return the integer reply
     */
    public Long getIntegerReply() {
        return getIntegerReply(getResponse());
    }

    /**
     * Gets the multi bulk reply.
     *
     * @return the multi bulk reply
     */
    public List<String> getMultiBulkReply() {
        return BuilderFactory.STRING_LIST.build(getBinaryMultiBulkReply());
    }

    /**
     * Gets the binary multi bulk reply.
     *
     * @return the binary multi bulk reply
     */
    @SuppressWarnings("unchecked")
    public List<byte[]> getBinaryMultiBulkReply() {
        return getBinaryMultiBulkReply(getResponse());
    }

    /**
     * Gets the object multi bulk reply.
     *
     * @return the object multi bulk reply
     */
    @SuppressWarnings("unchecked")
    public List<Object> getObjectMultiBulkReply() {
        return getObjectMultiBulkReply(getResponse());
    }

    /**
     * Gets the integer multi bulk reply.
     *
     * @return the integer multi bulk reply
     */
    @SuppressWarnings("unchecked")
    public List<Long> getIntegerMultiBulkReply() {
        return getIntegerMultiBulkReply(getResponse());
    }

    public List<Object> getAll() {
        return getAll(0);
    }

    public List<Object> getAll(int except) {
        List<Object> all = new ArrayList<Object>();
        while (pipelinedFutures.isEmpty() == false) {
            try {
                all.add(RedisProtocol.read(new RedisInputStream(new ByteArrayInputStream(getResponse()))));
            } catch (JedisDataException e) {
                all.add(e);
            }
        }
        return all;
    }

    /*
     * @see java.lang.Object#toString()
     */
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append("{");
        sb.append("host=").append(host).append(", ");
        sb.append("port=").append(port).append(", ");
        sb.append("timeout=").append(timeout).append(", ");
        sb.append("connected=").append(connectedTime).append(", ");
        sb.append("disconnected=").append(disconnectedTime).append(", ");
        long elapseTime = 0;
        if (connectedTime > 0 && disconnectedTime > 0) {
            elapseTime = disconnectedTime - connectedTime;
        } else if (connectedTime > 0) {
            elapseTime = System.currentTimeMillis() - connectedTime;
        }
        sb.append("elapse=").append(elapseTime);
        sb.append("}");

        return sb.toString();
    }

    // ---------------------------------------------------------------------------------------
    // Conversion Functions
    // ---------------------------------------------------------------------------------------
    /**
     * Gets the bulk reply.
     *
     * @return the bulk reply
     */
    public static String getBulkReply(byte[] input) {
        final byte[] result = getBinaryBulkReply(input);
        if (null != result) {
            return SafeEncoder.encode(result);
        } else {
            return null;
        }
    }

    /**
     * Gets the binary bulk reply.
     *
     * @return the binary bulk reply
     */
    public static byte[] getBinaryBulkReply(byte[] input) {
        return (byte[]) RedisProtocol.read(new RedisInputStream(new ByteArrayInputStream(input)));
    }

    /**
     * Gets the integer reply.
     *
     * @return the integer reply
     */
    public static Long getIntegerReply(RedisInputStream inputStream) {
        return (Long) RedisProtocol.read(inputStream);
    }

    /**
     * Gets the binary multi bulk reply.
     *
     * @return the binary multi bulk reply
     */
    @SuppressWarnings("unchecked")
    public static List<byte[]> getBinaryMultiBulkReply(byte[] input) {
        return (List<byte[]>) RedisProtocol.read(
                new RedisInputStream(new ByteArrayInputStream(input)));
    }

    /**
     * Gets the object multi bulk reply.
     *
     * @return the object multi bulk reply
     */
    @SuppressWarnings("unchecked")
    public static List<Object> getObjectMultiBulkReply(byte[] input) {
        return (List<Object>) RedisProtocol.read(
                new RedisInputStream(new ByteArrayInputStream(input)));
    }

    /**
     * Gets the integer multi bulk reply.
     *
     * @return the integer multi bulk reply
     */
    @SuppressWarnings("unchecked")
    public static List<Long> getIntegerMultiBulkReply(byte[] input) {
        return (List<Long>) RedisProtocol.read(
                new RedisInputStream(new ByteArrayInputStream(input)));
    }

    public static ListenableFuture<String> STRING(ListenableFuture<byte[]> f) {
        return Futures.transform(f, new Function<byte[], String>() {
            @Override
            public String apply(byte[] buf) {
                return getStatusCodeReply(buf);
            }
        });
    }

    /**
     * Gets the status code reply.
     *
     * @return the status code reply
     */
    public static String getStatusCodeReply(byte[] buf) {
        final byte[] resp = (byte[]) RedisProtocol.read(new RedisInputStream(new ByteArrayInputStream(buf)));
        if (null == resp) {
            return null;
        } else {
            return SafeEncoder.encode(resp);
        }
    }

    public static ListenableFuture<byte[]> BYTE(ListenableFuture<byte[]> f) {
        return Futures.transform(f, new Function<byte[], byte[]>() {
           @Override
           public byte[] apply(byte[] buf) {
               return getBinaryBulkReply(buf);
           }
        });
    }

    public static ListenableFuture<Long> LONG(ListenableFuture<byte[]> f) {
        return Futures.transform(f, new Function<byte[], Long>() {
            @Override
            public Long apply(byte[] buf) {
                return getIntegerReply(buf);
            }
        });
    }

    /**
     * Gets the integer reply.
     *
     * @return the integer reply
     */
    public static Long getIntegerReply(byte[] buf) {
        return (Long) RedisProtocol.read(new RedisInputStream(new ByteArrayInputStream(buf)));
    }

    public static ListenableFuture<Double> DOUBLE(ListenableFuture<byte[]> f) {
        return Futures.transform(f, new Function<byte[], Double>() {
            @Override
            public Double apply(byte[] buf) {
                return getDoubleReply(buf);
            }
        });
    }

    public static Double getDoubleReply(byte[] buf) {
        String reply = getBulkReply(buf);
        return (reply != null ? new Double(reply) : null);
    }

    public static ListenableFuture<Boolean> BOOLEAN(ListenableFuture<byte[]> f) {
        return Futures.transform(f,  new Function<byte[], Boolean>() {
            @Override
            public Boolean apply(byte[] input) {
                return getIntegerReply(input) == 1;
            }
        });
    }

    public static ListenableFuture<List<byte[]>> LIST_BYTE(ListenableFuture<byte[]> f) {
        return Futures.transform(f, new Function<byte[], List<byte[]>>() {
            @Override
            public List<byte[]> apply(byte[] buf) {
                return getListByteReply(buf);
            }
        });
    }

    public static List<byte[]> getListByteReply(byte[] buf) {
        List<byte[]> values = getBinaryMultiBulkReply(buf);
        if (values == null) {
            values = new ArrayList<byte[]>();
        }
        return values;
    }

    public static ListenableFuture<List<String>> LIST_STRING(ListenableFuture<byte[]> f) {
        return Futures.transform(f, new Function<byte[], List<String>>() {
            @Override
            public List<String> apply(byte[] buf) {
                return getListStringReply(buf);
            }
        });
    }

    public static List<String> getMultiBulkReply(byte[] buf) {
        return BuilderFactory.STRING_LIST.build(getBinaryMultiBulkReply(buf));
    }

    public static List<String> getListStringReply(byte[] buf) {
        return BuilderFactory.STRING_LIST.build(getBinaryMultiBulkReply(buf));
    }

    public static ListenableFuture<Map<byte[], List<byte[]>>> MAP_BYTE_LIST_BYTE(ListenableFuture<byte[]> f) {
        return Futures.transform(f, new Function<byte[], Map<byte[], List<byte[]>>>() {
            @Override
            public Map<byte[], List<byte[]>> apply(byte[] buf) {
                return getMapByteListByteReply(buf);
            }
        });
    }

    public static Map<byte[], List<byte[]>> getMapByteListByteReply(byte []buf) {
        List<byte[]> values = getBinaryMultiBulkReply(buf);
        if (values == null) {
            values = new ArrayList<byte[]>();
        }

        final Map<byte[], List<byte[]>> result = new ByteHashMap<List<byte[]>>();
        final Iterator<byte[]> iterator = values.iterator();
        if (iterator == null) {
            return result;
        }

        while (iterator.hasNext()) {
            final byte[] name = iterator.next();
            List<byte[]> list = result.get(name);
            if (list == null) {
                list = new ArrayList<byte[]>();
            }

            if (iterator.hasNext()) {
                final byte[] value = iterator.next();
                if (value != null) {
                    list.add(value);
                }
            }
            result.put(name, list);
        }

        return result;
    }

    public static ListenableFuture<Map<String, List<String>>> MAP_STRING_LIST_STRING(ListenableFuture<byte[]> f) {
        return Futures.transform(f, new Function<byte[], Map<String, List<String>>>() {
            @Override
            public Map<String, List<String>> apply(byte[] buf) {
                return getMapStringListStringReply(buf);
            }
        });
    }

    public static Map<String, List<String>> getMapStringListStringReply(byte[] buf) {
        List<String> values = getMultiBulkReply(buf);
        if (values == null) {
            values = new ArrayList<String>();
        }
        final Map<String, List<String>> result = new HashMap<String, List<String>>();
        final Iterator<String> iterator = values.iterator();
        if (iterator == null) {
            return result;
        }

        while (iterator.hasNext()) {
            final String name = iterator.next();
            List<String> list = result.get(name);
            if (list == null) {
                list = new ArrayList<String>();
            }

            if (iterator.hasNext()) {
                final String value = iterator.next();
                if (value != null) {
                    list.add(value);
                }
            }
            result.put(name, list);
        }

        return result;
    }

    public static ListenableFuture<Map<byte[], byte[]>> MAP_BYTE_BYTE(ListenableFuture<byte[]> f) {
        return Futures.transform(f, new Function<byte[], Map<byte[], byte[]>>() {
            @Override
            public Map<byte[], byte[]> apply(byte[] input) {
                return getMapByteByteReply(input);
            }
        });
    }

    public static Map<byte[], byte[]> getMapByteByteReply(byte []buf) {
        final List<byte[]> flatHash = getBinaryMultiBulkReply(buf);
        final Map<byte[], byte[]> hash = new JedisByteHashMap();
        if (flatHash == null) {
            return hash;
        }

        final Iterator<byte[]> iterator = flatHash.iterator();
        if (iterator == null) {
            return hash;
        }

        while (iterator.hasNext()) {
            hash.put(iterator.next(), iterator.next());
        }

        return hash;
    }

    public static ListenableFuture<Map<String, String>> MAP_STRING_STRING(ListenableFuture<byte[]> f) {
        return Futures.transform(f, new Function<byte[], Map<String, String>>() {
            @Override
            public Map<String, String> apply(byte[] input) {
                return BuilderFactory.STRING_MAP.build(getBinaryMultiBulkReply(input));

            }
        });
    }

    public static ListenableFuture<Map<byte[], Set<byte[]>>> MAP_SET_BYTE(ListenableFuture<byte[]> f) {
        return Futures.transform(f, new Function<byte[], Map<byte[], Set<byte[]>>>() {
            @Override
            public Map<byte[], Set<byte[]>> apply(byte[] buf) {
                return getMapSetByteReply(buf);
            }
        });
    }

    public static Map<byte[], Set<byte[]>> getMapSetByteReply(byte []buf) {
        List<byte[]> values = getBinaryMultiBulkReply(buf);
        if (values == null) {
            values = new ArrayList<byte[]>();
        }

        final Map<byte[], Set<byte[]>> result = new ByteHashMap<Set<byte[]>>();
        final Iterator<byte[]> iterator = values.iterator();
        if (iterator == null) {
            return result;
        }

        while (iterator.hasNext()) {
            final byte[] name = iterator.next();
            Set<byte[]> set = result.get(name);
            if (set == null) {
                set = new HashSet<byte[]>();
            }
            if (iterator.hasNext()) {
                final byte[] value = iterator.next();
                if (value != null) {
                    set.add(value);
                }
            }
            result.put(name, set);
        }

        return result;
    }

    public static ListenableFuture<Map<String, Set<String>>> MAP_SET_STRING(ListenableFuture<byte[]> f) {
        return Futures.transform(f, new Function<byte[], Map<String, Set<String>>>() {
            @Override
            public Map<String, Set<String>> apply(byte[] buf) {
                return getMapSetStringReply(buf);
            }
        });
    }

    public static Map<String, Set<String>> getMapSetStringReply(byte []buf) {
        List<String> values = getMultiBulkReply(buf);
        if (values == null) {
            values = new ArrayList<String>();
        }
        final Map<String, Set<String>> result = new HashMap<String, Set<String>>();
        final Iterator<String> iterator = values.iterator();
        if (iterator == null) {
            return result;
        }

        while (iterator.hasNext()) {
            final String name = iterator.next();
            Set<String> set = result.get(name);
            if (set == null) {
                set = new HashSet<String>();
            }
            if (iterator.hasNext()) {
                final String value = iterator.next();
                if (value != null) {
                    set.add(value);
                }
            }
            result.put(name, set);
        }

        return result;
    }

    public static ListenableFuture<Set<byte[]>> SET_BYTE(ListenableFuture<byte[]> f) {
        return Futures.transform(f,  new Function<byte[], Set<byte[]>>() {

            @Override
            public Set<byte[]> apply(byte[] input) {
                return getSetByteReply(input);
            }

        });
    }

    public static Set<byte[]> getSetByteReply(byte []buf) {
        List<byte[]> values = getBinaryMultiBulkReply(buf);
        if (values == null) {
            return new HashSet<byte[]>();
        }

        return new HashSet<byte[]>(values);
    }

    public static ListenableFuture<Set<String>> SET_STRING(ListenableFuture<byte[]> f) {
        return Futures.transform(f,  new Function<byte[], Set<String>>() {
            @Override
            public Set<String> apply(byte[] input) {
                return getSetStringReply(input);
            }

        });
    }

    public static Set<String> getSetStringReply(byte []buf) {
        List<String> values = getMultiBulkReply(buf);
        if (values == null) {
            return new HashSet<String>();
        }

        return new HashSet<String>(values);
    }

    public static ListenableFuture<Set<Tuple>> SET_TUPLE(ListenableFuture<byte[]> f) {
        return Futures.transform(f, new Function<byte[], Set<Tuple>>() {
            @Override
            public Set<Tuple> apply(byte[] input) {
                return getBinaryTupledSet(input);
            }
        });
    }

    /**
     * Gets the binary tupled set.
     *
     * @return the binary tupled set
     */
    public static Set<Tuple> getBinaryTupledSet(byte[] buf) {
        List<byte[]> membersWithScores = getBinaryMultiBulkReply(buf);
        Set<Tuple> set = new LinkedHashSet<Tuple>();
        if (membersWithScores == null) {
            return set;
        }

        Iterator<byte[]> iterator = membersWithScores.iterator();
        if (iterator == null) {
            return set;
        }

        while (iterator.hasNext()) {
            set.add(new Tuple(iterator.next(), Double.valueOf(SafeEncoder.encode(iterator.next()))));
        }
        return set;
    }

    /**
     * To byte arrays.
     *
     * @param keyspace     the keyspace
     * @param uid          the uid
     * @param serviceCode  the service code
     * @param expireMillis the expire millis
     * @param keys         the keys
     * @return the byte[][]
     */
    protected byte[][] toByteArrays(final byte[] keyspace, final byte[] uid, final byte[] serviceCode,
                                  final long expireMillis, final byte[][] keys) {
        final byte[][] allArgs = new byte[keys.length + 4][];

        int index = 0;
        allArgs[index++] = keyspace;
        allArgs[index++] = uid;
        allArgs[index++] = serviceCode;
        allArgs[index++] = RedisProtocol.toByteArray(expireMillis);

        for (int i = 0; i < keys.length; i++) {
            allArgs[index++] = keys[i];
        }

        return allArgs;
    }

    /**
     * To byte arrays.
     *
     * @param keyspace     the keyspace
     * @param uid          the uid
     * @param serviceCode  the service code
     * @param key          the key
     * @param expireMillis the expire millis
     * @param values       the values
     * @return the byte[][]
     */
    protected byte[][] toByteArrays(final byte[] keyspace, final byte[] uid, final byte[] serviceCode, final byte[] key,
                                  final long expireMillis, final byte[][] values) {
        final byte[][] allArgs = new byte[values.length * 2 + 4][];

        int index = 0;
        allArgs[index++] = keyspace;
        allArgs[index++] = uid;
        allArgs[index++] = serviceCode;
        allArgs[index++] = key;
        byte[] expireTime = RedisProtocol.toByteArray(expireMillis);

        for (int i = 0; i < values.length; i++) {
            allArgs[index++] = values[i];
            allArgs[index++] = expireTime;
        }

        return allArgs;
    }

    /**
     * To byte arrays.
     *
     * @param keyspace    the keyspace
     * @param uid         the uid
     * @param serviceCode the service code
     * @param key         the key
     * @param values      the values
     * @return the byte[][]
     */
    protected byte[][] toByteArrays(final byte[] keyspace, final byte[] uid, final byte[] serviceCode, final byte[] key,
                                  final byte[][] values) {
        final byte[][] allArgs = new byte[values.length + 4][];
        int index = 0;
        allArgs[index++] = keyspace;
        allArgs[index++] = uid;
        allArgs[index++] = serviceCode;
        allArgs[index++] = key;

        for (int i = 0; i < values.length; i++) {
            allArgs[index++] = values[i];
        }
        return allArgs;
    }

    /**
     * To byte arrays.
     *
     * @param keyspace    the keyspace
     * @param uid         the uid
     * @param serviceCode the service code
     * @param key         the key
     * @return the byte[][]
     */
    protected byte[][] toByteArrays(final byte[] keyspace, final byte[] uid, final byte[] serviceCode, final byte[][] key) {
        final byte[][] allArgs = new byte[key.length + 3][];
        int index = 0;
        allArgs[index++] = keyspace;
        allArgs[index++] = uid;
        allArgs[index++] = serviceCode;

        for (int i = 0; i < key.length; i++) {
            allArgs[index++] = key[i];
        }
        return allArgs;
    }

}


/*
 * Copyright 2015 NAVER Corp.
 *
 * 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 com.navercorp.redis.cluster.spring;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * The Class DefaultBoundSessionOfHashSetOperations.
 *
 * @param <H>  the generic type
 * @param <HK> the generic type
 * @param <HV> the generic type
 * @author jaehong.kim
 */
public class DefaultBoundSessionOfHashSetOperations<H, HK, HV> implements BoundSessionOfHashSetOperations<H, HK, HV> {

    /**
     * The ops.
     */
    private final SessionOfHashSetOperations<H, HK, HV> ops;

    /**
     * The key.
     */
    private H key;

    /**
     * The field.
     */
    private HK field;

    /**
     * Instantiates a new default bound session of hash set operations.
     *
     * @param key        the key
     * @param field      the field
     * @param operations the operations
     */
    public DefaultBoundSessionOfHashSetOperations(H key, HK field, RedisClusterOperations<H, ?> operations) {
        this.key = key;
        this.field = field;
        this.ops = operations.opsForSessionOfHashSet();
    }

    /**
     * Gets the key.
     *
     * @return the key
     */
    public H getKey() {
        return this.key;
    }

    /**
     * Gets the field.
     *
     * @return the field
     */
    public HK getField() {
        return this.field;
    }

    /*
     * @see BoundSessionOfHashSetOperations#get(java.lang.Object)
     */
    public Set<HV> get(HK key) {
        return ops.get(getKey(), getField(), key);
    }

    /*
     * @see BoundSessionOfHashSetOperations#multiGet(java.util.Collection)
     */
    public Map<HK, Set<HV>> multiGet(Collection<HK> keys) {
        return ops.mulitGet(getKey(), getField(), keys);
    }

    /*
     * @see BoundSessionOfHashSetOperations#keys()
     */
    public Set<HK> keys() {
        return ops.keys(getKey(), getField());
    }

    /*
     * @see BoundSessionOfHashSetOperations#add(java.lang.Object, java.lang.Object)
     */
    public Long add(HK key, HV value) {
        return ops.add(getKey(), getField(), key, value);
    }

    /*
     * @see BoundSessionOfHashSetOperations#add(java.lang.Object, java.lang.Object, long, java.util.concurrent.TimeUnit)
     */
    public Long add(HK key, HV value, long timeout, TimeUnit unit) {
        return ops.add(getKey(), getField(), key, value, timeout, unit);
    }

    public Long addAt(HK key, HV value, long millisecondsTimestamp) {
        return ops.addAt(getKey(), getField(), key, value, millisecondsTimestamp);
    }

    /*
     * @see BoundSessionOfHashSetOperations#add(java.lang.Object, java.util.Collection)
     */
    public Long add(HK key, Collection<HV> values) {
        return ops.multiAdd(getKey(), getField(), key, values);
    }

    /*
     * @see BoundSessionOfHashSetOperations#add(java.lang.Object, java.util.Collection, long, java.util.concurrent.TimeUnit)
     */
    public Long add(HK key, Collection<HV> values, long timeout, TimeUnit unit) {
        return ops.multiAdd(getKey(), getField(), key, values, timeout, unit);
    }

    public Long addAt(HK key, Collection<HV> values, long millisecondsTimestamp) {
        return ops.multiAddAt(getKey(), getField(), key, values, millisecondsTimestamp);
    }

    /*
     * @see BoundSessionOfHashSetOperations#set(java.lang.Object, java.lang.Object)
     */
    public Long set(HK key, HV value) {
        return ops.set(getKey(), getField(), key, value);
    }

    /*
     * @see BoundSessionOfHashSetOperations#set(java.lang.Object, java.lang.Object, long, java.util.concurrent.TimeUnit)
     */
    public Long set(HK key, HV value, long timeout, TimeUnit unit) {
        return ops.set(getKey(), getField(), key, value, timeout, unit);
    }

    /*
     * @see BoundSessionOfHashSetOperations#set(java.lang.Object, java.util.Collection)
     */
    public Long set(HK key, Collection<HV> values) {
        return ops.multiSet(getKey(), getField(), key, values);
    }

    /*
     * @see BoundSessionOfHashSetOperations#set(java.lang.Object, java.util.Collection, long, java.util.concurrent.TimeUnit)
     */
    public Long set(HK key, Collection<HV> values, long timeout, TimeUnit unit) {
        return ops.multiSet(getKey(), getField(), key, values, timeout, unit);
    }

    /*
     * @see BoundSessionOfHashSetOperations#del(java.lang.Object)
     */
    public Long del(HK key) {
        return ops.del(getKey(), getField(), key);
    }

    /*
     * @see BoundSessionOfHashSetOperations#del(java.lang.Object, java.lang.Object)
     */
    public Long del(HK key, HV value) {
        return ops.del(getKey(), getField(), key, value);
    }

    /*
     * @see BoundSessionOfHashSetOperations#del(java.lang.Object, java.util.Collection)
     */
    public Long del(HK key, Collection<HV> values) {
        return ops.multiDel(getKey(), getField(), key, values);
    }

    /*
     * @see BoundSessionOfHashSetOperations#count(java.lang.Object)
     */
    public Long count(HK key) {
        return ops.count(getKey(), getField(), key);
    }

    /*
     * @see BoundSessionOfHashSetOperations#expire(java.lang.Object, long, java.util.concurrent.TimeUnit)
     */
    public Long expire(HK key, long timeout, TimeUnit unit) {
        return ops.expire(getKey(), getField(), key, timeout, unit);
    }

    /*
     * @see BoundSessionOfHashSetOperations#expire(java.lang.Object, java.lang.Object, long, java.util.concurrent.TimeUnit)
     */
    public Long expire(HK key, HV value, long timeout, TimeUnit unit) {
        return ops.expire(getKey(), getField(), key, value, timeout, unit);
    }

    /*
     * @see BoundSessionOfHashSetOperations#ttl(java.lang.Object, java.lang.Object)
     */
    public Long ttl(HK key, HV value) {
        return ops.ttl(getKey(), getField(), key, value);
    }

    public List<HV> values() {
        return ops.values(getKey(), getField());
    }
}


/*
 * Copyright 2015 NAVER Corp.
 *
 * 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 com.navercorp.redis.cluster.spring;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;

/**
 * The Class DefaultSessionOfHashListOperations.
 *
 * @param <H>  the generic type
 * @param <HK> the generic type
 * @param <HV> the generic type
 * @author jaehong.kim
 */
public class DefaultSessionOfHashListOperations<H, HK, HV> extends AbstractOperations<H, Object> implements
        SessionOfHashListOperations<H, HK, HV> {

    /**
     * Instantiates a new default session of hash list operations.
     *
     * @param template the template
     */
    @SuppressWarnings("unchecked")
    DefaultSessionOfHashListOperations(RedisClusterTemplate<H, ?> template) {
        super((RedisClusterTemplate<H, Object>) template);
    }

    /*
     * @see SessionOfHashListOperations#get(java.lang.Object, java.lang.Object, java.lang.Object)
     */
    public List<HV> get(H key, HK field, HK name) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);
        final byte[] rawElementName = rawHashKey(name);

        List<byte[]> rawElementValues = execute(new RedisCallback<List<byte[]>>() {
            public List<byte[]> doInRedis(RedisConnection connection) {
                return ((RedisClusterConnection) connection).slGet(rawKey, rawHashKey, rawElementName);
            }
        }, true);

        return deserializeHashValues(rawElementValues);
    }

    /*
     * @see SessionOfHashListOperations#multiGet(java.lang.Object, java.lang.Object, java.util.Collection)
     */
    public Map<HK, List<HV>> multiGet(H key, HK field, Collection<HK> names) {
        if (names.isEmpty()) {
            return new HashMap<HK, List<HV>>();
        }

        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);
        final byte[][] rawElementNames = new byte[names.size()][];
        int count = 0;
        for (HK name : names) {
            rawElementNames[count++] = rawHashKey(name);
        }

        Map<byte[], List<byte[]>> rawElementValues = execute(new RedisCallback<Map<byte[], List<byte[]>>>() {
            public Map<byte[], List<byte[]>> doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slMGet(rawKey, rawHashKey, rawElementNames);
            }
        }, true);

        return deserializeHashList(rawElementValues);
    }

    /*
     * @see SessionOfHashListOperations#keys(java.lang.Object)
     */
    public Set<HK> keys(H key) {
        final byte[] rawKey = rawKey(key);

        Set<byte[]> rawHashKeys = execute(new RedisCallback<Set<byte[]>>() {
            public Set<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slKeys(rawKey);
            }
        }, true);

        return deserializeHashKeys(rawHashKeys);
    }

    /*
     * @see SessionOfHashListOperations#keys(java.lang.Object, java.lang.Object)
     */
    public Set<HK> keys(H key, HK field) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);

        Set<byte[]> rawElementNames = execute(new RedisCallback<Set<byte[]>>() {
            public Set<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slKeys(rawKey, rawHashKey);
            }
        }, true);

        return deserializeHashKeys(rawElementNames);
    }

    /*
     * @see SessionOfHashListOperations#add(java.lang.Object, java.lang.Object, java.lang.Object, java.lang.Object)
     */
    public Long add(H key, HK field, HK name, HV value) {
        return add(key, field, name, value, 0, TimeUnit.SECONDS);
    }

    /*
     * @see SessionOfHashListOperations#add(java.lang.Object, java.lang.Object, java.lang.Object, java.lang.Object, long, java.util.concurrent.TimeUnit)
     */
    public Long add(H key, HK field, HK name, HV value, long timeout, TimeUnit unit) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);
        final byte[] rawElementName = rawHashKey(name);
        final byte[] rawElementValue = rawHashValue(value);
        final long expireSeconds = unit.toSeconds(timeout);

        return execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slAdd(rawKey, rawHashKey, rawElementName, expireSeconds,
                        rawElementValue);
            }
        }, true);
    }

    public Long addAt(H key, HK field, HK name, HV value, final long millisecondsTimestamp) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);
        final byte[] rawElementName = rawHashKey(name);
        final byte[] rawElementValue = rawHashValue(value);

        return execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slAddAt(rawKey, rawHashKey, rawElementName,
                        millisecondsTimestamp, rawElementValue);
            }
        }, true);
    }

    /*
     * @see SessionOfHashListOperations#multiAdd(java.lang.Object, java.lang.Object, java.lang.Object, java.util.Collection)
     */
    public Long multiAdd(H key, HK field, HK name, Collection<HV> values) {
        return multiAdd(key, field, name, values, 0, TimeUnit.SECONDS);
    }

    /*
     * @see SessionOfHashListOperations#multiAdd(java.lang.Object, java.lang.Object, java.lang.Object, java.util.Collection, long, java.util.concurrent.TimeUnit)
     */
    public Long multiAdd(H key, HK field, HK name, Collection<HV> values, long timeout, TimeUnit unit) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);
        final byte[] rawElementName = rawHashKey(name);
        final byte[][] rawElementValues = new byte[values.size()][];
        int count = 0;
        for (HV value : values) {
            rawElementValues[count++] = rawHashValue(value);
        }
        final long expireSeconds = unit.toSeconds(timeout);

        return execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slAdd(rawKey, rawHashKey, rawElementName, expireSeconds,
                        rawElementValues);
            }
        }, true);
    }

    public Long multiAddAt(H key, HK field, HK name, Collection<HV> values, final long millisecondsTimestamp) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);
        final byte[] rawElementName = rawHashKey(name);
        final byte[][] rawElementValues = new byte[values.size()][];
        int count = 0;
        for (HV value : values) {
            rawElementValues[count++] = rawHashValue(value);
        }

        return execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slAddAt(rawKey, rawHashKey, rawElementName,
                        millisecondsTimestamp, rawElementValues);
            }
        }, true);
    }

    /*
     * @see SessionOfHashListOperations#set(java.lang.Object, java.lang.Object, java.lang.Object, java.lang.Object)
     */
    public Long set(H key, HK field, HK name, HV value) {
        return set(key, field, name, value, 0, TimeUnit.SECONDS);
    }

    /*
     * @see SessionOfHashListOperations#set(java.lang.Object, java.lang.Object, java.lang.Object, java.lang.Object, long, java.util.concurrent.TimeUnit)
     */
    public Long set(H key, HK field, HK name, HV value, long timeout, TimeUnit unit) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);
        final byte[] rawElementName = rawHashKey(name);
        final byte[] rawElementValue = rawHashValue(value);
        final long expireSeconds = unit.toSeconds(timeout);

        return execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slSet(rawKey, rawHashKey, rawElementName, expireSeconds,
                        rawElementValue);
            }
        }, true);
    }

    /*
     * @see SessionOfHashListOperations#multiSet(java.lang.Object, java.lang.Object, java.lang.Object, java.util.Collection)
     */
    public Long multiSet(H key, HK field, HK name, Collection<HV> values) {
        return multiSet(key, field, name, values, 0, TimeUnit.SECONDS);
    }

    /*
     * @see SessionOfHashListOperations#multiSet(java.lang.Object, java.lang.Object, java.lang.Object, java.util.Collection, long, java.util.concurrent.TimeUnit)
     */
    public Long multiSet(H key, HK field, HK name, Collection<HV> values, long timeout, TimeUnit unit) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);
        final byte[] rawElementName = rawHashKey(name);
        final byte[][] rawElementValues = new byte[values.size()][];
        int count = 0;
        for (HV value : values) {
            rawElementValues[count++] = rawHashValue(value);
        }
        final long expireSeconds = unit.toSeconds(timeout);

        return execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slSet(rawKey, rawHashKey, rawElementName, expireSeconds,
                        rawElementValues);
            }
        }, true);
    }

    /*
     * @see SessionOfHashListOperations#del(java.lang.Object)
     */
    public Long del(H key) {
        final byte[] rawKey = rawKey(key);

        return execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slDel(rawKey);
            }
        }, true);
    }

    /*
     * @see SessionOfHashListOperations#del(java.lang.Object, java.lang.Object)
     */
    public Long del(H key, HK field) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);

        return execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slRem(rawKey, rawHashKey);
            }
        }, true);
    }

    /*
     * @see SessionOfHashListOperations#del(java.lang.Object, java.lang.Object, java.lang.Object)
     */
    public Long del(H key, HK field, HK name) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);
        final byte[] rawElementName = rawHashKey(name);

        return execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slRem(rawKey, rawHashKey, rawElementName);
            }
        }, true);
    }

    /*
     * @see SessionOfHashListOperations#del(java.lang.Object, java.lang.Object, java.lang.Object, java.lang.Object)
     */
    public Long del(H key, HK field, HK name, HV value) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);
        final byte[] rawElementName = rawHashKey(name);
        final byte[] rawElementValue = rawHashValue(value);

        return execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slRem(rawKey, rawHashKey, rawElementName, rawElementValue);
            }
        }, true);
    }

    /*
     * @see SessionOfHashListOperations#multiDel(java.lang.Object, java.lang.Object, java.lang.Object, java.util.Collection)
     */
    public Long multiDel(H key, HK field, HK name, Collection<HV> values) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);
        final byte[] rawElementName = rawHashKey(name);
        final byte[][] rawElementValues = new byte[values.size()][];
        int count = 0;
        for (HV value : values) {
            rawElementValues[count++] = rawHashValue(value);
        }

        return execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slRem(rawKey, rawHashKey, rawElementName, rawElementValues);
            }
        }, true);
    }

    /*
     * @see SessionOfHashListOperations#count(java.lang.Object, java.lang.Object, java.lang.Object)
     */
    public Long count(H key, HK field, HK name) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);
        final byte[] rawElementName = rawHashKey(name);

        return execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slCount(rawKey, rawHashKey, rawElementName);
            }
        }, true);
    }

    /*
     * @see SessionOfHashListOperations#exists(java.lang.Object, java.lang.Object, java.lang.Object, java.lang.Object)
     */
    public Boolean exists(H key, HK field, HK name, HV value) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);
        final byte[] rawElementName = rawHashKey(name);
        final byte[] rawElementValue = rawHashValue(value);

        return execute(new RedisCallback<Boolean>() {
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slExists(rawKey, rawHashKey, rawElementName,
                        rawElementValue);
            }
        }, true);
    }

    /*
     * @see SessionOfHashListOperations#expire(java.lang.Object, long, java.util.concurrent.TimeUnit)
     */
    public Long expire(H key, long timeout, TimeUnit unit) {
        final byte[] rawKey = rawKey(key);
        final long expireSeconds = unit.toSeconds(timeout);

        return execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slExpire(rawKey, expireSeconds);
            }
        }, true);
    }

    /*
     * @see SessionOfHashListOperations#expire(java.lang.Object, java.lang.Object, long, java.util.concurrent.TimeUnit)
     */
    public Long expire(H key, HK field, long timeout, TimeUnit unit) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);
        final long expireSeconds = unit.toSeconds(timeout);

        return execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slExpire(rawKey, rawHashKey, expireSeconds);
            }
        }, true);
    }

    /*
     * @see SessionOfHashListOperations#expire(java.lang.Object, java.lang.Object, java.lang.Object, long, java.util.concurrent.TimeUnit)
     */
    public Long expire(H key, HK field, HK name, long timeout, TimeUnit unit) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);
        final byte[] rawElementName = rawHashKey(name);
        final long expireSeconds = unit.toSeconds(timeout);

        return execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slExpire(rawKey, rawHashKey, rawElementName, expireSeconds);
            }
        }, true);
    }

    /*
     * @see SessionOfHashListOperations#expire(java.lang.Object, java.lang.Object, java.lang.Object, java.lang.Object, long, java.util.concurrent.TimeUnit)
     */
    public Long expire(H key, HK field, HK name, HV value, long timeout, TimeUnit unit) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);
        final byte[] rawElementName = rawHashKey(name);
        final byte[] rawElementValue = rawHashValue(value);
        final long expireSeconds = unit.toSeconds(timeout);

        return execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slExpire(rawKey, rawHashKey, rawElementName,
                        rawElementValue, expireSeconds);
            }
        }, true);
    }

    /*
     * @see SessionOfHashListOperations#ttl(java.lang.Object, java.lang.Object, java.lang.Object, java.lang.Object)
     */
    public Long ttl(H key, HK field, HK name, HV value) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);
        final byte[] rawElementName = rawHashKey(name);
        final byte[] rawElementValue = rawHashValue(value);

        return execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slTTL(rawKey, rawHashKey, rawElementName, rawElementValue);
            }
        }, true);
    }

    public List<HV> values(H key, HK field) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(field);

        List<byte[]> rawElementValues = execute(new RedisCallback<List<byte[]>>() {
            public List<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
                return ((RedisClusterConnection) connection).slVals(rawKey, rawHashKey);
            }
        }, true);

        return deserializeHashValues(rawElementValues);
    }
}


/*
 * Copyright 2015 NAVER Corp.
 *
 * 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 com.navercorp.redis.cluster.gateway;

import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * The Class GatewayServerSelector.
 *
 * @author jaehong.kim
 */
public class GatewayServerSelector {

    /**
     * The Constant METHOD_RANDOM.
     */
    public static final String METHOD_RANDOM = "random";

    /**
     * The Constant METHOD_ROUND_ROBIN.
     */
    public static final String METHOD_ROUND_ROBIN = "round-robin";

    /**
     * The Constant MAX_SEQ.
     */
    private static final int MAX_SEQ = Integer.MAX_VALUE;

    /**
     * The Enum METHOD.
     */
    private enum METHOD {

        /**
         * The RANDOM.
         */
        RANDOM,
        /**
         * The ROUN d_ robin.
         */
        ROUND_ROBIN
    }

    /**
     * The method.
     */
    private final METHOD method;

    /**
     * The seq.
     */
    private final AtomicInteger seq = new AtomicInteger(0);

    /**
     * Instantiates a new gateway server selector.
     *
     * @param method the method
     */
    public GatewayServerSelector(String method) {
        if (method != null && METHOD_RANDOM.equals(method.toLowerCase())) {
            this.method = METHOD.RANDOM;
        } else {
            // default
            this.method = METHOD.ROUND_ROBIN;
        }
    }

    /**
     * Gets the.
     *
     * @param list the list
     * @return the gateway server
     */
    public GatewayServer get(List<GatewayServer> list) {
        if (list.size() == 0) {
            throw new IllegalArgumentException("list must not be zero size");
        }

        if (list.size() == 1) {
            return list.get(0);
        }

        if (this.method == METHOD.RANDOM) {
            return random(list);
        }

        return roundrobin(list);
    }

    /**
     * Random.
     *
     * @param list the list
     * @return the gateway server
     */
    GatewayServer random(List<GatewayServer> list) {
        return list.get(new Random().nextInt(list.size()));
    }

    /**
     * Roundrobin.
     *
     * @param list the list
     * @return the gateway server
     */
    GatewayServer roundrobin(List<GatewayServer> list) {
        int index = this.seq.getAndIncrement();
        if(index < 0) {
            this.seq.set(0);
            index = 0;
        }

        final int size = list.size();
        if (index != 0 && index >= size) {
            index = index % size;
        }

        return list.get(index);
    }
}