// 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 rdpclient.rdp;

import streamer.BaseElement;
import streamer.ByteBuffer;
import streamer.Element;
import streamer.Link;
import streamer.Pipeline;
import streamer.PipelineImpl;
import streamer.debug.FakeSink;
import common.BitmapOrder;
import common.BitmapRectangle;

/**
 * @see http://msdn.microsoft.com/en-us/library/cc240624.aspx
 */
public class ServerBitmapUpdate extends BaseElement {
    public static final int UPDATETYPE_BITMAP = 0x0001;

    /**
     * Indicates that the bitmap data is compressed. The bitmapComprHdr field MUST
     * be present if the NO_BITMAP_COMPRESSION_HDR (0x0400) flag is not set.
     */
    public static final int BITMAP_COMPRESSION = 0x0001;

    /**
     * Indicates that the bitmapComprHdr field is not present (removed for
     * bandwidth efficiency to save 8 bytes).
     */
    private static final int NO_BITMAP_COMPRESSION_HDR = 0x0400;

    public ServerBitmapUpdate(String id) {
        super(id);
    }

    @Override
    public void handleData(ByteBuffer buf, Link link) {

        if (verbose)
            System.out.println("[" + this + "] INFO: Data received: " + buf + ".");

        // * DEBUG */System.out.println(buf.toHexString(buf.length));

        BitmapOrder order = new BitmapOrder();

        // (2 bytes): A 16-bit, unsigned integer. The update type. This field MUST
        // be set to UPDATETYPE_BITMAP (0x0001).
        int updateType = buf.readSignedShortLE();
        if (updateType != UPDATETYPE_BITMAP)
            throw new RuntimeException("Unknown update type. Expected update type: UPDATETYPE_BITMAP (0x1). Actual update type: " + updateType + ", buf: " + buf
                    + ".");

        // (2 bytes): A 16-bit, unsigned integer. The number of screen rectangles
        // present in the rectangles field.
        int numberRectangles = buf.readSignedShortLE();

        // (variable): Variable-length array of TS_BITMAP_DATA structures, each of
        // which contains a rectangular clipping taken from the server-side screen
        // frame buffer. The number of screen clippings in the array is specified by
        // the numberRectangles field.
        BitmapRectangle[] rectangles = new BitmapRectangle[numberRectangles];
        for (int i = 0; i < numberRectangles; i++) {
            rectangles[i] = readRectangle(buf);
        }
        order.rectangles = rectangles;

        buf.assertThatBufferIsFullyRead();

        ByteBuffer data = new ByteBuffer(order);
        pushDataToAllOuts(data);

        buf.unref();
    }

    public BitmapRectangle readRectangle(ByteBuffer buf) {

        BitmapRectangle rectangle = new BitmapRectangle();

        // (2 bytes): A 16-bit, unsigned integer. Left bound of the rectangle.
        rectangle.x = buf.readSignedShortLE();

        // (2 bytes): A 16-bit, unsigned integer. Top bound of the rectangle.
        rectangle.y = buf.readSignedShortLE();

        // (2 bytes): A 16-bit, unsigned integer. Inclusive right bound of the
        // rectangle.
        int destRight = buf.readSignedShortLE();
        rectangle.width = destRight - rectangle.x + 1;

        // (2 bytes): A 16-bit, unsigned integer. Inclusive bottom bound of the
        // rectangle.
        int destBottom = buf.readSignedShortLE();
        rectangle.height = destBottom - rectangle.y + 1;

        // (2 bytes): A 16-bit, unsigned integer. The width of the rectangle.
        rectangle.bufferWidth = buf.readSignedShortLE();

        // (2 bytes): A 16-bit, unsigned integer. The height of the rectangle.
        rectangle.bufferHeight = buf.readSignedShortLE();

        // (2 bytes): A 16-bit, unsigned integer. The color depth of the rectangle
        // data in bits-per-pixel.
        rectangle.colorDepth = buf.readSignedShortLE();

        // (2 bytes): A 16-bit, unsigned integer. The flags describing the format of
        // the bitmap data in the bitmapDataStream field.
        int flags = buf.readSignedShortLE();

        // BITMAP_COMPRESSION 0x0001
        // Indicates that the bitmap data is compressed. The bitmapComprHdr field
        // MUST be present if the NO_BITMAP_COMPRESSION_HDR (0x0400) flag is not
        // set.
        boolean compressed = ((flags & BITMAP_COMPRESSION) > 0);

        // (2 bytes): A 16-bit, unsigned integer. The size in bytes of the data in
        // the bitmapComprHdr and bitmapDataStream fields.
        int bitmapLength = buf.readSignedShortLE();

        // NO_BITMAP_COMPRESSION_HDR 0x0400
        // Indicates that the bitmapComprHdr field is not present (removed for
        // bandwidth efficiency to save 8 bytes).
        if (compressed && (flags & NO_BITMAP_COMPRESSION_HDR) == 0) {
            // (8 bytes): Optional Compressed Data Header structure specifying the
            // bitmap data in the bitmapDataStream.
            // This field MUST be present if the BITMAP_COMPRESSION (0x0001) flag is
            // present in the Flags field, but the NO_BITMAP_COMPRESSION_HDR (0x0400)
            // flag is not.

            // Note: Even when compression header is enabled, server sends nothing.
            // rectangle.compressedBitmapHeader = buf.readBytes(8);
        }

        // (variable): A variable-length array of bytes describing a bitmap image.
        // Bitmap data is either compressed or uncompressed, depending on whether
        // the BITMAP_COMPRESSION flag is present in the Flags field. Uncompressed
        // bitmap data is formatted as a bottom-up, left-to-right series of pixels.
        // Each pixel is a whole number of bytes. Each row contains a multiple of
        // four bytes (including up to three bytes of padding, as necessary).
        // Compressed bitmaps not in 32 bpp format are compressed using Interleaved
        // RLE and encapsulated in an RLE Compressed Bitmap Stream structure,
        // while compressed bitmaps at a color depth of 32 bpp are compressed
        // using RDP 6.0 Bitmap Compression and stored inside
        // an RDP 6.0 Bitmap Compressed Stream structure.
        if (!compressed) {
            rectangle.bitmapDataStream = buf.readBytes(bitmapLength);
        } else {
            ByteBuffer compressedImage = buf.readBytes(bitmapLength);
            //* DEBUG */System.out.println("Compressed image: " + compressedImage + ", depth: " + rectangle.bitsPerPixel + ".");
            rectangle.bitmapDataStream = RLEBitmapDecompression.rleDecompress(compressedImage, rectangle.bufferWidth, rectangle.bufferHeight, rectangle.colorDepth);
            compressedImage.unref();
        }

        return rectangle;
    }

    /**
     * Example.
     */
    public static void main(String args[]) {
        ByteBuffer packet = new ByteBuffer(new byte[] {0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x10, 0x00, 0x01, 0x00, 0x10, 0x00,
                0x01, 0x04, 0x0a, 0x00, 0x0c, (byte)0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});

        Element bitmap = new ServerBitmapUpdate("bitmap") {
            {
                verbose = true;
            }
        };
        FakeSink fakeSink = new FakeSink("sink") {
            {
                verbose = true;
            }
        };
        Pipeline pipeline = new PipelineImpl("test");

        // BufferedImageCanvas canvas = new BufferedImageCanvas(1024, 768);
        // Element adapter = new AwtRdpAdapter("test",canvas );
        // pipeline.addAndLink(bitmap, adapter);
        pipeline.addAndLink(bitmap, fakeSink);

        bitmap.handleData(packet, null);

    }

}
