/*
 * 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 com.datastax.driver.core;

import static com.datastax.driver.core.SchemaElement.AGGREGATE;
import static com.datastax.driver.core.SchemaElement.FUNCTION;
import static com.datastax.driver.core.SchemaElement.KEYSPACE;
import static com.datastax.driver.core.SchemaElement.TABLE;

import com.datastax.driver.core.Responses.Result.Rows.Metadata;
import com.datastax.driver.core.exceptions.AlreadyExistsException;
import com.datastax.driver.core.exceptions.AuthenticationException;
import com.datastax.driver.core.exceptions.BootstrappingException;
import com.datastax.driver.core.exceptions.CASWriteUnknownException;
import com.datastax.driver.core.exceptions.CDCWriteException;
import com.datastax.driver.core.exceptions.DriverException;
import com.datastax.driver.core.exceptions.DriverInternalError;
import com.datastax.driver.core.exceptions.FunctionExecutionException;
import com.datastax.driver.core.exceptions.InvalidConfigurationInQueryException;
import com.datastax.driver.core.exceptions.InvalidQueryException;
import com.datastax.driver.core.exceptions.OverloadedException;
import com.datastax.driver.core.exceptions.ProtocolError;
import com.datastax.driver.core.exceptions.ReadFailureException;
import com.datastax.driver.core.exceptions.ReadTimeoutException;
import com.datastax.driver.core.exceptions.ServerError;
import com.datastax.driver.core.exceptions.SyntaxError;
import com.datastax.driver.core.exceptions.TruncateException;
import com.datastax.driver.core.exceptions.UnauthorizedException;
import com.datastax.driver.core.exceptions.UnavailableException;
import com.datastax.driver.core.exceptions.UnpreparedException;
import com.datastax.driver.core.exceptions.WriteFailureException;
import com.datastax.driver.core.exceptions.WriteTimeoutException;
import com.datastax.driver.core.utils.Bytes;
import io.netty.buffer.ByteBuf;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

class Responses {

  private Responses() {}

  static class Error extends Message.Response {

    static final Message.Decoder<Error> decoder =
        new Message.Decoder<Error>() {
          @Override
          public Error decode(ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) {
            ExceptionCode code = ExceptionCode.fromValue(body.readInt());
            String msg = CBUtil.readString(body);
            Object infos = null;
            ConsistencyLevel clt;
            int received, blockFor;
            switch (code) {
              case UNAVAILABLE:
                ConsistencyLevel clu = CBUtil.readConsistencyLevel(body);
                int required = body.readInt();
                int alive = body.readInt();
                infos = new UnavailableException(clu, required, alive);
                break;
              case WRITE_TIMEOUT:
              case READ_TIMEOUT:
                clt = CBUtil.readConsistencyLevel(body);
                received = body.readInt();
                blockFor = body.readInt();
                if (code == ExceptionCode.WRITE_TIMEOUT) {
                  WriteType writeType = Enum.valueOf(WriteType.class, CBUtil.readString(body));
                  infos = new WriteTimeoutException(clt, writeType, received, blockFor);
                } else {
                  byte dataPresent = body.readByte();
                  infos = new ReadTimeoutException(clt, received, blockFor, dataPresent != 0);
                }
                break;
              case WRITE_FAILURE:
              case READ_FAILURE:
                clt = CBUtil.readConsistencyLevel(body);
                received = body.readInt();
                blockFor = body.readInt();
                int failures = body.readInt();
                Map<InetAddress, Integer> failuresMap;
                if (version.compareTo(ProtocolVersion.V5) < 0) {
                  failuresMap = Collections.emptyMap();
                } else {
                  failuresMap = new HashMap<InetAddress, Integer>();
                  for (int i = 0; i < failures; i++) {
                    InetAddress address = CBUtil.readInetWithoutPort(body);
                    int reasonCode = body.readUnsignedShort();
                    failuresMap.put(address, reasonCode);
                  }
                }
                if (code == ExceptionCode.WRITE_FAILURE) {
                  WriteType writeType = Enum.valueOf(WriteType.class, CBUtil.readString(body));
                  infos =
                      new WriteFailureException(
                          clt, writeType, received, blockFor, failures, failuresMap);
                } else {
                  byte dataPresent = body.readByte();
                  infos =
                      new ReadFailureException(
                          clt, received, blockFor, failures, failuresMap, dataPresent != 0);
                }
                break;
              case CAS_WRITE_UNKNOWN:
                clt = CBUtil.readConsistencyLevel(body);
                received = body.readInt();
                blockFor = body.readInt();
                infos = new CASWriteUnknownException(clt, received, blockFor);
                break;
              case UNPREPARED:
                infos = MD5Digest.wrap(CBUtil.readBytes(body));
                break;
              case ALREADY_EXISTS:
                String ksName = CBUtil.readString(body);
                String cfName = CBUtil.readString(body);
                infos = new AlreadyExistsException(ksName, cfName);
                break;
            }
            return new Error(version, code, msg, infos);
          }
        };

    final ProtocolVersion serverProtocolVersion;
    final ExceptionCode code;
    final String message;
    final Object infos; // can be null

    private Error(
        ProtocolVersion serverProtocolVersion, ExceptionCode code, String message, Object infos) {
      super(Message.Response.Type.ERROR);
      this.serverProtocolVersion = serverProtocolVersion;
      this.code = code;
      this.message = message;
      this.infos = infos;
    }

    DriverException asException(EndPoint endPoint) {
      switch (code) {
        case SERVER_ERROR:
          return new ServerError(endPoint, message);
        case PROTOCOL_ERROR:
          return new ProtocolError(endPoint, message);
        case BAD_CREDENTIALS:
          return new AuthenticationException(endPoint, message);
        case UNAVAILABLE:
          return ((UnavailableException) infos)
              .copy(endPoint); // We copy to have a nice stack trace
        case OVERLOADED:
          return new OverloadedException(endPoint, message);
        case IS_BOOTSTRAPPING:
          return new BootstrappingException(endPoint, message);
        case TRUNCATE_ERROR:
          return new TruncateException(endPoint, message);
        case WRITE_TIMEOUT:
          return ((WriteTimeoutException) infos).copy(endPoint);
        case READ_TIMEOUT:
          return ((ReadTimeoutException) infos).copy(endPoint);
        case WRITE_FAILURE:
          return ((WriteFailureException) infos).copy(endPoint);
        case READ_FAILURE:
          return ((ReadFailureException) infos).copy(endPoint);
        case FUNCTION_FAILURE:
          return new FunctionExecutionException(endPoint, message);
        case CDC_WRITE_FAILURE:
          return new CDCWriteException(endPoint, message);
        case CAS_WRITE_UNKNOWN:
          return ((CASWriteUnknownException) infos).copy(endPoint);
        case SYNTAX_ERROR:
          return new SyntaxError(endPoint, message);
        case UNAUTHORIZED:
          return new UnauthorizedException(endPoint, message);
        case INVALID:
          return new InvalidQueryException(endPoint, message);
        case CONFIG_ERROR:
          return new InvalidConfigurationInQueryException(endPoint, message);
        case ALREADY_EXISTS:
          return ((AlreadyExistsException) infos).copy(endPoint);
        case UNPREPARED:
          return new UnpreparedException(endPoint, message);

        default:
          return new DriverInternalError(
              String.format(
                  "Unknown protocol error code %s returned by %s. The error message was: %s",
                  code, endPoint, message));
      }
    }

    @Override
    public String toString() {
      return "ERROR " + code + ": " + message;
    }
  }

  static class Ready extends Message.Response {

    static final Message.Decoder<Ready> decoder =
        new Message.Decoder<Ready>() {
          @Override
          public Ready decode(ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) {
            // TODO: Would it be cool to return a singleton? Check we don't need to
            // set the streamId or something
            return new Ready();
          }
        };

    Ready() {
      super(Message.Response.Type.READY);
    }

    @Override
    public String toString() {
      return "READY";
    }
  }

  static class Authenticate extends Message.Response {

    static final Message.Decoder<Authenticate> decoder =
        new Message.Decoder<Authenticate>() {
          @Override
          public Authenticate decode(
              ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) {
            String authenticator = CBUtil.readString(body);
            return new Authenticate(authenticator);
          }
        };

    final String authenticator;

    Authenticate(String authenticator) {
      super(Message.Response.Type.AUTHENTICATE);
      this.authenticator = authenticator;
    }

    @Override
    public String toString() {
      return "AUTHENTICATE " + authenticator;
    }
  }

  static class Supported extends Message.Response {

    static final Message.Decoder<Supported> decoder =
        new Message.Decoder<Supported>() {
          @Override
          public Supported decode(
              ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) {
            return new Supported(CBUtil.readStringToStringListMap(body));
          }
        };

    final Map<String, List<String>> supported;
    final Set<ProtocolOptions.Compression> supportedCompressions =
        EnumSet.noneOf(ProtocolOptions.Compression.class);

    Supported(Map<String, List<String>> supported) {
      super(Message.Response.Type.SUPPORTED);
      this.supported = supported;

      parseCompressions();
    }

    private void parseCompressions() {
      List<String> compList = supported.get(Requests.Startup.COMPRESSION_OPTION);
      if (compList == null) return;

      for (String compStr : compList) {
        ProtocolOptions.Compression compr = ProtocolOptions.Compression.fromString(compStr);
        if (compr != null) supportedCompressions.add(compr);
      }
    }

    @Override
    public String toString() {
      return "SUPPORTED " + supported;
    }
  }

  abstract static class Result extends Message.Response {

    static final Message.Decoder<Result> decoder =
        new Message.Decoder<Result>() {
          @Override
          public Result decode(ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) {
            Kind kind = Kind.fromId(body.readInt());
            return kind.subDecoder.decode(body, version, codecRegistry);
          }
        };

    enum Kind {
      VOID(1, Void.subcodec),
      ROWS(2, Rows.subcodec),
      SET_KEYSPACE(3, SetKeyspace.subcodec),
      PREPARED(4, Prepared.subcodec),
      SCHEMA_CHANGE(5, SchemaChange.subcodec);

      private final int id;
      final Message.Decoder<Result> subDecoder;

      private static final Kind[] ids;

      static {
        int maxId = -1;
        for (Kind k : Kind.values()) maxId = Math.max(maxId, k.id);
        ids = new Kind[maxId + 1];
        for (Kind k : Kind.values()) {
          if (ids[k.id] != null) throw new IllegalStateException("Duplicate kind id");
          ids[k.id] = k;
        }
      }

      Kind(int id, Message.Decoder<Result> subDecoder) {
        this.id = id;
        this.subDecoder = subDecoder;
      }

      static Kind fromId(int id) {
        Kind k = ids[id];
        if (k == null)
          throw new DriverInternalError(String.format("Unknown kind id %d in RESULT message", id));
        return k;
      }
    }

    final Kind kind;

    protected Result(Kind kind) {
      super(Message.Response.Type.RESULT);
      this.kind = kind;
    }

    static class Void extends Result {
      // Even though we have no specific information here, don't make a
      // singleton since as each message it has in fact a streamid and connection.
      Void() {
        super(Kind.VOID);
      }

      static final Message.Decoder<Result> subcodec =
          new Message.Decoder<Result>() {
            @Override
            public Result decode(
                ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) {
              return new Void();
            }
          };

      @Override
      public String toString() {
        return "EMPTY RESULT";
      }
    }

    static class SetKeyspace extends Result {
      final String keyspace;

      private SetKeyspace(String keyspace) {
        super(Kind.SET_KEYSPACE);
        this.keyspace = keyspace;
      }

      static final Message.Decoder<Result> subcodec =
          new Message.Decoder<Result>() {
            @Override
            public Result decode(
                ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) {
              return new SetKeyspace(CBUtil.readString(body));
            }
          };

      @Override
      public String toString() {
        return "RESULT set keyspace " + keyspace;
      }
    }

    static class Rows extends Result {

      static class Metadata {

        private enum Flag {
          // The order of that enum matters!!
          GLOBAL_TABLES_SPEC,
          HAS_MORE_PAGES,
          NO_METADATA,
          METADATA_CHANGED;

          static EnumSet<Flag> deserialize(int flags) {
            EnumSet<Flag> set = EnumSet.noneOf(Flag.class);
            Flag[] values = Flag.values();
            for (int n = 0; n < values.length; n++) {
              if ((flags & (1 << n)) != 0) set.add(values[n]);
            }
            return set;
          }

          static int serialize(EnumSet<Flag> flags) {
            int i = 0;
            for (Flag flag : flags) i |= 1 << flag.ordinal();
            return i;
          }
        }

        static final Metadata EMPTY = new Metadata(null, 0, null, null, null);

        final int columnCount;
        final ColumnDefinitions columns; // Can be null if no metadata was asked by the query
        final ByteBuffer pagingState;
        final int[] pkIndices;
        final MD5Digest
            metadataId; // only present if the flag METADATA_CHANGED is set (ROWS response only)

        private Metadata(
            MD5Digest metadataId,
            int columnCount,
            ColumnDefinitions columns,
            ByteBuffer pagingState,
            int[] pkIndices) {
          this.metadataId = metadataId;
          this.columnCount = columnCount;
          this.columns = columns;
          this.pagingState = pagingState;
          this.pkIndices = pkIndices;
        }

        static Metadata decode(
            ByteBuf body, ProtocolVersion protocolVersion, CodecRegistry codecRegistry) {
          return decode(body, false, protocolVersion, codecRegistry);
        }

        static Metadata decode(
            ByteBuf body,
            boolean withPkIndices,
            ProtocolVersion protocolVersion,
            CodecRegistry codecRegistry) {

          // flags & column count
          EnumSet<Flag> flags = Flag.deserialize(body.readInt());
          int columnCount = body.readInt();

          ByteBuffer state = null;
          if (flags.contains(Flag.HAS_MORE_PAGES)) state = CBUtil.readValue(body);

          MD5Digest resultMetadataId = null;
          if (flags.contains(Flag.METADATA_CHANGED)) {
            assert ProtocolFeature.PREPARED_METADATA_CHANGES.isSupportedBy(protocolVersion)
                : "METADATA_CHANGED flag is not supported in protocol version " + protocolVersion;
            assert !flags.contains(Flag.NO_METADATA)
                : "METADATA_CHANGED and NO_METADATA are mutually exclusive flags";
            resultMetadataId = MD5Digest.wrap(CBUtil.readBytes(body));
          }

          int[] pkIndices = null;
          int pkCount;
          if (withPkIndices && (pkCount = body.readInt()) > 0) {
            pkIndices = new int[pkCount];
            for (int i = 0; i < pkCount; i++) pkIndices[i] = (int) body.readShort();
          }

          if (flags.contains(Flag.NO_METADATA))
            return new Metadata(resultMetadataId, columnCount, null, state, pkIndices);

          boolean globalTablesSpec = flags.contains(Flag.GLOBAL_TABLES_SPEC);

          String globalKsName = null;
          String globalCfName = null;
          if (globalTablesSpec) {
            globalKsName = CBUtil.readString(body);
            globalCfName = CBUtil.readString(body);
          }

          // metadata (names/types)
          ColumnDefinitions.Definition[] defs = new ColumnDefinitions.Definition[columnCount];
          for (int i = 0; i < columnCount; i++) {
            String ksName = globalTablesSpec ? globalKsName : CBUtil.readString(body);
            String cfName = globalTablesSpec ? globalCfName : CBUtil.readString(body);
            String name = CBUtil.readString(body);
            DataType type = DataType.decode(body, protocolVersion, codecRegistry);
            defs[i] = new ColumnDefinitions.Definition(ksName, cfName, name, type);
          }

          return new Metadata(
              resultMetadataId,
              columnCount,
              new ColumnDefinitions(defs, codecRegistry),
              state,
              pkIndices);
        }

        @Override
        public String toString() {
          StringBuilder sb = new StringBuilder();

          if (columns == null) {
            sb.append('[').append(columnCount).append(" columns]");
          } else {
            for (ColumnDefinitions.Definition column : columns) {
              sb.append('[').append(column.getName());
              sb.append(" (").append(column.getType()).append(")]");
            }
          }
          if (pagingState != null) sb.append(" (to be continued)");
          return sb.toString();
        }
      }

      static final Message.Decoder<Result> subcodec =
          new Message.Decoder<Result>() {
            @Override
            public Result decode(
                ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) {

              Metadata metadata = Metadata.decode(body, version, codecRegistry);

              int rowCount = body.readInt();
              int columnCount = metadata.columnCount;

              Queue<List<ByteBuffer>> data = new ArrayDeque<List<ByteBuffer>>(rowCount);
              for (int i = 0; i < rowCount; i++) {
                List<ByteBuffer> row = new ArrayList<ByteBuffer>(columnCount);
                for (int j = 0; j < columnCount; j++) row.add(CBUtil.readValue(body));
                data.add(row);
              }

              return new Rows(metadata, data, version);
            }
          };

      final Metadata metadata;
      final Queue<List<ByteBuffer>> data;
      private final ProtocolVersion version;

      private Rows(Metadata metadata, Queue<List<ByteBuffer>> data, ProtocolVersion version) {
        super(Kind.ROWS);
        this.metadata = metadata;
        this.data = data;
        this.version = version;
      }

      @Override
      public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("ROWS ").append(metadata).append('\n');
        for (List<ByteBuffer> row : data) {
          for (int i = 0; i < row.size(); i++) {
            ByteBuffer v = row.get(i);
            if (v == null) {
              sb.append(" | null");
            } else {
              sb.append(" | ");
              if (metadata.columns != null) {
                DataType dataType = metadata.columns.getType(i);
                sb.append(dataType);
                sb.append(" ");
                TypeCodec<Object> codec = metadata.columns.codecRegistry.codecFor(dataType);
                Object o = codec.deserialize(v, version);
                String s = codec.format(o);
                if (s.length() > 100) s = s.substring(0, 100) + "...";
                sb.append(s);
              } else {
                sb.append(Bytes.toHexString(v));
              }
            }
          }
          sb.append('\n');
        }
        sb.append("---");
        return sb.toString();
      }
    }

    static class Prepared extends Result {

      static final Message.Decoder<Result> subcodec =
          new Message.Decoder<Result>() {
            @Override
            public Result decode(
                ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) {
              MD5Digest id = MD5Digest.wrap(CBUtil.readBytes(body));
              MD5Digest resultMetadataId = null;
              if (ProtocolFeature.PREPARED_METADATA_CHANGES.isSupportedBy(version))
                resultMetadataId = MD5Digest.wrap(CBUtil.readBytes(body));
              boolean withPkIndices = version.compareTo(ProtocolVersion.V4) >= 0;
              Rows.Metadata metadata =
                  Rows.Metadata.decode(body, withPkIndices, version, codecRegistry);
              Rows.Metadata resultMetadata = decodeResultMetadata(body, version, codecRegistry);
              return new Prepared(id, resultMetadataId, metadata, resultMetadata);
            }

            private Metadata decodeResultMetadata(
                ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) {
              switch (version) {
                case V1:
                  return Rows.Metadata.EMPTY;
                case V2:
                case V3:
                case V4:
                case V5:
                case V6:
                  return Rows.Metadata.decode(body, version, codecRegistry);
                default:
                  throw version.unsupported();
              }
            }
          };

      final MD5Digest statementId;
      final MD5Digest resultMetadataId;
      final Rows.Metadata metadata;
      final Rows.Metadata resultMetadata;

      private Prepared(
          MD5Digest statementId,
          MD5Digest resultMetadataId,
          Rows.Metadata metadata,
          Rows.Metadata resultMetadata) {
        super(Kind.PREPARED);
        this.statementId = statementId;
        this.resultMetadataId = resultMetadataId;
        this.metadata = metadata;
        this.resultMetadata = resultMetadata;
      }

      @Override
      public String toString() {
        return "RESULT PREPARED "
            + statementId
            + ' '
            + metadata
            + " (resultMetadata="
            + resultMetadata
            + ')';
      }
    }

    static class SchemaChange extends Result {

      enum Change {
        CREATED,
        UPDATED,
        DROPPED
      }

      final Change change;
      final SchemaElement targetType;
      final String targetKeyspace;
      final String targetName;
      final List<String> targetSignature;

      static final Message.Decoder<Result> subcodec =
          new Message.Decoder<Result>() {
            @Override
            public Result decode(
                ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) {
              // Note: the CREATE KEYSPACE/TABLE/TYPE SCHEMA_CHANGE response is different from the
              // SCHEMA_CHANGE EVENT type
              Change change;
              SchemaElement targetType;
              String targetKeyspace, targetName;
              List<String> targetSignature;
              switch (version) {
                case V1:
                case V2:
                  change = CBUtil.readEnumValue(Change.class, body);
                  targetKeyspace = CBUtil.readString(body);
                  targetName = CBUtil.readString(body);
                  targetType = targetName.isEmpty() ? KEYSPACE : TABLE;
                  targetSignature = Collections.emptyList();
                  return new SchemaChange(
                      change, targetType, targetKeyspace, targetName, targetSignature);
                case V3:
                case V4:
                case V5:
                case V6:
                  change = CBUtil.readEnumValue(Change.class, body);
                  targetType = CBUtil.readEnumValue(SchemaElement.class, body);
                  targetKeyspace = CBUtil.readString(body);
                  targetName = (targetType == KEYSPACE) ? "" : CBUtil.readString(body);
                  targetSignature =
                      (targetType == FUNCTION || targetType == AGGREGATE)
                          ? CBUtil.readStringList(body)
                          : Collections.<String>emptyList();
                  return new SchemaChange(
                      change, targetType, targetKeyspace, targetName, targetSignature);
                default:
                  throw version.unsupported();
              }
            }
          };

      private SchemaChange(
          Change change,
          SchemaElement targetType,
          String targetKeyspace,
          String targetName,
          List<String> targetSignature) {
        super(Kind.SCHEMA_CHANGE);
        this.change = change;
        this.targetType = targetType;
        this.targetKeyspace = targetKeyspace;
        this.targetName = targetName;
        this.targetSignature = targetSignature;
      }

      @Override
      public String toString() {
        return "RESULT schema change "
            + change
            + " on "
            + targetType
            + ' '
            + targetKeyspace
            + (targetName.isEmpty() ? "" : '.' + targetName);
      }
    }
  }

  static class Event extends Message.Response {

    static final Message.Decoder<Event> decoder =
        new Message.Decoder<Event>() {
          @Override
          public Event decode(ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) {
            return new Event(ProtocolEvent.deserialize(body, version));
          }
        };

    final ProtocolEvent event;

    Event(ProtocolEvent event) {
      super(Message.Response.Type.EVENT);
      this.event = event;
    }

    @Override
    public String toString() {
      return "EVENT " + event;
    }
  }

  static class AuthChallenge extends Message.Response {

    static final Message.Decoder<AuthChallenge> decoder =
        new Message.Decoder<AuthChallenge>() {
          @Override
          public AuthChallenge decode(
              ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) {
            ByteBuffer b = CBUtil.readValue(body);
            if (b == null) return new AuthChallenge(null);

            byte[] token = new byte[b.remaining()];
            b.get(token);
            return new AuthChallenge(token);
          }
        };

    final byte[] token;

    private AuthChallenge(byte[] token) {
      super(Message.Response.Type.AUTH_CHALLENGE);
      this.token = token;
    }
  }

  static class AuthSuccess extends Message.Response {

    static final Message.Decoder<AuthSuccess> decoder =
        new Message.Decoder<AuthSuccess>() {
          @Override
          public AuthSuccess decode(
              ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) {
            ByteBuffer b = CBUtil.readValue(body);
            if (b == null) return new AuthSuccess(null);

            byte[] token = new byte[b.remaining()];
            b.get(token);
            return new AuthSuccess(token);
          }
        };

    final byte[] token;

    private AuthSuccess(byte[] token) {
      super(Message.Response.Type.AUTH_SUCCESS);
      this.token = token;
    }
  }
}
