/*
 * Copyright (c) 2010-2015 Pivotal Software, Inc. All rights reserved.
 *
 * 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. See accompanying
 * LICENSE file.
 */
package com.gemstone.gemfire.internal.memcached.commands;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import com.gemstone.gemfire.cache.Cache;
import com.gemstone.gemfire.cache.Region;
import com.gemstone.gemfire.internal.memcached.Command;
import com.gemstone.gemfire.internal.memcached.KeyWrapper;
import com.gemstone.gemfire.internal.memcached.Reply;
import com.gemstone.gemfire.internal.memcached.RequestReader;
import com.gemstone.gemfire.internal.memcached.ResponseStatus;
import com.gemstone.gemfire.internal.memcached.ValueWrapper;
import com.gemstone.gemfire.memcached.GemFireMemcachedServer.Protocol;

/**
 * 
 * The retrieval commands "get" and "gets" operates like this:<br/>
 * <code>
 * get &lt;key&gt;*\r\n<br/>
 * gets &lt;key&gt;*\r\n
 * </code>
 * <p>
 * Each item sent by the server looks like this:<br/>
 * <code>
 * VALUE &lt;key&gt; &lt;flags&gt; &lt;bytes&gt; [&lt;cas unique&gt;]\r\n<br/>
 * &lt;data block&gt;\r\n
 * </code>
 * 
 * @author Swapnil Bawaskar
 *
 */
public class GetCommand extends AbstractCommand {

  private static final String VALUE = "VALUE";
  private static final String W_SPACE = " ";
  private static final String RN = "\r\n";
  private static final ByteBuffer RN_BUF = asciiCharset.encode(RN);
  private static final ByteBuffer END_BUF = asciiCharset.encode(Reply.END.toString());
  
  /**
   * buffer used to compose one line of reply
   */
  private static ThreadLocal<CharBuffer> lineBuffer = new ThreadLocal<CharBuffer>();
  
  /**
   * defaults to the default send buffer size on socket
   */
  private static final int REPLY_BUFFER_CAPACITY = Integer.getInteger("replyBufferCapacity", 146988);
  
  /**
   * buffer for sending get replies, one per thread
   */
  private static ThreadLocal<ByteBuffer> replyBuffer = new ThreadLocal<ByteBuffer>();

  private static final int EXTRAS_LENGTH = 4;

  @Override
  public ByteBuffer processCommand(RequestReader request, Protocol protocol, Cache cache) {
    if (protocol == Protocol.ASCII) {
      return processAsciiCommand(request, cache);
    }
    return processBinaryCommand(request.getRequest(), request, cache, request.getResponse());
  }

  protected ByteBuffer processBinaryCommand(ByteBuffer buffer, RequestReader request, Cache cache, ByteBuffer response) {
    Region<Object, ValueWrapper> r = getMemcachedRegion(cache);
    
    KeyWrapper key = getKey(buffer, HEADER_LENGTH);
    ValueWrapper val = r.get(key);
    if (getLogger().fineEnabled()) {
      getLogger().fine("get:key:"+key+" val:"+val);
    }
    if (val == null) {
      if (isQuiet()) {
        return null;
      }
      response.putShort(POSITION_RESPONSE_STATUS, ResponseStatus.KEY_NOT_FOUND.asShort());
    } else {
      byte[] realValue = val.getValue();
      int responseLength = HEADER_LENGTH + realValue.length + EXTRAS_LENGTH + (sendKeysInResponse() ? key.getKey().length : 0);
      if (response.capacity() < responseLength) {
        response = request.getResponse(responseLength);
      }
      response.limit(responseLength);
      response.putShort(POSITION_RESPONSE_STATUS, ResponseStatus.NO_ERROR.asShort());
      response.put(EXTRAS_LENGTH_INDEX, (byte) EXTRAS_LENGTH);
      response.putInt(TOTAL_BODY_LENGTH_INDEX, EXTRAS_LENGTH + realValue.length + (sendKeysInResponse() ? key.getKey().length : 0));
      response.putLong(POSITION_CAS, val.getVersion());
      response.position(HEADER_LENGTH);
      response.putInt(val.getFlags());
      if (sendKeysInResponse()) {
        response.put(key.getKey());
      }
      response.put(realValue);
      
      response.flip();
    }
    return response;
  }

  /**
   * Overridden by GetQ and getKQ to not send reply on cache miss
   */
  protected boolean isQuiet() {
    return false;
  }

  /**
   * Overridden by GetK command
   */
  protected boolean sendKeysInResponse() {
    return false;
  }

  private ByteBuffer processAsciiCommand(RequestReader request, Cache cache) {
    ByteBuffer buffer = request.getRequest();
    CharBuffer flb = getFirstLineBuffer();
    getAsciiDecoder().reset();
    getAsciiDecoder().decode(buffer, flb, false);
    flb.flip();
    String firstLine = getFirstLine();
    String[] firstLineElements = firstLine.split(" ");
    
    boolean isGets = firstLineElements[0].equals("gets");
    Set<String> keys = new HashSet<String>();
    for (int i=1; i<firstLineElements.length; i++) {
      keys.add(stripNewline(firstLineElements[i]));
    }
    
    Region<Object, ValueWrapper> r = getMemcachedRegion(cache);
    Map<Object, ValueWrapper> results = r.getAll(keys);
    
    return composeReply(results, isGets);
  }

  private ByteBuffer composeReply(Map<Object, ValueWrapper> results, boolean isGets) {
    Iterator<Entry<Object, ValueWrapper>> it = results.entrySet().iterator();
    ByteBuffer buffer = getReplyBuffer();
    while (it.hasNext()) {
      Entry<Object, ValueWrapper> e = it.next();
      if (getLogger().fineEnabled()) {
        getLogger().fine("get compose reply:"+e);
      }
      ValueWrapper valWrapper = e.getValue();
      if (valWrapper != null) {
        byte[] v = valWrapper.getValue();
        CharBuffer reply = getLineBuffer();
        reply.put(VALUE).put(W_SPACE);
        reply.put(e.getKey().toString()).put(W_SPACE);
        reply.put(Integer.toString(valWrapper.getFlags())).put(W_SPACE); // flags
        
        String valBytes = v == null ? Integer.toString(0) : Integer.toString(v.length);
        reply.put(valBytes);
        if (isGets) {
          // send the version for gets command
          reply.put(W_SPACE);
          reply.put(Long.toString(valWrapper.getVersion()));
        }
        reply.put(RN);
        reply.flip();
        getAsciiEncoder().encode(reply, buffer, false);
        // put the actual value
        buffer.put(v);
        RN_BUF.rewind();
        buffer.put(RN_BUF);
      }
    }
    END_BUF.rewind();
    buffer.put(END_BUF);
    buffer.flip();
    return buffer;
  }
  
  private ByteBuffer getReplyBuffer() {
    ByteBuffer retVal = replyBuffer.get();
    if (retVal == null) {
      retVal = ByteBuffer.allocate(REPLY_BUFFER_CAPACITY);
      replyBuffer.set(retVal);
    }
    retVal.clear();
    return retVal;
  }
  
  private CharBuffer getLineBuffer() {
    CharBuffer retVal = lineBuffer.get();
    if (retVal == null) {
      retVal = CharBuffer.allocate(1024);
      lineBuffer.set(retVal);
    }
    retVal.clear();
    return retVal;
  }
}
