package com.github.nkzawa.socketio.parser;

import com.github.nkzawa.emitter.Emitter;
import org.json.JSONException;
import org.json.JSONTokener;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;

public class Parser {

    private static final Logger logger = Logger.getLogger(Parser.class.getName());

    /**
     * Packet type `connect`.
     */
    public static final int CONNECT = 0;

    /**
     * Packet type `disconnect`.
     */
    public static final int DISCONNECT = 1;

    /**
     * Packet type `event`.
     */
    public static final int EVENT = 2;

    /**
     * Packet type `ack`.
     */
    public static final int ACK = 3;

    /**
     * Packet type `error`.
     */
    public static final int ERROR = 4;

    /**
     * Packet type `binary event`.
     */
    public static final int BINARY_EVENT = 5;

    /**
     * Packet type `binary ack`.
     */
    public static final int BINARY_ACK = 6;

    public static int protocol = 4;

    /**
     * Packet types.
     */
    public static String[] types = new String[] {
        "CONNECT",
        "DISCONNECT",
        "EVENT",
        "BINARY_EVENT",
        "ACK",
        "BINARY_ACK",
        "ERROR",
    };


    private Parser() {}

    private static Packet<String> error() {
        return new Packet<String>(ERROR, "parser error");
    }


    public static class Encoder {

        public Encoder() {}

        public void encode(Packet obj, Callback callback) {
            logger.fine(String.format("encoding packet %s", obj));

            if (BINARY_EVENT == obj.type || BINARY_ACK == obj.type) {
                encodeAsBinary(obj, callback);
            } else {
                String encoding = encodeAsString(obj);
                callback.call(new String[] {encoding});
            }
        }

        private String encodeAsString(Packet obj) {
            StringBuilder str = new StringBuilder();
            boolean nsp = false;

            str.append(obj.type);

            if (BINARY_EVENT == obj.type || BINARY_ACK == obj.type) {
                str.append(obj.attachments);
                str.append("-");
            }

            if (obj.nsp != null && obj.nsp.length() != 0 && !"/".equals(obj.nsp)) {
                nsp = true;
                str.append(obj.nsp);
            }

            if (obj.id >= 0) {
                if (nsp) {
                    str.append(",");
                    nsp = false;
                }
                str.append(obj.id);
            }

            if (obj.data != null) {
                if (nsp) str.append(",");
                str.append(obj.data);
            }

            logger.fine(String.format("encoded %s as %s", obj, str));
            return str.toString();
        }

        private void encodeAsBinary(Packet obj, Callback callback) {
            Binary.DeconstructedPacket deconstruction = Binary.deconstructPacket(obj);
            String pack = encodeAsString(deconstruction.packet);
            List<Object> buffers = new ArrayList<Object>(Arrays.asList(deconstruction.buffers));

            buffers.add(0, pack);
            callback.call(buffers.toArray());
        }

        public interface Callback {

            public void call(Object[] data);
        }
    }

    public static class Decoder extends Emitter {

        public static String EVENT_DECODED = "decoded";

        /*package*/ BinaryReconstructor reconstructor;

        public Decoder() {
            this.reconstructor = null;
        }

        public void add(String obj) {
            Packet packet = decodeString(obj);
            if (BINARY_EVENT == packet.type || BINARY_ACK == packet.type) {
                this.reconstructor = new BinaryReconstructor(packet);

                if (this.reconstructor.reconPack.attachments == 0) {
                    this.emit(EVENT_DECODED, packet);
                }
            } else {
                this.emit(EVENT_DECODED, packet);
            }
        }

        public void add(byte[] obj) {
            if (this.reconstructor == null) {
                throw new RuntimeException("got binary data when not reconstructing a packet");
            } else {
                Packet packet = this.reconstructor.takeBinaryData(obj);
                if (packet != null) {
                    this.reconstructor = null;
                    this.emit(EVENT_DECODED, packet);
                }
            }
        }

        public static Packet decodeString(String str) {
            Packet p = new Packet();
            int i = 0;

            p.type = Character.getNumericValue(str.charAt(0));
            if (p.type < 0 || p.type > types.length - 1) return error();

            if (BINARY_EVENT == p.type || BINARY_ACK == p.type) {
                StringBuilder attachments = new StringBuilder();
                while (str.charAt(++i) != '-') {
                    attachments.append(str.charAt(i));
                }
                p.attachments = Integer.parseInt(attachments.toString());
            }

            if (str.length() > i + 1 && '/' == str.charAt(i + 1)) {
                StringBuilder nsp = new StringBuilder();
                while (true) {
                    ++i;
                    char c = str.charAt(i);
                    if (',' == c) break;
                    nsp.append(c);
                    if (i + 1 == str.length()) break;
                }
                p.nsp = nsp.toString();
            } else {
                p.nsp = "/";
            }

            Character next;
            try {
                next = str.charAt(i + 1);
            } catch (IndexOutOfBoundsException e) {
                next = Character.UNASSIGNED;
            }
            if (Character.UNASSIGNED != next && Character.getNumericValue(next) > -1) {
                StringBuilder id = new StringBuilder();
                while (true) {
                    ++i;
                    char c = str.charAt(i);
                    if (Character.getNumericValue(c) < 0) {
                        --i;
                        break;
                    }
                    id.append(c);
                    if (i + 1 == str.length()) break;
                }
                p.id = Integer.parseInt(id.toString());
            }

            try {
                str.charAt(++i);
                p.data = new JSONTokener(str.substring(i)).nextValue();
            } catch (IndexOutOfBoundsException e) {
                // do nothing
            } catch (JSONException e) {
                return error();
            }

            logger.fine(String.format("decoded %s as %s", str, p));
            return p;
        }

        public void destroy() {
            if (this.reconstructor != null) {
                this.reconstructor.finishReconstruction();
            }
        }
    }


    /*package*/ static class BinaryReconstructor {

        public Packet reconPack;

        /*package*/ List<byte[]> buffers;

        BinaryReconstructor(Packet packet) {
            this.reconPack = packet;
            this.buffers = new ArrayList<byte[]>();
        }

        public Packet takeBinaryData(byte[] binData) {
            this.buffers.add(binData);
            if (this.buffers.size() == this.reconPack.attachments) {
                Packet packet = Binary.reconstructPacket(this.reconPack,
                        this.buffers.toArray(new byte[this.buffers.size()][]));
                this.finishReconstruction();
                return packet;
            }
            return null;
        }

        public void finishReconstruction () {
            this.reconPack = null;
            this.buffers = new ArrayList<byte[]>();
        }
    }
}



@@ -4469,37 +4469,38 @@
     %7D%0A%0A        p
-ublic
+rivate
  static Packet d

add a test