// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.consoleproxy;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.cloud.consoleproxy.util.Logger;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;

public class ConsoleProxyAjaxHandler implements HttpHandler {
    private static final Logger s_logger = Logger.getLogger(ConsoleProxyAjaxHandler.class);
    
    public ConsoleProxyAjaxHandler() {
    }
    
    public void handle(HttpExchange t) throws IOException {
        try {
            if(s_logger.isTraceEnabled())
                s_logger.trace("AjaxHandler " + t.getRequestURI());
            
            long startTick = System.currentTimeMillis();
            
            doHandle(t);
            
            if(s_logger.isTraceEnabled())
                s_logger.trace(t.getRequestURI() + " process time " + (System.currentTimeMillis() - startTick) + " ms");
        } catch (IOException e) {
            throw e;
        } catch (IllegalArgumentException e) {
            s_logger.warn("Exception, ", e);
            t.sendResponseHeaders(400, -1);     // bad request
        } catch(Throwable e) {
            s_logger.error("Unexpected exception, ", e);
            t.sendResponseHeaders(500, -1);     // server error
        } finally {
            t.close();
        }
    }
    
    private void doHandle(HttpExchange t) throws Exception, IllegalArgumentException {
        String queries = t.getRequestURI().getQuery();
        if(s_logger.isTraceEnabled())
            s_logger.trace("Handle AJAX request: " + queries);
        
        Map<String, String> queryMap = ConsoleProxyHttpHandlerHelper.getQueryMap(queries);
        
        String host = queryMap.get("host");
        String portStr = queryMap.get("port");
        String sid = queryMap.get("sid");
        String tag = queryMap.get("tag");
        String ticket = queryMap.get("ticket");
        String ajaxSessionIdStr = queryMap.get("sess");
        String eventStr = queryMap.get("event");
        String console_url = queryMap.get("consoleurl");
        String console_host_session = queryMap.get("sessionref");
        String vm_locale = queryMap.get("locale");

        if(tag == null)
            tag = "";
        
        long ajaxSessionId = 0;
        int event = 0;
        
        int port;

        if(host == null || portStr == null || sid == null) 
            throw new IllegalArgumentException();
        
        try {
            port = Integer.parseInt(portStr);
        } catch (NumberFormatException e) {
            s_logger.warn("Invalid number parameter in query string: " + portStr);
            throw new IllegalArgumentException(e);
        }

        if(ajaxSessionIdStr != null) {
            try {
                ajaxSessionId = Long.parseLong(ajaxSessionIdStr);
            } catch (NumberFormatException e) {
                s_logger.warn("Invalid number parameter in query string: " + ajaxSessionIdStr);
                throw new IllegalArgumentException(e);
            }
        }

        if(eventStr != null) {
            try {
                event = Integer.parseInt(eventStr);
            } catch (NumberFormatException e) {
                s_logger.warn("Invalid number parameter in query string: " + eventStr);
                throw new IllegalArgumentException(e);
            }
        }
        
        ConsoleProxyClient viewer = null;
        try {
            ConsoleProxyClientParam param = new ConsoleProxyClientParam();
            param.setClientHostAddress(host);
            param.setClientHostPort(port);
            param.setClientHostPassword(sid);
            param.setClientTag(tag);
            param.setTicket(ticket);
            param.setClientTunnelUrl(console_url);
            param.setClientTunnelSession(console_host_session);
            param.setLocale(vm_locale);

            viewer = ConsoleProxy.getAjaxVncViewer(param, ajaxSessionIdStr);
        } catch(Exception e) {

            s_logger.warn("Failed to create viewer due to " + e.getMessage(), e);

            String[] content = new String[] {
                "<html><head></head><body>",
                "<div id=\"main_panel\" tabindex=\"1\">",
                "<p>Access is denied for the console session. Please close the window and retry again</p>",
                "</div></body></html>"
            };
            
            StringBuffer sb = new StringBuffer();
            for(int i = 0; i < content.length; i++)
                sb.append(content[i]);
            
            sendResponse(t, "text/html", sb.toString());
            return;
        }
        
        if(event != 0) {
            if(ajaxSessionId != 0 && ajaxSessionId == viewer.getAjaxSessionId()) {
                if(event == 7) {
                    // client send over an event bag
                    InputStream is = t.getRequestBody();
                    handleClientEventBag(viewer, convertStreamToString(is, true));
                } else {
                    handleClientEvent(viewer, event, queryMap);
                }
                sendResponse(t, "text/html", "OK");
            } else {
                if(s_logger.isDebugEnabled())
                    s_logger.debug("Ajax request comes from a different session, id in request: " + ajaxSessionId + ", id in viewer: " + viewer.getAjaxSessionId());
                
                sendResponse(t, "text/html", "Invalid ajax client session id");
            }
        } else {
            if(ajaxSessionId != 0 && ajaxSessionId != viewer.getAjaxSessionId()) {
                s_logger.info("Ajax request comes from a different session, id in request: " + ajaxSessionId + ", id in viewer: " + viewer.getAjaxSessionId());
                handleClientKickoff(t, viewer);
            } else if(ajaxSessionId == 0) {
                if(s_logger.isDebugEnabled())
                    s_logger.debug("Ajax request indicates a fresh client start");
        
                String title = queryMap.get("t");
                String guest = queryMap.get("guest");
                handleClientStart(t, viewer, title != null ? title : "", guest);
            } else {
                
                if(s_logger.isTraceEnabled())
                    s_logger.trace("Ajax request indicates client update");
                
                handleClientUpdate(t, viewer);
            }
        }
    }

    private static String convertStreamToString(InputStream is, boolean closeStreamAfterRead) { 
        BufferedReader reader = new BufferedReader(new InputStreamReader(is)); 
        StringBuilder sb = new StringBuilder(); 
        String line = null; 
        try { 
            while ((line = reader.readLine()) != null) { 
                sb.append(line + "\n"); 
            } 
        } catch (IOException e) {
            s_logger.warn("Exception while reading request body: ", e);
        } finally {
            if(closeStreamAfterRead) {
                try { 
                    is.close(); 
                } catch (IOException e) { 
                } 
            }
        } 
        return sb.toString(); 
    }   
    
    private void sendResponse(HttpExchange t, String contentType, String response) throws IOException {
        Headers hds = t.getResponseHeaders();
        hds.set("Content-Type", contentType);
    
        t.sendResponseHeaders(200, response.length());
        OutputStream os = t.getResponseBody();
        try {
            os.write(response.getBytes());
        } finally {
            os.close();
        }
    }
    
    @SuppressWarnings("deprecation")
    private void handleClientEventBag(ConsoleProxyClient viewer, String requestData) {
        if(s_logger.isTraceEnabled())
            s_logger.trace("Handle event bag, event bag: " + requestData);
        
        int start = requestData.indexOf("=");
        if(start < 0)
            start = 0;
        else if(start > 0)
            start++;
        String data = URLDecoder.decode(requestData.substring(start));
        String[] tokens = data.split("\\|");
        if(tokens != null && tokens.length > 0) {
            int count = 0;
            try {
                count = Integer.parseInt(tokens[0]);
                int parsePos = 1;
                int type, event, x, y, code, modifiers;
                for(int i = 0; i < count; i++) {
                    type = Integer.parseInt(tokens[parsePos++]);
                    if(type == 1)   {
                        // mouse event
                        event = Integer.parseInt(tokens[parsePos++]);
                        x = Integer.parseInt(tokens[parsePos++]);
                        y = Integer.parseInt(tokens[parsePos++]);
                        code = Integer.parseInt(tokens[parsePos++]);
                        modifiers = Integer.parseInt(tokens[parsePos++]);
                        
                        Map<String, String> queryMap = new HashMap<String, String>();
                        queryMap.put("event", String.valueOf(event));
                        queryMap.put("x", String.valueOf(x));
                        queryMap.put("y", String.valueOf(y));
                        queryMap.put("code", String.valueOf(code));
                        queryMap.put("modifier", String.valueOf(modifiers));
                        handleClientEvent(viewer, event, queryMap);
                    } else {
                        // keyboard event
                        event = Integer.parseInt(tokens[parsePos++]);
                        code = Integer.parseInt(tokens[parsePos++]);
                        modifiers = Integer.parseInt(tokens[parsePos++]);
                        
                        Map<String, String> queryMap = new HashMap<String, String>();
                        queryMap.put("event", String.valueOf(event));
                        queryMap.put("code", String.valueOf(code));
                        queryMap.put("modifier", String.valueOf(modifiers));
                        handleClientEvent(viewer, event, queryMap);
                    }
                }
            } catch(NumberFormatException e) {
                s_logger.warn("Exception in handle client event bag: " + data + ", ", e);
            } catch(Exception e) {
                s_logger.warn("Exception in handle client event bag: " + data + ", ", e);
            } catch(OutOfMemoryError e) {
                s_logger.error("Unrecoverable OutOfMemory Error, exit and let it be re-launched");
                System.exit(1);
            }
        }
    }
    
    private void handleClientEvent(ConsoleProxyClient viewer, int event, Map<String, String> queryMap) {
        int code = 0;
        int x = 0, y = 0;
        int modifiers = 0;
        
        String str;
        switch(event) {
        case 1:     // mouse move
        case 2:     // mouse down
        case 3:     // mouse up
        case 8:     // mouse double click
            str = queryMap.get("x");
            if(str != null) {
                try {
                    x = Integer.parseInt(str);
                } catch (NumberFormatException e) {
                    s_logger.warn("Invalid number parameter in query string: " + str);
                    throw new IllegalArgumentException(e);
                }
            }
            str = queryMap.get("y");
            if(str != null) {
                try {
                    y = Integer.parseInt(str);
                } catch (NumberFormatException e) {
                    s_logger.warn("Invalid number parameter in query string: " + str);
                    throw new IllegalArgumentException(e);
                }
            }
            
            if(event != 1) {
                str = queryMap.get("code");
                try {
                    code = Integer.parseInt(str);
                } catch (NumberFormatException e) {
                    s_logger.warn("Invalid number parameter in query string: " + str);
                    throw new IllegalArgumentException(e);
                }
                
                str = queryMap.get("modifier");
                try {
                    modifiers = Integer.parseInt(str);
                } catch (NumberFormatException e) {
                    s_logger.warn("Invalid number parameter in query string: " + str);
                    throw new IllegalArgumentException(e);
                }
                
                if(s_logger.isTraceEnabled())
                    s_logger.trace("Handle client mouse event. event: " + event + ", x: " + x + ", y: " + y + ", button: " + code + ", modifier: " + modifiers);
            } else {
                if(s_logger.isTraceEnabled())
                    s_logger.trace("Handle client mouse move event. x: " + x + ", y: " + y);
            }
            viewer.sendClientMouseEvent(InputEventType.fromEventCode(event), x, y, code, modifiers);
            break;
            
        case 4:     // key press
        case 5:     // key down
        case 6:     // key up
            str = queryMap.get("code");
            try {
                code = Integer.parseInt(str);
            } catch (NumberFormatException e) {
                s_logger.warn("Invalid number parameter in query string: " + str);
                throw new IllegalArgumentException(e);
            }
            
            str = queryMap.get("modifier");
            try {
                modifiers = Integer.parseInt(str);
            } catch (NumberFormatException e) {
                s_logger.warn("Invalid number parameter in query string: " + str);
                throw new IllegalArgumentException(e);
            }
            
            if(s_logger.isDebugEnabled())
                s_logger.debug("Handle client keyboard event. event: " + event + ", code: " + code + ", modifier: " + modifiers);
            viewer.sendClientRawKeyboardEvent(InputEventType.fromEventCode(event), code, modifiers);
            break;
            
        default :
            break;
        }
    }
    
    private void handleClientKickoff(HttpExchange t, ConsoleProxyClient viewer) throws IOException {
        String response = viewer.onAjaxClientKickoff();
        t.sendResponseHeaders(200, response.length());
        OutputStream os = t.getResponseBody();
        try {
            os.write(response.getBytes());
        } finally {
            os.close();
        }
    }
    
    private void handleClientStart(HttpExchange t, ConsoleProxyClient viewer, String title, String guest) throws IOException {
        List<String> languages = t.getRequestHeaders().get("Accept-Language");
        String response = viewer.onAjaxClientStart(title, languages, guest);
        
        Headers hds = t.getResponseHeaders();
        hds.set("Content-Type", "text/html");
        hds.set("Cache-Control", "no-cache");
        hds.set("Cache-Control", "no-store");
        t.sendResponseHeaders(200, response.length());
        
        OutputStream os = t.getResponseBody();
        try {
            os.write(response.getBytes());
        } finally {
            os.close();
        }
    }
    
    private void handleClientUpdate(HttpExchange t, ConsoleProxyClient viewer) throws IOException {
        String response = viewer.onAjaxClientUpdate();
        
        Headers hds = t.getResponseHeaders();
        hds.set("Content-Type", "text/javascript");
        t.sendResponseHeaders(200, response.length());
        
        OutputStream os = t.getResponseBody();
        try {
            os.write(response.getBytes());
        } finally {
            os.close();
        }
    }
}
