package network;

import java.net.InetAddress;
import java.util.Collection;
import java.util.HashMap;
import java.util.Random;

/**
 * This class provides capability to split messages to be sent into smaller
 * fragments that can be reassebled at remote end. User is allowed to specify
 * maximum size of fragments.
 *
 * Every fragment is prepended string that uniquely identifies its position
 * in sequence of fragments, so original message can be assebled correctly.
 * This string also contains value, which should distinguish fragment from
 * fragments belonging to other message.
 *
 * Format of string used is: "fragment: xxxxxxxxxx:yyyyyyyyyy\n" where x is
 * sequence number and y is unique identifier. "\n" represents new line.
 * 
 * @author Marcel Duris
 */
public class FragmentManager {

    private int fragmentSize;
    private String sid; //session id
    private static final int DEF_FRAG_SIZE = 1000;
    private final String HEADER = "fragment: ";
    private final String FIELD = "0000000000";
    private final String FORMAT = HEADER + FIELD + ":" + FIELD + "\n";

    /**
     * Creates new fragment manager.
     */
    public FragmentManager() {
        this(DEF_FRAG_SIZE);
    }

    /**
     * Creates new fragment manager splitting large messages into fragments of
     * maximum size entered by user. Every fragment has header prepended, so
     * actual size of fragment is larger by few bytes.
     *
     * @param fragmentSize maximum size of fragment
     */
    public FragmentManager(int fragmentSize) {
        this.fragmentSize = fragmentSize;
        sid = FIELD;
        sid = sid + Integer.toString(new Random().nextInt(10000));
        try {
            String adr = "";
            for (byte b : InetAddress.getLocalHost().getAddress()) {
                adr += Integer.toString(b, 32);
            }
            sid = sid + adr.substring(0, Math.max(0, adr.length() - 2));
        } catch (Exception ex) {
            //Logger.getLogger(FragmentManager.class.getName()).log(Level.SEVERE, null, ex);
        }
        sid = sid.substring(sid.length() - FIELD.length());
    }

    /**
     * Given array of bytes representing single message, this function splits
     * it into several messages and prepends header to each of them. This
     * fragments can be send over network to remote end where they will be
     * reassembled.
     *
     * @param message byte array of message
     * @return array of message fragments
     */
    public byte[][] createFragments(byte[] message) {
        int fragmentCount = (int) Math.ceil(message.length / (double) fragmentSize);
        byte[][] result = new byte[fragmentCount][];

        for (int i = 0; i < fragmentCount; i++) {
            String fragNo = FIELD + i;
            fragNo = fragNo.substring(fragNo.length() - FIELD.length());
            String header = HEADER + fragNo + ":" + sid + "\n";
            int toCopy = Math.min(fragmentSize, message.length - i * fragmentSize);
            result[i] = new byte[toCopy + FORMAT.length()];
            System.arraycopy(header.getBytes(), 0, result[i], 0, header.length());
            System.arraycopy(message, i * fragmentSize,
                    result[i], header.length(),
                    toCopy);
        }
        return result;
    }

    /**
     * Given array of fragments received, this function merges them into
     * original messages. If fragments of several messages are passed to
     * function, they are sorted, so they don't mix. Array of reassembled
     * messages is returned.
     *
     * @param fragments array of fragments to be reassebled
     * @return assembled messages
     */
    public byte[][] mergeFragments(byte[][] fragments) {

        Collection<byte[][]> values = sortBySid(fragments);
        byte[][] result = new byte[values.size()][];
        int resptr = 0;
        for (byte[][] messageFragments : values) {
            byte[] message = new byte[10000];
            int pos = 0;
            for (byte[] singleFragment : messageFragments) {
                if (singleFragment == null) {
                    continue;
                }
                System.arraycopy(singleFragment, 0, message, pos, singleFragment.length);
                pos += singleFragment.length;
            }
            result[resptr] = new byte[pos];
            System.arraycopy(message, 0, result[resptr], 0, pos);
            resptr++;
        }
        return result;
    }

    /**
     * This method sorts messages by their session id. Collection which entries
     * correspond to fragments belonging together is returned.
     *
     * @param fragments fragments to be sorted
     * @return collection of fragments divided into groups by their session id
     */
    private Collection<byte[][]> sortBySid(byte[][] fragments) {

        HashMap<String, byte[][]> m = new HashMap<String, byte[][]>();
        for (int i = 0; i < fragments.length; i++) {
            String header = new String(fragments[i], 0, FORMAT.length());
            String[] fields = header.split(":");

            byte[] fragment = new byte[fragments[i].length - FORMAT.length()];
            System.arraycopy(fragments[i], FORMAT.length(), fragment, 0, fragment.length);

            byte[][] entry;
            if (m.containsKey(fields[2])) {
                entry = m.get(fields[2]);
            } else {
                entry = new byte[100][];
                m.put(fields[2], entry);
            }
            entry[Integer.parseInt(fields[1].trim())] = fragment;
        }
        return m.values();
    }
}
