// 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 static com.cloud.utils.AutoCloseableUtil.closeAutoCloseable;

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.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class ConsoleProxyAjaxHandler implements HttpHandler {
    protected Logger logger = LogManager.getLogger(getClass());

    public ConsoleProxyAjaxHandler() {
    }

    @Override
    public void handle(HttpExchange t) throws IOException {
        try {
            if (logger.isTraceEnabled())
                logger.trace("AjaxHandler " + t.getRequestURI());

            long startTick = System.currentTimeMillis();

            doHandle(t);

            if (logger.isTraceEnabled())
                logger.trace(t.getRequestURI() + " process time " + (System.currentTimeMillis() - startTick) + " ms");
        } catch (IOException e) {
            throw e;
        } catch (IllegalArgumentException e) {
            logger.warn("Exception, ", e);
            t.sendResponseHeaders(400, -1);     // bad request
        } catch (Throwable e) {
            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 (logger.isTraceEnabled())
            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 displayName = queryMap.get("displayname");
        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");
        String hypervHost = queryMap.get("hypervHost");
        String username = queryMap.get("username");
        String password = queryMap.get("password");

        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) {
            logger.warn("Invalid number parameter in query string: " + portStr);
            throw new IllegalArgumentException(e);
        }

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

        if (eventStr != null) {
            try {
                event = Integer.parseInt(eventStr);
            } catch (NumberFormatException e) {
                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.setClientDisplayName(displayName);
            param.setTicket(ticket);
            param.setClientTunnelUrl(console_url);
            param.setClientTunnelSession(console_host_session);
            param.setLocale(vm_locale);
            param.setHypervHost(hypervHost);
            param.setUsername(username);
            param.setPassword(password);

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

            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 (logger.isDebugEnabled())
                    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()) {
                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 (logger.isDebugEnabled())
                    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 (logger.isTraceEnabled())
                    logger.trace("Ajax request indicates client update");

                handleClientUpdate(t, viewer);
            }
        }
    }

    private 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) {
            logger.warn("Exception while reading request body: ", e);
        } finally {
            if (closeStreamAfterRead) {
                closeAutoCloseable(is, "error closing stream after read");
            }
        }
        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 (logger.isTraceEnabled())
            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) {
                logger.warn("Exception in handle client event bag: " + data + ", ", e);
            } catch (Exception e) {
                logger.warn("Exception in handle client event bag: " + data + ", ", e);
            } catch (OutOfMemoryError e) {
                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
            case 9:     // mouse scroll
                str = queryMap.get("x");
                if (str != null) {
                    try {
                        x = Integer.parseInt(str);
                    } catch (NumberFormatException e) {
                        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) {
                        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) {
                        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) {
                        logger.warn("Invalid number parameter in query string: " + str);
                        throw new IllegalArgumentException(e);
                    }

                    if (logger.isTraceEnabled())
                        logger.trace("Handle client mouse event. event: " + event + ", x: " + x + ", y: " + y + ", button: " + code + ", modifier: " + modifiers);
                } else {
                    if (logger.isTraceEnabled())
                        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) {
                    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) {
                    logger.warn("Invalid number parameter in query string: " + str);
                    throw new IllegalArgumentException(e);
                }

                if (logger.isDebugEnabled())
                    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();
        }
    }
}
