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

import streamer.ByteBuffer;
import streamer.Element;
import streamer.Link;
import streamer.OneTimeSwitch;
import streamer.Pipeline;
import streamer.PipelineImpl;
import streamer.debug.MockSink;
import streamer.debug.MockSource;
import common.ScreenDescription;

public class VncInitializer extends OneTimeSwitch {

    // Pad names
    public static final String CLIENT_SUPPORTED_ENCODINGS_ADAPTER_PAD = "encodings";
    public static final String CLIENT_PIXEL_FORMAT_ADAPTER_PAD = "pixel_format";

    protected byte sharedFlag = RfbConstants.EXCLUSIVE_ACCESS;

    /**
     * Properties of remote screen .
     */
    protected ScreenDescription screen;

    public VncInitializer(String id, boolean shared, ScreenDescription screen) {
        super(id);

        setSharedFlag(shared);
        this.screen = screen;

        declarePads();
    }

    @Override
    protected void declarePads() {
        super.declarePads();
        outputPads.put(CLIENT_SUPPORTED_ENCODINGS_ADAPTER_PAD, null);
        outputPads.put(CLIENT_PIXEL_FORMAT_ADAPTER_PAD, null);
    }

    public ScreenDescription getScreen() {
        return screen;
    }

    public void setScreen(ScreenDescription screen) {
        this.screen = screen;
    }

    public void setSharedFlag(boolean shared) {
        if (shared)
            sharedFlag = RfbConstants.SHARED_ACCESS;
        else
            sharedFlag = RfbConstants.EXCLUSIVE_ACCESS;
    }

    @Override
    protected void handleOneTimeData(ByteBuffer buf, Link link) {
        if (verbose)
            System.out.println("[" + this + "] INFO: Data received: " + buf + ".");

        // Server initialization message is at least 24 bytes long + length of
        // desktop name
        if (!cap(buf, 24, UNLIMITED, link, false))
            return;

        // Read server initialization message
        // Read frame buffer size
        int framebufferWidth = buf.readUnsignedShort();
        int framebufferHeight = buf.readUnsignedShort();

        // Read pixel format
        int bitsPerPixel = buf.readUnsignedByte();
        int depth = buf.readUnsignedByte();

        int bigEndianFlag = buf.readUnsignedByte();
        int trueColorFlag = buf.readUnsignedByte();

        int redMax = buf.readUnsignedShort();
        int greenMax = buf.readUnsignedShort();
        int blueMax = buf.readUnsignedShort();

        int redShift = buf.readUnsignedByte();
        int greenShift = buf.readUnsignedByte();
        int blueShift = buf.readUnsignedByte();

        // Skip padding
        buf.skipBytes(3);

        // Read desktop name
        int length = buf.readSignedInt();

        // Consume exactly $length bytes, push back any extra bytes
        if (!cap(buf, length, length, link, true))
            return;

        String desktopName = buf.readString(length, RfbConstants.US_ASCII_CHARSET);
        buf.unref();
        if (verbose)
            System.out.println("[" + this + "] INFO: Desktop name: \"" + desktopName + "\", bpp: " + bitsPerPixel + ", depth: " + depth + ", screen size: "
                    + framebufferWidth + "x" + framebufferHeight + ".");

        // Set screen properties
        screen.setFramebufferSize(framebufferWidth, framebufferHeight);
        screen.setPixelFormat(bitsPerPixel, depth, bigEndianFlag != RfbConstants.LITTLE_ENDIAN, trueColorFlag == RfbConstants.TRUE_COLOR, redMax, greenMax, blueMax, redShift,
                greenShift, blueShift);
        screen.setDesktopName(desktopName);

        // If sever screen has different parameters than ours, then change it
        if (!screen.isRGB888_32_LE()) {
            // Send client pixel format
            sendClientPixelFormat();
        }

        // Send encodings supported by client
        sendSupportedEncodings();

        switchOff();

    }

    @Override
    protected void onStart() {
        ByteBuffer buf = new ByteBuffer(new byte[] {sharedFlag});
        pushDataToOTOut(buf);
    }

    private void sendClientPixelFormat() {
        pushDataToPad(CLIENT_PIXEL_FORMAT_ADAPTER_PAD, new ByteBuffer(0));
    }

    private void sendSupportedEncodings() {
        pushDataToPad(CLIENT_SUPPORTED_ENCODINGS_ADAPTER_PAD, new ByteBuffer(0));
    }

    @Override
    public String toString() {
        return "VncInit(" + id + ")";
    }

    /**
     * Example.
     */
    public static void main(String args[]) {
        // System.setProperty("streamer.Link.debug", "true");
        System.setProperty("streamer.Element.debug", "true");
        // System.setProperty("streamer.Pipeline.debug", "true");

        final String desktopName = "test";

        Element source = new MockSource("source") {
            {
                bufs = ByteBuffer.convertByteArraysToByteBuffers(
                        // Send screen description
                        new byte[] {
                                // Framebuffer width (short)
                                0, (byte)200,
                                // Framebuffer height (short)
                                0, 100,
                                // Bits per pixel
                                32,
                                // Depth,
                                24,
                                // Endianness flag
                                RfbConstants.LITTLE_ENDIAN,
                                // Truecolor flag
                                RfbConstants.TRUE_COLOR,
                                // Red max (short)
                                0, (byte)255,
                                // Green max (short)
                                0, (byte)255,
                                // Blue max (short)
                                0, (byte)255,
                                // Red shift
                                16,
                                // Green shift
                                8,
                                // Blue shift
                                0,
                                // Padding
                                0, 0, 0,
                                // Desktop name length (int)
                                0, 0, 0, 4,
                                // Desktop name ("test", 4 bytes)
                                't', 'e', 's', 't',

                                // Tail
                                1, 2, 3

                        },
                        // Tail packet
                        new byte[] {4, 5, 6});
            }
        };

        ScreenDescription screen = new ScreenDescription();
        final VncInitializer init = new VncInitializer("init", true, screen);
        Element initSink = new MockSink("initSink") {
            {
                // Expect shared flag
                bufs = ByteBuffer.convertByteArraysToByteBuffers(new byte[] {RfbConstants.SHARED_ACCESS});
            }
        };
        Element mainSink = new MockSink("mainSink") {
            {
                // Expect two tail packets
                bufs = ByteBuffer.convertByteArraysToByteBuffers(new byte[] {1, 2, 3}, new byte[] {4, 5, 6});
            }
        };
        ByteBuffer[] emptyBuf = ByteBuffer.convertByteArraysToByteBuffers(new byte[] {});
        Element encodingsSink = new MockSink("encodings", emptyBuf);
        Element pixelFormatSink = new MockSink("pixel_format", emptyBuf);

        Pipeline pipeline = new PipelineImpl("test");
        pipeline.addAndLink(source, init, mainSink);
        pipeline.add(encodingsSink, pixelFormatSink, initSink);
        pipeline.link("init >otout", "initSink");
        pipeline.link("init >" + CLIENT_SUPPORTED_ENCODINGS_ADAPTER_PAD, "encodings");
        pipeline.link("init >" + CLIENT_PIXEL_FORMAT_ADAPTER_PAD, "pixel_format");

        pipeline.runMainLoop("source", STDOUT, false, false);

        if (!screen.isRGB888_32_LE())
            System.err.println("Screen description was read incorrectly: " + screen + ".");
        if (!desktopName.equals(screen.getDesktopName()))
            System.err.println("Screen desktop name was read incorrectly: \"" + screen.getDesktopName() + "\".");

    }
}
