/*
 * Copyright (C) 2013 Yoshinori Hayakawa <hayakawa@cite.tohoku.ac.jp>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package turtlemessenger;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import static java.lang.Integer.MAX_VALUE;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

/**
 *
 * @author Yoshinori Hayakawa <hayakawa@cite.tohoku.ac.jp>
 */



public class TMReceiver {
    private TMController controller ;
    
    private ServerSocket serverSock ;
    private int serverPort ;
    private final int BUFFSIZE = 8192 ;
    private String myPasscode ;
    
    private List<FeedbackMessage> clientMessages  ;
    ReceiverThread receiver ;
    private boolean receiverIsTerminating ;
    
    private int lastReadPosition ;
    
    TMReceiver(TMController parent, int port) {
        controller = parent ;
        serverPort = port ;
        myPasscode = "" ;
        
        clientMessages = new ArrayList<FeedbackMessage>();
        receiver = null ;
        receiverIsTerminating=false ;
        lastReadPosition = 0 ;
    }
    
    public String messageAt(int i, boolean privacyMode) {
        if (i<0 || i>= clientMessages.size()) return null ;
        FeedbackMessage fm = clientMessages.get(i) ;
        StringBuilder strBuf = new StringBuilder("") ;
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy'/'MM'/'dd HH:mm:ss");
        strBuf.append(dateFormat.format(fm.time) + "\t") ;
        if (!privacyMode) {
            strBuf.append(fm.userid + "\t") ;
            strBuf.append(fm.hostname + "\t") ;
        }
        strBuf.append(fm.nickname + "\t") ;
        strBuf.append(fm.choice + "\t") ;
        strBuf.append(fm.message + "\t") ; 
        strBuf.append(fm.error + "\t") ;
        strBuf.append("\n") ;
        return strBuf.toString() ;
    }
    
    private static String escapeHTML(String str) {
        char c1, c2, c3, c4;
        StringBuffer strBuf = new StringBuffer("");
        for (int i = 0; i < str.length(); i++) {
            c1 = str.charAt(i);
            if (i < str.length() - 3) {
                c2 = str.charAt(i + 1);
                c3 = str.charAt(i + 2);
                c4 = str.charAt(i + 3);
            } else {
                c2 = c3 = c4 = c1 ;
            }
            if (c1 == '<' && (c2 == 'b' || c2 == 'B') && (c3 == 'r' || c3 == 'R') && c4 == '>') {
                strBuf.append(c1);
                strBuf.append(c2);
                strBuf.append(c3);
                strBuf.append(c4);
                i = i + 3; // should be +3 not + 4
                continue;
            } else {
                if (c1 == '"' || c1 == '<' || c1 == '>') {
                    strBuf.append("&#" + (int) c1 + ";");
                } else {
                    strBuf.append(c1);
                }
            }
        }
        return strBuf.toString();
    }
    
           public String getMessageInJSON_Id(int id) {
        StringBuilder strBuf = new StringBuilder("");
        try {
            FeedbackMessage fm = clientMessages.get(id);
            strBuf.append("{");
            strBuf.append("\"id\":" + id + ",");
            String[] ds = fm.time.toString().split(" "); // 0:"dow" 1:"mon" 2:"dd" 3:"hh:mm:ss" 4:"zzz" 5:"yyyy"
            String timeStr = ds[3];
            strBuf.append("\"time\":\"" + timeStr + "\",");
            strBuf.append("\"nickname\":\"" + escapeHTML(fm.nickname) + "\",");
            strBuf.append("\"message\":\"" + escapeHTML(fm.message) + "\",");
            strBuf.append("\"error\":\"" + escapeHTML(fm.error) + "\"");
            strBuf.append("}");
        } catch (IndexOutOfBoundsException e) {
            ; 
        }
        return strBuf.toString();
    }
    
    public String getMessagesInJSON(int nmsg) {
        boolean firstElement = true;
        StringBuilder strBuf = new StringBuilder("");
        int n = clientMessages.size();
        if (nmsg <= 0) {
            nmsg = MAX_VALUE;
        }
        strBuf.append("[");
        for (int i = n - 1, cnt = 0; i >= 0 && cnt < nmsg; i--, cnt++) { // in reverse order 
            FeedbackMessage fm = clientMessages.get(i);
            if (fm.message.length() > 0 || fm.error.length() > 0) { // null message is omitted
                if (firstElement) {
                    firstElement = false;
                } else {
                    strBuf.append(",");
                }
                strBuf.append(getMessageInJSON_Id(i)) ;
            }
        }
        strBuf.append("]");
        return strBuf.toString();
    }
 
    private String combineMsgAndErr(String msg, String err) {
        msg = msg.replaceAll("<br>", "\n") ;
        err = err.replaceAll("<br>", "\n") ;
        if (msg.length()>0 && err.length()>0) return msg + "\n" + err ;
        else if (msg.length()>0 ) return msg ;
        else return err ;
    }
    
    public List<List<String>> getMessages(int nmsg) {
        List<List<String>> msgList = new ArrayList<List<String>>() ;
        int n = clientMessages.size();
        if (nmsg <= 0) {
            nmsg = MAX_VALUE;
        }
        for (int i = n - 1, cnt = 0; i >= 0 && cnt < nmsg; i--, cnt++) { // in reverse order 
            FeedbackMessage fm = clientMessages.get(i);
            if (fm.message.length() > 0 || fm.error.length()>0) { // null message is omitted
                List<String> row = new ArrayList<String>() ; 
                String[] ds = fm.time.toString().split(" "); // 0:"dow" 1:"mon" 2:"dd" 3:"hh:mm:ss" 4:"zzz" 5:"yyyy"
                String timeStr = ds[3];
                row.add(timeStr) ;
                row.add(fm.nickname) ;
                row.add(combineMsgAndErr(fm.message,fm.error)) ;
                msgList.add(row) ;
            }
        }
        return msgList ;
    }

    public List<List<String>> getNewMessages() {
        List<List<String>> msgList = new ArrayList<List<String>>() ;
        int n = clientMessages.size();
        for (int i = lastReadPosition; i < n; i++) {
            FeedbackMessage fm = clientMessages.get(i);
            if (fm.message.length() > 0 || fm.error.length()>0) { // null message is omitted
                List<String> row = new ArrayList<String>() ;            
                String[] ds = fm.time.toString().split(" "); // 0:"dow" 1:"mon" 2:"dd" 3:"hh:mm:ss" 4:"zzz" 5:"yyyy"
                String timeStr = ds[3];
                row.add(timeStr) ;
                row.add(fm.nickname) ;
                row.add(combineMsgAndErr(fm.message,fm.error)) ;
                msgList.add(row) ;
            }
        }
        lastReadPosition = n;
        return msgList;
    }
    
    public void setMyPasscode(String s) {
        myPasscode = s.trim() ;
    }
    
    public void setReceiverPort(int port) {
        serverPort = port ;
    }
    
    public void terminateReceiver() {
        if (receiver==null) return ;
        receiverIsTerminating=true ;
        do {
            try {
                Thread.sleep(200) ;
            } catch (InterruptedException ex) {
                 ;
            }
        } while (receiverIsTerminating) ;
        receiver = null ;
    }
    
    public boolean hasMessage() {
        if (clientMessages.size()>0) return true ;
        else return false ;
    }
    
    public void startReceiver() {
        if (receiver!=null && receiver.isAlive()) return ;  
        receiverIsTerminating=false ;
        receiver = new ReceiverThread() ;
        receiver.start() ;
    }
    
    private int byte2int(byte c)
    {
        int r=0 ;
        if (c >= '0' && c<= '9') r = c - '0' ;
        else if (c>='a' && c<='f') r = c - 'a' + 10 ;
        else if (c>='A' && c<='F') r = c - 'A' + 10 ;
        return r ;
    }

    public byte[] decodeHexByte(String hex) {
        int cnt ; int d0=0,d1=0 ;
        ByteArrayOutputStream bao = new ByteArrayOutputStream() ;
        byte[] bytes = hex.getBytes() ;
        cnt=0 ;
        for (byte b : bytes) {
            if (b==' ') continue ;
            if (cnt%2==0) d1 = byte2int(b) ;
            else {
                d0 = byte2int(b) ;
                bao.write(d1*16 + d0) ;
            }
            cnt++ ;
        }
        return bao.toByteArray() ;
    }
           
    
    public String decryptShortMessage(String hex) {
        byte[] data = decodeHexByte(hex) ;
        Key key = controller.privateKey ;
        
        byte[] decrypted = null;

        try {
            Cipher cipher = Cipher.getInstance("RSA");
            cipher.init(Cipher.DECRYPT_MODE, key);
            decrypted = cipher.doFinal(data);
        } catch (NoSuchAlgorithmException e) {
           ;
        } catch (NoSuchPaddingException e) {
           ;
        } catch (BadPaddingException e) {
           ;
        } catch (InvalidKeyException e) {
           ;
        } catch (IllegalBlockSizeException e) {
           ;
        }
        try {
            if (decrypted!=null) return new String(decrypted,"UTF-8");
            else return "?" ;
        } catch (UnsupportedEncodingException ex) {
            return "?" ;
        }
        
    }
    
    public class FeedbackMessage {
        public Date time ;
        public int choice ;
        public String message ;
        public String error ;
        public String nickname ;
        public String userid ;
        public String hostname ;
        FeedbackMessage(String host,  String uid, String handle, int c, String msg, String err) {
            choice = c ;
            message = msg ;
            error = err ;
            userid = uid ;
            nickname = handle ;
            hostname = host ;
            time = new Date() ;
        }
    }
    
    public class ReceiverThread extends Thread {

        ReceiverThread() {
            ;
        }

        @Override
        public void run() {            
            BufferedReader reader;
            OutputStreamWriter writer;
            String regx = "^(.+?)=(.*)$" ;
            Pattern pat = Pattern.compile(regx) ;
            
            try {
                serverSock = new ServerSocket();
                serverSock.setReuseAddress(true); 
                serverSock.setSoTimeout(10);
                serverSock.bind(new InetSocketAddress(serverPort)); 
                
                byte[] receiveBuf = new byte[BUFFSIZE];
                for (;;) {
                    int status = 0 ;
                    int choice=0;
                    String passcode="" ;
                    String message="";
                    String errmsg = "";
                    String nickname="";
                    String userid="";
                    String hostname="";
                    Socket clientSock=null ;
                    try {
                        clientSock = serverSock.accept();
                    } catch (SocketTimeoutException e) {
                        if (receiverIsTerminating) {
                            serverSock.close() ;
                            // System.err.println("RECEIVER TERMINATED") ;
                            controller.receiverTerminated() ;
                            receiverIsTerminating=false ;
                            return ;
                        }
                        continue ;
                    }
                    
                    SocketAddress clientAddress = clientSock.getRemoteSocketAddress();
                    //  System.out.println("接続中：" + clientAddress);

                    reader = new BufferedReader(
                            new InputStreamReader(clientSock.getInputStream(), "utf-8"));
                    writer = new OutputStreamWriter(clientSock.getOutputStream(),"utf-8");
                    
                    String line;
                    while ((line = reader.readLine()) != null) {                        
                        Matcher mat = pat.matcher(line) ;
                        if (mat.find()) {
                            String key = mat.group(1);
                            String value = mat.group(2);
                            //// System.out.println(key + " " + value + "\n") ;
                            if (key.equals("passcode")) {
                                passcode = decryptShortMessage(value) ;
                                if (myPasscode.length()==0 || myPasscode.equalsIgnoreCase("any") || myPasscode.equals("*")) status = 200 ;
                                else if (passcode.equals(myPasscode)) status = 200 ;
                                else status = 300 ;
                                
                            } else if (key.equals("choice")) {
                                choice = Integer.valueOf(value) ;
                            } else if (key.equals("message")) {
                                message = value ;
                            } else if (key.equals("nickname")) {
                                nickname = value ;
                            } else if (key.equals("userid")) {
                                userid = decryptShortMessage(value) ;
                            } else if (key.equals("hostname")) {
                                hostname = decryptShortMessage(value) ;
                            } else if (key.equals("errmsg")) {
                                errmsg = value;
                            }
                        }
                    }

                    if (status == 200) {
                        writer.write(java.util.ResourceBundle.getBundle("turtlemessenger/TMBundle").getString("MESSAGE ACCEPTED") + "\r\n");
                        // record client_msg 
                        FeedbackMessage fm = new FeedbackMessage(hostname, userid, nickname, choice, message, errmsg) ;
                        clientMessages.add(fm) ;
                        controller.appendLog(messageAt(clientMessages.size()-1,false));
                        controller.votedTo(userid,choice) ;
                    } else {
                        writer.write(java.util.ResourceBundle.getBundle("turtlemessenger/TMBundle").getString("CANNOT SEND YOUR FEEDBACK") + "\r\n");
                    }
                    writer.flush();
                    clientSock.shutdownOutput();                  
                    clientSock.close(); 
                }
            } catch (IOException ex) {
                controller.appendLogWithTimestamp("#\tI/O ERROR" + "\n") ;
                Logger.getLogger(TMReceiver.class.getName()).log(Level.SEVERE, null, ex);
            }
            try {
                serverSock.close() ;
            } catch (IOException ex) {
                controller.appendLogWithTimestamp("#\tSOCKET ERROR" + "\n") ;
            }
            // System.err.println("RECEIVER TERMINATED") ;
            controller.receiverTerminated() ;
            receiverIsTerminating=false ;
        }
        
    }
    
}
