/**
 * Copyright (c) 2012 YCSB contributors. 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.yahoo.ycsb.db;

import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Properties;
import java.util.Set;
import java.util.Vector;
import java.util.Map;

import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;

import com.yahoo.mapkeeper.BinaryResponse;
import com.yahoo.mapkeeper.MapKeeper;
import com.yahoo.mapkeeper.Record;
import com.yahoo.mapkeeper.RecordListResponse;
import com.yahoo.mapkeeper.ResponseCode;
import com.yahoo.mapkeeper.ScanOrder;
import com.yahoo.ycsb.ByteIterator;
import com.yahoo.ycsb.DB;
import com.yahoo.ycsb.StringByteIterator;
import com.yahoo.ycsb.workloads.CoreWorkload;
import com.yahoo.ycsb.Status;

public class MapKeeperClient extends DB {
    private static final String HOST = "mapkeeper.host";
    private static final String HOST_DEFAULT = "localhost";
    private static final String PORT = "mapkeeper.port";
    private static final String PORT_DEFAULT = "9090";
    MapKeeper.Client c; 
    boolean writeallfields;
    static boolean initteddb = false;
    private synchronized static void initDB(Properties p, MapKeeper.Client c) throws TException {
        if(!initteddb) {
            initteddb = true;
            c.addMap(p.getProperty(CoreWorkload.TABLENAME_PROPERTY, CoreWorkload.TABLENAME_PROPERTY_DEFAULT));
        }
    }

    public void init() {
        String host = getProperties().getProperty(HOST, HOST_DEFAULT);
        int port = Integer.parseInt(getProperties().getProperty(PORT, PORT_DEFAULT));
        TTransport tr = new TFramedTransport(new TSocket(host, port));
        TProtocol proto = new TBinaryProtocol(tr);
        c = new MapKeeper.Client(proto);
        try {
            tr.open();
            initDB(getProperties(), c);
        } catch(TException e) {
            throw new RuntimeException(e);
        }
        writeallfields = Boolean.parseBoolean(getProperties().getProperty(CoreWorkload.WRITE_ALL_FIELDS_PROPERTY, 
                    CoreWorkload.WRITE_ALL_FIELDS_PROPERTY_DEFAULT));
    }

    ByteBuffer encode(HashMap<String, ByteIterator> values) {
        int len = 0;
        for(Map.Entry<String, ByteIterator> entry : values.entrySet()) {
            len += (entry.getKey().length() + 1 + entry.getValue().bytesLeft() + 1);
        }
        byte[] array = new byte[len];
        int i = 0;
        for(Map.Entry<String, ByteIterator> entry : values.entrySet()) {
            for(int j = 0; j < entry.getKey().length(); j++) {
                array[i] = (byte)entry.getKey().charAt(j);
                i++;
            }
            array[i] = '\t'; // XXX would like to use sane delimiter (null, 254, 255, ...) but java makes this nearly impossible
            i++;
            ByteIterator v = entry.getValue();
            i = v.nextBuf(array, i);
            array[i] = '\t';
            i++;
        }
        array[array.length-1] = 0;
        ByteBuffer buf = ByteBuffer.wrap(array);
        buf.rewind();
        return buf;
    }
    void decode(Set<String> fields, String tups, HashMap<String, ByteIterator> tup) {
        String[] tok = tups.split("\\t");
        if(tok.length == 0) { throw new IllegalStateException("split returned empty array!"); }
        for(int i = 0; i < tok.length; i+=2) {
            if(fields == null || fields.contains(tok[i])) {
                if(tok.length < i+2) { throw new IllegalStateException("Couldn't parse tuple <" + tups + "> at index " + i); }
                if(tok[i] == null || tok[i+1] == null) throw new NullPointerException("Key is " + tok[i] + " val is + " + tok[i+1]);
                tup.put(tok[i], new StringByteIterator(tok[i+1]));
            }
        }
        if(tok.length == 0) {
            System.err.println("Empty tuple: " + tups);
        }
    }

    int ycsbThriftRet(BinaryResponse succ, ResponseCode zero, ResponseCode one) {
        return ycsbThriftRet(succ.responseCode, zero, one);
    }
    int ycsbThriftRet(ResponseCode rc, ResponseCode zero, ResponseCode one) {
        return
            rc == zero ? 0 :
            rc == one  ? 1 : 2;
    }
    ByteBuffer bufStr(String str) {
        ByteBuffer buf = ByteBuffer.wrap(str.getBytes());
        return buf;
    }
    String strResponse(BinaryResponse buf) {
        return new String(buf.value.array());
    }

    @Override
    public Status read(String table, String key, Set<String> fields,
            HashMap<String, ByteIterator> result) {
        try {
            ByteBuffer buf = bufStr(key);

            BinaryResponse succ = c.get(table, buf);

            int ret = ycsbThriftRet(
                    succ,
                    ResponseCode.RecordExists,
                    ResponseCode.RecordNotFound);

            if(ret == 0) {
                decode(fields, strResponse(succ), result);
		return Status.OK;
            }
            return Status.ERROR;
        } catch(TException e) {
            e.printStackTrace();
            return Status.ERROR;
        }
    }

    @Override
    public Status scan(String table, String startkey, int recordcount,
            Set<String> fields, Vector<HashMap<String, ByteIterator>> result) {
        try {
            //XXX what to pass in for nulls / zeros?
            RecordListResponse res = c.scan(table, ScanOrder.Ascending, bufStr(startkey), true, null, false, recordcount, 0);
            int ret = ycsbThriftRet(res.responseCode, ResponseCode.Success, ResponseCode.ScanEnded);
            if(ret == 0) {
                for(Record r : res.records) {
                    HashMap<String, ByteIterator> tuple = new HashMap<String, ByteIterator>();
                    // Note: r.getKey() and r.getValue() call special helper methods that trim the buffer
                    // to an appropriate length, and memcpy it to a byte[].  Trying to manipulate the ByteBuffer
                    // directly leads to trouble.
                    tuple.put("key", new StringByteIterator(new String(r.getKey())));
                    decode(fields, new String(r.getValue())/*strBuf(r.bufferForValue())*/, tuple);
                    result.add(tuple);
                }
		return Status.OK;
            }
            return Status.ERROR;
        } catch(TException e) {
            e.printStackTrace();
            return Status.ERROR;
        }
    }

    @Override
    public Status update(String table, String key,
            HashMap<String, ByteIterator> values) {
        try {
            if(!writeallfields) {
                HashMap<String, ByteIterator> oldval = new HashMap<String, ByteIterator>();
                read(table, key, null, oldval);
                for(Map.Entry<String, ByteIterator> entry : values.entrySet()) {
                    oldval.put(entry.getKey(), entry.getValue());
                }
                values = oldval;
            }
            ResponseCode succ = c.update(table, bufStr(key), encode(values));
            int ret = ycsbThriftRet(succ, ResponseCode.RecordExists, ResponseCode.RecordNotFound);
	    if (ret == 0) {
		return Status.OK;
	    }
	    return Status.ERROR;
        } catch(TException e) {
            e.printStackTrace();
            return Status.ERROR;
        }
    }

    @Override
    public Status insert(String table, String key,
            HashMap<String, ByteIterator> values) {
        try {
            int ret = ycsbThriftRet(c.insert(table, bufStr(key), encode(values)), ResponseCode.Success, ResponseCode.RecordExists);
            return (ret == 0) ? Status.OK : Status.ERROR;
        } catch(TException e) {
            e.printStackTrace();
            return Status.ERROR;
        }
    }

    @Override
    public Status delete(String table, String key) {
        try {
            return (ycsbThriftRet(c.remove(table, bufStr(key)), ResponseCode.Success, ResponseCode.RecordExists) == 0) ? Status.OK : Status.ERROR;
        } catch(TException e) {
            e.printStackTrace();
            return Status.ERROR;
        }
    }
}
