package com.wecise.odb;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.stream.JsonReader;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.HashMap;

public class Message {
    private String seq;
    private int mtype;
    private String uri;
    private HashMap<String, String> meta;
    private int body_codec;
    private String body;

    public Message(String seq, int mtype, String uri, HashMap<String, String> meta, int bodyCodec, String body) {
        this.seq = seq;
        this.mtype = mtype;
        this.uri = uri;
        this.meta = meta;
        this.body_codec = bodyCodec;
        this.body = body;
    }

    public Message() {

    }

    /*
	  {length bytes} {xfer_pipe length byte} {xfer_pipe bytes} {JSON bytes}

	  {length bytes}: uint32, 4 bytes, big endian
	  {xfer_pipe length byte}: 1 byte
	  {xfer_pipe bytes}: one byte one xfer
	  {JSON bytes}: {"seq":%d,"mtype":%d,"uri":%q,"meta":%q,"body_codec":%d,"body":"%s"}
	 */
    public byte[] encode() {
        StringBuilder meta = new StringBuilder();
        for (String key : this.meta.keySet()) {
            if (!meta.toString().equals("")) {
                meta.append("&");
            }
            meta.append(String.format("%s=%s", key, this.meta.get(key)));
        }

        HashMap msg = new <String, Object>HashMap();
        msg.put("seq", this.seq);
        msg.put("mtype", this.mtype);
        msg.put("uri", this.uri);
        msg.put("body_codec", this.body_codec);
        msg.put("body", this.body);
        msg.put("meta", meta.toString());

        byte[] content = new Gson().toJson(msg).getBytes();
        byte[] xferBytes = new byte[0]; // no filter now

        int size = 1 + xferBytes.length + content.length;
        byte[] b =  new byte[4 + size];
        // lenght bytes copy into total bytes. golang: binary.BigEndian.PutUint32(b, size)
        byte[] lengthBytes = Util.intToBytesBig(size);
        System.arraycopy(lengthBytes, 0, b, 0, 4);
        b[4] = (byte)xferBytes.length; // golang: b[4] = byte(len(xferBytes))
        System.arraycopy(xferBytes, 0, b, 4 + 1, xferBytes.length); // golang: copy(b[4+1:], xferBytes)
        System.arraycopy(content, 0, b, 4 + 1 + xferBytes.length, content.length); // golang: copy(b[4+1+len(xferBytes):], content)

        return b;
    }

    /* golang implements:
    b := make([]byte, 2048)

	message := new(bytes.Buffer)
	var isMessage bool
	var size uint32
	var xferLen uint32
	var remaining int
	for {
		if !isMessage { // read data length
			n, err := conn.Read(b)
			if err != nil {
				log.Fatal(err)
			}
			buf := bytes.NewBuffer(b[:4])
			//var size uint32
			err = binary.Read(buf, binary.BigEndian, &size)
			if err != nil {
				log.Fatal(err)
			}
			//log.Println("total:", size, "read:", n)

			buf.Reset()
			buf.Write(b[4:5]) // xfer length
			xferLen = uint32(buf.Bytes()[0])

			var xfers []rune // xfer does not use
			buf.Reset()
			buf.Write(b[5:5+xferLen]) // xfer
			for _, bt := range buf.Bytes() {
				xfers = append(xfers, rune(bt))
			}

			message.Write(b[5+xferLen : n]) // data
			isMessage = true // next read message
		} else {
			if int(remaining) < len(b) {
				b = make([]byte, remaining)
			}
			n, err := conn.Read(b)
			if err != nil {
				log.Fatal(err)
			}
			message.Write(b[:n])
		}

		remaining = int(size) - 1 - int(xferLen) - message.Len()
		log.Println("length:", size, "read:", message.Len(), "remaining:", remaining)
		if remaining <= 0 {
			break
		}
	}
	log.Println("Recv:", message.String())
    */
    public void decode(InputStream inputStream) {
        byte[] b = new byte[2048];

        byte[] message = new byte[0];
        boolean isMessage = false;
        int size = 0;
        int xferLen = 0;
        int remaining = 0;
        while (true) {
            try {
                if (!isMessage) {
                    int n = inputStream.read(b);
                    size = Util.bytesToIntBig(Util.subBytes(b, 0, 4), 0);

                    xferLen = (int)Util.subBytes(b, 4, 1)[0]; // Java default is big endian
                    char[] xfers = new char[xferLen];
                    byte[] xferBytes = Util.subBytes(b, 5, xferLen);
                    for (int i = 0; i < xferBytes.length; i++) {
                        xfers[i] = (char) xferBytes[i];
                    }

                    message = Util.bytesMerger(message, Util.subBytes(b, 5 + xferLen, n - 5 - xferLen)); // data

                    isMessage = true;
                } else {
                    if (remaining < b.length) {
                        b = new byte[remaining];
                    }
                    int n = inputStream.read(b);
                    if (n < b.length) {
                        message = Util.bytesMerger(message, Util.subBytes(b, 0, n)); // data
                    } else {
                        message = Util.bytesMerger(message, b);
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
                break;
            }

            remaining = size -1 -xferLen - message.length;
            if (remaining <= 0) {
                break;
            }
        }
        StringReader reader = new StringReader(new String(message));
        JsonReader jr = new JsonReader(reader);
        jr.setLenient(true);
        JsonElement js = new Gson().fromJson(jr, JsonElement.class);
        this.seq = js.getAsJsonObject().get("seq").getAsString();
        this.mtype = js.getAsJsonObject().get("mtype").getAsInt();
        this.uri = js.getAsJsonObject().get("uri").getAsString();
        String metaStr = js.getAsJsonObject().get("meta").getAsString();
        HashMap<String, String> meta = new HashMap<>();
        if (metaStr != null) {
            String[] a = metaStr.split("&");
            if (a.length > 0) {
                for (String s: a) {
                    if (!s.equals("")) {
                        String[] kv = s.split("=");
                        meta.put(kv[0], kv[1]);
                    }
                }
            }
        }
        this.meta = meta;
        this.body_codec = js.getAsJsonObject().get("body_codec").getAsInt();
        this.body = js.getAsJsonObject().get("body").getAsString();
    }

    public String getSeq() {
        return seq;
    }

    public void setSeq(String seq) {
        this.seq = seq;
    }

    public int getMtype() {
        return mtype;
    }

    public void setMtype(int mtype) {
        this.mtype = mtype;
    }

    public String getUri() {
        return uri;
    }

    public void setUri(String uri) {
        this.uri = uri;
    }

    public HashMap<String, String> getMeta() {
        return meta;
    }

    public void setMeta(HashMap<String, String> meta) {
        this.meta = meta;
    }

    public int getBody_codec() {
        return body_codec;
    }

    public void setBody_codec(int body_codec) {
        this.body_codec = body_codec;
    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }
}
