/**
 * license (MIT) Copyright Nubisa Inc. 2014
 */
package jxm;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.NoRouteToHostException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.Date;


public class PListen extends Thread {

    public static final String ENCODING = "UTF8";

    public final static String CreateText(Client client, String mess, boolean in) {
        if (client.getEncrypted()) {
            if(in){
                mess = new String(Base64.decode(mess));
            }
        }

        if (in) {
            // server sends unescaped
            // mess = unescape(mess);
            mess = mess.replace("\r", "\\r").replace("\n", "\\n");
        } else {
            mess = mess.replace("\r", "\\r").replace("\n", "\\n");
            // escape() is not really compatible
            //mess = escape(mess);
            mess = encodeUriComponent(mess);
        }

        if (client.getEncrypted()) {
            if(!in){
                mess = Base64.encode(mess.getBytes());
            }
        }

        return mess;
    }

    public static String escape(String str1) {
        final String str2 = "0123456789ABCDEF";
        final int length = str1.length();
        StringBuilder stringBuilder = new StringBuilder(length * 2);
        int index = -1;
        while (++index < length) {
            char ch = str1.charAt(index);
            int num = (int) ch;
            if ((97 > num || num > 122) && (65 > num || num > 90)
                    && ((48 > num || num > 57) && ((int) ch != 64 && (int) ch != 42))
                    && ((int) ch != 95 && (int) ch != 43 && ((int) ch != 45 && (int) ch != 46) && (int) ch != 47)) {
                stringBuilder.append('%');
                if (num < 256) {
                    stringBuilder.append(str2.charAt(num / 16));
                    ch = str2.charAt(num % 16);
                } else {
                    stringBuilder.append('u');
                    stringBuilder.append(str2.charAt((num >> 12) % 16));
                    stringBuilder.append(str2.charAt((num >> 8) % 16));
                    stringBuilder.append(str2.charAt((num >> 4) % 16));
                    ch = str2.charAt(num % 16);
                }
            }
            stringBuilder.append(ch);
        }
        return stringBuilder.toString();
    }

    private static int HexDigit(char c) {
        if ((int) c >= 48 && (int) c <= 57) return (int) c - 48;
        if ((int) c >= 65 && (int) c <= 70) return 10 + (int) c - 65;
        if ((int) c >= 97 && (int) c <= 102) return 10 + (int) c - 97;

        return -1;
    }

    public static String encodeUriComponent(String string) {
    	String result = null;

        try
        {
          result = URLEncoder.encode(string, "UTF-8");
          String[] src = { "\\+", "\\%21", 	"\\%27", 	"\\%28", 	"\\%29", 	"\\%7E" };	
          String[] dst = { "%20", "!", 		"'", 		"(", 		")", 		"~" };
          int ln = src.length;
          for (int i = 0; i < ln; i++) {
        	  result = result.replaceAll(src[i], dst[i]);
          }
        } 
        catch (Exception ex)
        {
          result = string;
        }

        return result;
    }

    public static String unescape(String str) {
        int length = str.length();
        StringBuilder stringBuilder = new StringBuilder(length);
        int index = -1;
        while (++index < length) {
            char ch = str.charAt(index);
            if ((int) ch == 37) {
                int num1;
                int num2;
                int num3;
                int num4;
                if (index + 5 < length && (int) str.charAt(index + 1) == 117
                        && ((num1 = HexDigit(str.charAt(index + 2))) != -1 && (num2 = HexDigit(str.charAt(index + 3))) != -1)
                        && ((num3 = HexDigit(str.charAt(index + 4))) != -1 && (num4 = HexDigit(str.charAt(index + 5))) != -1)) {
                    ch = (char) ((num1 << 12) + (num2 << 8) + (num3 << 4) + num4);
                    index += 5;
                } else {
                    int num5;
                    int num6;
                    if (index + 2 < length && (num5 = HexDigit(str.charAt(index + 1))) != -1
                            && (num6 = HexDigit(str.charAt(index + 2))) != -1) {
                        ch = (char) ((num5 << 4) + num6);
                        index += 2;
                    }
                }
            }
            stringBuilder.append(ch);
        }
        return stringBuilder.toString();
    }

    private InputStream activeStream = null;

    private Client client;

    private String errorMessage = "";

    public boolean exit = false;

    private boolean isDisposing = false;

    public PEvents notifier = null;

    private Socketer socketConn = null;

    private boolean socketConnected = false;
    private boolean socketConnecting = false;

    public boolean socketDisabled = false;

    public PListen(final String name, final Client pClient) {
        super(pClient.GetClientId() + name);
        client = pClient;
    }

    public void Dispose() {
        client.fireLog("Disposing Listener", LogLevel.Critical);
        exit = true;
        if (socketConnected || socketConnecting) {
            try {
                socketConn.close();
            } catch (Exception e) {
            }
        }
        socketConnected = false;
        socketDisabled = false;
        socketConnecting = false;
        isDisposing = true;
        try {
            activeStream.close();
        } catch (Exception e) {
        }
    }

    public String downloadString(final String connStr) {
        InputStream stream = null;
        try {
            URL url = new URL(connStr);
            stream = url.openStream();
            activeStream = stream;
        } catch (Exception e) {
            errorMessage = e.getClass().getName() + " : " + e.getMessage();
            fire(null, true);
            client.goClose();
            return null;
        }


        InputStreamReader isr = new InputStreamReader(stream);
        BufferedReader rs = new BufferedReader(isr);
        StringBuilder str = new StringBuilder();

        try {
            String st;
            while ((st = rs.readLine()) != null) {
                byte[] data = st.getBytes();
                str.append(new String(data, 0, data.length, ENCODING));
            }
            stream.close();
        } catch (Exception ex) {
            if (isDisposing)
                return "";

            errorMessage = "Error during download:" + ex.getMessage();
            fire(null, true);
            return null;
        }

        return str.toString();
    }

    private static String uid = null;

    private static long uidc = 0;

    public static String getUID(boolean reset) {  //TODO: update here from server side
        if (uid == null || reset)
            uid = java.util.UUID.randomUUID().toString().replace("-","") + (uidc++);

        return uid;
    }

    public String downloadString(final String connStr, final String params) {
        URL url;
        URLConnection conn;
        try {
            url = new URL(connStr);
            conn = url.openConnection();

            conn.setDoOutput(true);

            OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream());
            writer.write(params);
            writer.flush();

            InputStreamReader irs = new InputStreamReader(conn.getInputStream());
            BufferedReader in = new BufferedReader(irs);
            StringBuilder str = new StringBuilder();

            String st;
            while ((st = in.readLine()) != null) {
                byte[] data = st.getBytes();
                str.append(new String(data, 0, data.length, ENCODING));
            }

            writer.close();
            in.close();

            return str.toString();
        } catch (NoRouteToHostException ne) {
            errorMessage = "connection to host is lost";
            fire(null, true);
            notifier.UpdateIsConnected(false);
            return null;
        } catch (Exception e) {
            errorMessage = e.getClass().getName() + " : " + e.getMessage();
            fire(null, true);
            return null;
        }
    }

    private void fire(String msg, boolean isError) {
        if (notifier != null) {
            if (isError) {
                notifier.ErrorReceived(errorMessage);
            } else {
                notifier.MessageReceived(msg);
            }
        }
    }

    public String getErrorMessage() {
        return errorMessage;
    }

    public boolean getSocketConnected() {
        return socketConnected;
    }

    public void ListenSocket() throws Exception {
        if (socketConnected || socketConnecting)
            return;

        int port = client.getSocketPort();

        if(client.getSecuredKey() == null)
            return;

        String url = "ws://" + client.getSocketURL() + ":" + port + "/" + client.getApplicationPath()+ "/jx?c=" + client.GetClientId()
                +"&sid="+client.getSecuredKey()+"&de=1";

        URI uri = new URI(url);
        socketConn = new Socketer(uri);
        socketConnecting = true;

        socketConn.events = new SocketEvents() {
            @Override
            public void close() {
                socketConnected = false;
                socketConnecting = false;
                client.fireLog("Socket Closed", LogLevel.Critical);
                client.goClose();
            }

            @Override
            public void error(Exception e) {
                errorMessage = "SocketError:" + e.getMessage();
                fire(null, true);
                //client.Call("71n9", null, null);
            }

            @Override
            public void message(String message) {
                client.fireLog("Socket Received:" + message, LogLevel.Informative);
                message = PListen.CreateText(client, message, true);
                fire(message, false);
            }

            @Override
            public void open() {
                socketConnected = true;
                socketConnecting = false;
                client.fireLog("Socket Connected", LogLevel.Informative);
                if(client.Events!=null){
                    client.Events.OnConnect(client);
                }
            }
        };

        socketConn.connect();
    }

    public void Reload() {
        client.fireLog("Reloading Client (not suggested! instead recreate DesktopClient instance)", LogLevel.Critical);
        isDisposing = false;
    }

    @Override
    public void run() {

        isDisposing = false;
        exit = false;

        while (!exit) {
            try {
                Thread.sleep(25);
            } catch (Exception e) {
                break;
            }

            if (this.socketConnected || this.isDisposing) {
                continue;
            }

            boolean hasListen = false;

            if (client.getSocketURL() != null && !socketDisabled && !isDisposing) {
                hasListen = true;
            }
            else{
                String connStr = client.getSocketURL().concat(":" + client.getSocketPort() + "/" + client.getApplicationPath() + "/jx?");
                if(client.getIsSecure()){
                    connStr = "https://" + connStr;
                }else{
                    connStr = "http://" + connStr;
                }

                if(client.getSecuredKey() == null)
                    return;

                connStr = connStr.concat("c=" + client.GetClientId() + "&de=1&sid="+client.getSecuredKey()+"&co=" + ((Long) (new Date().getTime())).toString());
                //client.fireLog("HTTP DownloadStart "+connStr, LogLevel.Informative);
                String message = downloadString(connStr);
                //	client.fireLog("HTTP DownladEnd", LogLevel.Informative);
                if (message == null) {
                    continue;
                }

                client.fireLog("HTTP Received:" + message, LogLevel.Informative);
                if (isDisposing) {
                    continue;
                }

                if (!client.getIsConnected() && !isDisposing) {
                    continue;
                }

                String[] messages;
                if (client.getEncrypted()) {
                    messages = message.split(";");
                    final int ln = messages.length;
                    for (int i = 0; i < ln; i++) {
                        if (messages[i] != null) {
                            try {
                                messages[i] = PListen.CreateText(client, messages[i].trim(), true);
                            } catch (Exception e) {
                            }
                        }
                    }
                } else {
                    messages = new String[1];
                    try {
                        messages[0] = PListen.CreateText(client, message.trim(), true);
                    } catch (Exception e) {
                    }
                }

                for (String mess : messages) {
                    if (mess == null)
                        continue;

                    if (!hasListen) {
                        hasListen = mess.contains("jxcore.Listen");
                    }
                    fire(mess, false);
                }
            }

            if (hasListen) {
                if (client.getSocketURL() != null && !socketDisabled && !isDisposing) {
                    try {
                        if (!socketEnabled())
                            this.ListenSocket();
                    } catch (Exception e) {
                        socketDisabled = true;
                        errorMessage = "Socket Connection Error (" + e.getMessage() + ") Socket connection disabled.";
                        fire(null, true);
                    }
                }
                continue;
            } else {
                if (socketConnected || socketConnecting){}else{
                    notifier.UpdateIsConnected(false);
                    break;
                }
            }
        }
    }

    public boolean socketEnabled() {
        return (this.socketConnected);
    }

    public boolean socketSend(String msg) {
        if (socketConn == null)
            return false;
        try {
            socketConn.send(msg);
        } catch (Exception e) {
            errorMessage = "socketSend Error:" + e.getMessage();
            fire(null, true);
            return false;
        }

        return true;
    }
}
