/**
 * Copyright 2015 Red Hat, Inc.
 *
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  and Apache License v2.0 which accompanies this distribution.
 *
 *  The Eclipse Public License is available at
 *  http://www.eclipse.org/legal/epl-v10.html
 *
 *  The Apache License v2.0 is available at
 *  http://www.opensource.org/licenses/apache2.0.php
 *
 *  You may elect to redistribute this code under either of these licenses.
 */
package io.vertx.redis.impl;

import io.vertx.core.*;
import io.vertx.core.eventbus.EventBus;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.redis.RedisClient;
import io.vertx.redis.RedisOptions;

import java.nio.charset.Charset;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public abstract class AbstractRedisClient implements RedisClient {

  private final EventBus eb;
  private final RedisSubscriptions subscriptions;
  private final String encoding;
  private final Charset charset;
  private final Charset binaryCharset;
  private final String baseAddress;

  // we need 2 connections, one for normal commands and a second in case we do pub/sub
  private final RedisConnection redis;
  private final RedisConnection pubsub;

  AbstractRedisClient(Vertx vertx, RedisOptions config) {
    this.eb = vertx.eventBus();
    this.encoding = config.getEncoding();
    this.charset = Charset.forName(encoding);
    this.binaryCharset = Charset.forName("iso-8859-1");
    this.baseAddress = config.getAddress();

    subscriptions = new RedisSubscriptions(vertx);

    redis = new RedisConnection(vertx, config, null);
    pubsub = new RedisConnection(vertx, config, subscriptions);
  }

  @Override
  public synchronized void close(Handler<AsyncResult<Void>> handler) {
    // this is a special case it should sent the message QUIT and then close the sockets
    final AtomicInteger cnt = new AtomicInteger(0);

    final Handler<AsyncResult<Void>> cb = v -> {
      if (cnt.incrementAndGet() == 2) {
        handler.handle(Future.succeededFuture());
      }
    };

    redis.disconnect(cb);
    pubsub.disconnect(cb);
  }

  private ResponseTransform getResponseTransformFor(RedisCommand command) {
    if (command == RedisCommand.HGETALL) {
      return ResponseTransform.HASH;
    }
    if (command == RedisCommand.INFO) {
      return ResponseTransform.INFO;
    }

    if (command == RedisCommand.EVAL || command == RedisCommand.EVALSHA) {
      return ResponseTransform.ARRAY;
    }

    return ResponseTransform.NONE;
  }

  final void sendString(final RedisCommand command, final List<?> args, final Handler<AsyncResult<String>> resultHandler) {
    send(command, args, String.class, false, resultHandler);
  }

  final void sendLong(final RedisCommand command, final List<?> args, final Handler<AsyncResult<Long>> resultHandler) {
    send(command, args, Long.class, false, resultHandler);
  }

  final void sendVoid(final RedisCommand command, final List<?> args, final Handler<AsyncResult<Void>> resultHandler) {
    send(command, args, Void.class, false, resultHandler);
  }

  final void sendJsonArray(final RedisCommand command, final List<?> args, final Handler<AsyncResult<JsonArray>> resultHandler) {
    send(command, args, JsonArray.class, false, resultHandler);
  }

  final void sendJsonObject(final RedisCommand command, final List<?> args, final Handler<AsyncResult<JsonObject>> resultHandler) {
    send(command, args, JsonObject.class, false, resultHandler);
  }

  final <T> void send(final RedisCommand command, final List<?> redisArgs, final Class<T> returnType,
                      final boolean binary,
                      final Handler<AsyncResult<T>> resultHandler) {

    final Command<T> cmd = new Command<>(Vertx.currentContext(), command, redisArgs, binary ? binaryCharset : charset, getResponseTransformFor(command), returnType).handler(resultHandler);

    switch (command) {
      case PSUBSCRIBE:
        cmd.setExpectedReplies(redisArgs.size());

        for (Object obj : redisArgs) {
          String pattern = (String) obj;
          // compose the listening address as base + . + pattern
          final String vertxChannel = baseAddress + "." + pattern;
          subscriptions.registerPatternSubscribeHandler(pattern, (pattern1, replyData) -> {
            JsonObject replyMessage = new JsonObject();
            replyMessage.put("status", "ok");
            JsonObject message = new JsonObject();
            message.put("pattern", pattern1);
            message.put("channel", replyData[2].asType(String.class, encoding));
            message.put("message", replyData[3].asType(String.class, encoding));
            replyMessage.put("value", message);
            eb.send(vertxChannel, replyMessage);
          });
        }
        pubsub.send(cmd);
        break;

      case SUBSCRIBE:
        cmd.setExpectedReplies(redisArgs.size());

        for (Object obj : redisArgs) {
          String channel = (String) obj;
          // compose the listening address as base + . + channel
          final String vertxChannel = baseAddress + "." + channel;
          subscriptions.registerChannelSubscribeHandler(channel, (channel1, replyData) -> {
            JsonObject replyMessage = new JsonObject();
            replyMessage.put("status", "ok");
            JsonObject message = new JsonObject();
            message.put("channel", channel1);
            message.put("message", replyData[2].asType(String.class, encoding));
            replyMessage.put("value", message);
            eb.send(vertxChannel, replyMessage);
          });
        }
        pubsub.send(cmd);
        break;

      case PUNSUBSCRIBE:
        // unregister all channels
        if (redisArgs == null || redisArgs.size() == 0) {
          // unsubscribe all
          cmd.setExpectedReplies(subscriptions.patternSize());
          subscriptions.unregisterPatternSubscribeHandler(null);
        } else {
          cmd.setExpectedReplies(redisArgs.size());

          for (Object obj : redisArgs) {
            String pattern = (String) obj;
            subscriptions.unregisterPatternSubscribeHandler(pattern);
          }
        }
        pubsub.send(cmd);
        break;

      case UNSUBSCRIBE:
        // unregister all channels
        if (redisArgs == null || redisArgs.size() == 0) {
          // unsubscribe all
          cmd.setExpectedReplies(subscriptions.channelSize());
          subscriptions.unregisterChannelSubscribeHandler(null);
        } else {
          cmd.setExpectedReplies(redisArgs.size());

          for (Object obj : redisArgs) {
            String channel = (String) obj;
            subscriptions.unregisterChannelSubscribeHandler(channel);
          }
        }
        pubsub.send(cmd);
        break;
      case QUIT:
        // this is a special case that must be sent to all connections
        redis.send(cmd);
        pubsub.send(cmd);
        break;
      default:
        // all other commands are sent to the normal connection
        redis.send(cmd);
        break;
    }
  }
}