// 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.vnc;

import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.io.DataOutputStream;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

import com.cloud.consoleproxy.vnc.packet.client.ClientPacket;
import com.cloud.consoleproxy.vnc.packet.client.FramebufferUpdateRequestPacket;
import com.cloud.consoleproxy.vnc.packet.client.KeyboardEventPacket;
import com.cloud.consoleproxy.vnc.packet.client.MouseEventPacket;
import com.cloud.consoleproxy.vnc.packet.client.SetEncodingsPacket;
import com.cloud.consoleproxy.vnc.packet.client.SetPixelFormatPacket;

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

public class VncClientPacketSender implements Runnable, PaintNotificationListener, KeyListener, MouseListener, MouseMotionListener, FrameBufferUpdateListener {
    protected Logger logger = LogManager.getLogger(getClass());

    // Queue for outgoing packets
    private final BlockingQueue<ClientPacket> queue = new ArrayBlockingQueue<ClientPacket>(30);

    private final DataOutputStream os;
    private final VncScreenDescription screen;
    private final VncClient vncConnection;

    private boolean connectionAlive = true;

    // Don't send update request again until we receive next frame buffer update
    private boolean updateRequestSent = false;

    public VncClientPacketSender(DataOutputStream os, VncScreenDescription screen, VncClient vncConnection) {
        this.os = os;
        this.screen = screen;
        this.vncConnection = vncConnection;

        sendSetPixelFormat();
        sendSetEncodings();
        requestFullScreenUpdate();
    }

    public void sendClientPacket(ClientPacket packet) {
        queue.add(packet);
    }

    @Override
    public void run() {
        try {
            while (connectionAlive) {
                ClientPacket packet = queue.poll(1, TimeUnit.SECONDS);
                if (packet != null) {
                    packet.write(os);
                    os.flush();
                }
            }
        } catch (Throwable e) {
            logger.error("Unexpected exception: ", e);
            if (connectionAlive) {
                closeConnection();
            }
        } finally {
            logger.info("Sending thread exit processing, shutdown connection");
            vncConnection.shutdown();
        }
    }

    private void sendSetEncodings() {
        queue.add(new SetEncodingsPacket(RfbConstants.SUPPORTED_ENCODINGS_ARRAY));
    }

    private void sendSetPixelFormat() {
        if (!screen.isRGB888_32_LE()) {
            queue.add(new SetPixelFormatPacket(screen, 32, 24, RfbConstants.LITTLE_ENDIAN, RfbConstants.TRUE_COLOR, 255, 255, 255, 16, 8, 0));
        }
    }

    public void closeConnection() {
        connectionAlive = false;
    }

    public void requestFullScreenUpdate() {
        queue.add(new FramebufferUpdateRequestPacket(RfbConstants.FRAMEBUFFER_FULL_UPDATE_REQUEST, 0, 0, screen.getFramebufferWidth(), screen.getFramebufferHeight()));
        updateRequestSent = true;
    }

    @Override
    public void imagePaintedOnScreen() {
        if (!updateRequestSent) {
            queue.add(new FramebufferUpdateRequestPacket(RfbConstants.FRAMEBUFFER_INCREMENTAL_UPDATE_REQUEST, 0, 0, screen.getFramebufferWidth(),
                screen.getFramebufferHeight()));
            updateRequestSent = true;
        }
    }

    @Override
    public void frameBufferPacketReceived() {
        updateRequestSent = false;
    }

    @Override
    public void mouseDragged(MouseEvent e) {
        queue.add(new MouseEventPacket(mapAwtModifiersToVncButtonMask(e.getModifiersEx()), e.getX(), e.getY()));
    }

    @Override
    public void mouseMoved(MouseEvent e) {
        queue.add(new MouseEventPacket(mapAwtModifiersToVncButtonMask(e.getModifiersEx()), e.getX(), e.getY()));
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        // Nothing to do
    }

    @Override
    public void mousePressed(MouseEvent e) {
        queue.add(new MouseEventPacket(mapAwtModifiersToVncButtonMask(e.getModifiersEx()), e.getX(), e.getY()));
    }

    @Override
    public void mouseReleased(MouseEvent e) {
        queue.add(new MouseEventPacket(mapAwtModifiersToVncButtonMask(e.getModifiersEx()), e.getX(), e.getY()));
    }

    @Override
    public void mouseEntered(MouseEvent e) {
        // Nothing to do
    }

    @Override
    public void mouseExited(MouseEvent e) {
        // Nothing to do
    }

    /**
     * Current state of buttons 1 to 8 are represented by bits 0 to 7 of
     * button-mask respectively, 0 meaning up, 1 meaning down (pressed). On a
     * conventional mouse, buttons 1, 2 and 3 correspond to the left, middle and
     * right buttons on the mouse. On a wheel mouse, each step of the wheel
     * upwards is represented by a press and release of button 4, and each step
     * downwards is represented by a press and release of button 5.
     *
     * @param modifiers
     *            extended modifiers from AWT mouse event
     * @return VNC mouse button mask
     */
    public static int mapAwtModifiersToVncButtonMask(int modifiers) {
        int mask =
            (((modifiers & InputEvent.BUTTON1_DOWN_MASK) != 0) ? 0x1 : 0) | (((modifiers & InputEvent.BUTTON2_DOWN_MASK) != 0) ? 0x2 : 0) |
                (((modifiers & InputEvent.BUTTON3_DOWN_MASK) != 0) ? 0x4 : 0);
        return mask;
    }

    @Override
    public void keyTyped(KeyEvent e) {
        // Do nothing
    }

    @Override
    public void keyPressed(KeyEvent e) {
        ClientPacket request = new KeyboardEventPacket(RfbConstants.KEY_DOWN, mapAwtKeyToVncKey(e.getKeyCode()));
        queue.add(request);
    }

    @Override
    public void keyReleased(KeyEvent e) {
        ClientPacket request = new KeyboardEventPacket(RfbConstants.KEY_UP, mapAwtKeyToVncKey(e.getKeyCode()));
        queue.add(request);
    }

    private int mapAwtKeyToVncKey(int key) {
        switch (key) {
            case KeyEvent.VK_BACK_SPACE:
                return 0xff08;
            case KeyEvent.VK_TAB:
                return 0xff09;
            case KeyEvent.VK_ENTER:
                return 0xff0d;
            case KeyEvent.VK_ESCAPE:
                return 0xff1b;
            case KeyEvent.VK_INSERT:
                return 0xff63;
            case KeyEvent.VK_DELETE:
                return 0xffff;
            case KeyEvent.VK_HOME:
                return 0xff50;
            case KeyEvent.VK_END:
                return 0xff57;
            case KeyEvent.VK_PAGE_UP:
                return 0xff55;
            case KeyEvent.VK_PAGE_DOWN:
                return 0xff56;
            case KeyEvent.VK_LEFT:
                return 0xff51;
            case KeyEvent.VK_UP:
                return 0xff52;
            case KeyEvent.VK_RIGHT:
                return 0xff53;
            case KeyEvent.VK_DOWN:
                return 0xff54;
            case KeyEvent.VK_F1:
                return 0xffbe;
            case KeyEvent.VK_F2:
                return 0xffbf;
            case KeyEvent.VK_F3:
                return 0xffc0;
            case KeyEvent.VK_F4:
                return 0xffc1;
            case KeyEvent.VK_F5:
                return 0xffc2;
            case KeyEvent.VK_F6:
                return 0xffc3;
            case KeyEvent.VK_F7:
                return 0xffc4;
            case KeyEvent.VK_F8:
                return 0xffc5;
            case KeyEvent.VK_F9:
                return 0xffc6;
            case KeyEvent.VK_F10:
                return 0xffc7;
            case KeyEvent.VK_F11:
                return 0xffc8;
            case KeyEvent.VK_F12:
                return 0xffc9;
            case KeyEvent.VK_SHIFT:
                return 0xffe1;
            case KeyEvent.VK_CONTROL:
                return 0xffe3;
            case KeyEvent.VK_META:
                return 0xffe7;
            case KeyEvent.VK_ALT:
                return 0xffe9;
            case KeyEvent.VK_ALT_GRAPH:
                return 0xffea;
            case KeyEvent.VK_BACK_QUOTE:
                return 0x0060;
        }

        return key;
    }

}
