package org.jgroups.util;

import org.jgroups.Global;
import org.jgroups.Header;
import org.jgroups.conf.ClassConfigurator;

import java.util.HashMap;
import java.util.Map;

/**
 * Open addressing based implementation of a hashmap (not supporting the Map interface though) for message
 * headers. The keys are shorts (IDs) and the values Headers, and they're stored in 2 arrays: an ID array and a headers
 * array. The indices of the IDs array corespond with the headers array, e.g.
 * <pre>
 * IDs:      id-1  | id-2  | id-3  | ... | id-n |
 * Headers:  hdr-1 | hdr-2 | hdr-3 | ... | hdr-n |
 * </pre>
 *
 * The arrays are populated from left to right, and any 0 slots in 'ids' can terminate an interation, or signal empty slots.
 * <br/>
 * It is assumed that we only have a few headers, 3-4 on average. Note that getting a header for a given key and
 * putting a new key/header are operations with O(n) cost, so this implementation is <em>not</em> recommended for
 * a large number of elements.
 * <br/>
 * This class is not synchronized
 * @author Bela Ban
 */
public class Headers {
    private short[]  ids;
    private Header[] hdrs;

    /** Add space for 3 new elements when resizing */
    private static final int RESIZE_INCR=3;

    public Headers(int capacity) {
        ids=new short[capacity];
        hdrs=new Header[capacity];
    }

    public Headers(Headers other) {
        this(other.ids.length);
        System.arraycopy(other.ids, 0, this.ids, 0, other.ids.length);
        System.arraycopy(other.hdrs, 0, this.hdrs, 0, other.hdrs.length);
    }

    public short[] getRawIDs() {
        return ids;
    }

    public Header[] getRawHeaders() {
        return hdrs;
    }

    /**
     * Returns the header associated with an ID
     * @param id The ID
     * @return
     */
    public Header getHeader(short id) {
        for(int i=0; i < ids.length; i++) {
            short current_id=ids[i];
            if(current_id == 0)
                return null;
            if(current_id == id)
                return hdrs[i];
        }
        return null;
    }

    public Map<Short,Header> getHeaders() {
        Map<Short,Header> retval=new HashMap<Short,Header>(ids.length);
        for(int i=0; i < ids.length; i++) {
            if(ids[i] > 0)
                retval.put(ids[i], hdrs[i]);
            else
                break;
        }
        return retval;
    }

    public String printHeaders() {
        StringBuilder sb=new StringBuilder();
        boolean first=true;
        for(int i=0; i < ids.length; i++) {
            if(ids[i] > 0) {
                if(first)
                    first=false;
                else
                    sb.append(", ");
                Class clazz=ClassConfigurator.getProtocol(ids[i]);
                String name=clazz != null? clazz.getSimpleName() : Short.toString(ids[i]);
                sb.append(name).append(": ").append(hdrs[i]);
            }
            else
                break;
        }
        return sb.toString();
    }


    /** Puts a header given a key into the hashmap. Overwrites potential existing entry. */
    public void putHeader(short id, Header hdr) {
        _putHeader(id, hdr, 0, true);
    }




    /**
     * Puts a header given a key into the map, only if the key doesn't exist yet
     * @param id
     * @param hdr
     * @return the previous value associated with the specified id, or
     *         <tt>null</tt> if there was no mapping for the id.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with the id,
     *         if the implementation supports null values.)
     */
    public Header putHeaderIfAbsent(short id, Header hdr) {
        return _putHeader(id, hdr, 0, false);
    }


    public Headers copy() {
        return new Headers(this);
    }

    public int marshalledSize() {
        int retval=0;
        for(int i=0; i < ids.length; i++) {
            if(ids[i] > 0) {
                retval+=Global.SHORT_SIZE *2;    // for protocol ID and magic number
                retval+=hdrs[i].size();
            }
            else
                break;
        }
        return retval;
    }

    public int size() {
        int retval=0;
        for(int i=0; i < ids.length; i++) {
            if(ids[i] > 0)
                retval++;
            else
                break;
        }
        return retval;
    }

    public int capacity() {
        return ids.length;
    }

    public String printObjectHeaders() {
        StringBuilder sb=new StringBuilder();
        for(int i=0; i < ids.length; i++) {
            if(ids[i] > 0)
                sb.append(ids[i]).append(": ").append(hdrs[i]).append('\n');
            else
                break;
        }
        return sb.toString();
    }

    public String toString() {
        return printHeaders();
    }


    /**
     * Increases the capacity of the array and copies the contents of the old into the new array
     */
    private void resize() {
        int new_capacity=ids.length + RESIZE_INCR;

        short[] new_ids=new short[new_capacity];
        Header[] new_hdrs=new Header[new_capacity];

        System.arraycopy(ids, 0, new_ids, 0, ids.length);
        System.arraycopy(hdrs, 0, new_hdrs, 0, hdrs.length);

        ids=new_ids;
        hdrs=new_hdrs;
    }


    private Header _putHeader(short id, Header hdr, int start_index, boolean replace_if_present) {
        int i=start_index;
        while(i < ids.length) {
            if(ids[i] == 0) {
                ids[i]=id;
                hdrs[i]=hdr;
                return null;
            }
            if(ids[i] == id) {
                Header retval=hdrs[i];
                if(replace_if_present) {
                    hdrs[i]=hdr;
                }
                return retval;
            }
            i++;
            if(i >= ids.length) {
                resize();
            }
        }
        throw new IllegalStateException("unable to add element " + id + ", index=" + i); // we should never come here
    }


}