/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed 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.android.ddmlib;

import com.android.annotations.Nullable;

import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DirectColorModel;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.nio.ByteBuffer;

/** Data representing an image taken from a device frame buffer. */
public final class RawImage {
    public int version;
    public int bpp;
    public int colorSpace;
    public int size;
    public int width;
    public int height;
    public int red_offset;
    public int red_length;
    public int blue_offset;
    public int blue_length;
    public int green_offset;
    public int green_length;
    public int alpha_offset;
    public int alpha_length;

    public byte[] data;

    // These values must match the values defined in frameworks/base/cmds/screencap
    public static final int COLOR_SPACE_UNKNOWN = 0;
    public static final int COLOR_SPACE_SRGB = 1;
    public static final int COLOR_SPACE_DISPLAY_P3 = 2;

    /**
     * Reads the header of a RawImage from a {@link ByteBuffer}.
     *
     * <p>The way the data is sent over adb is defined in system/core/adb/framebuffer_service.c
     *
     * @param version the version of the protocol.
     * @param buf the buffer to read from.
     * @return true if success
     */
    public boolean readHeader(int version, ByteBuffer buf) {
        this.version = version;

        if (version == 16) {
            // compatibility mode with original protocol
            this.bpp = 16;

            // read actual values.
            this.size = buf.getInt();
            this.width = buf.getInt();
            this.height = buf.getInt();

            // create default values for the rest. Format is 565
            this.red_offset = 11;
            this.red_length = 5;
            this.green_offset = 5;
            this.green_length = 6;
            this.blue_offset = 0;
            this.blue_length = 5;
            this.alpha_offset = 0;
            this.alpha_length = 0;
        } else if (version == 1 || version == 2) {
            this.bpp = buf.getInt();
            if (version == 2) {
                this.colorSpace = buf.getInt();
            }
            this.size = buf.getInt();
            this.width = buf.getInt();
            this.height = buf.getInt();
            this.red_offset = buf.getInt();
            this.red_length = buf.getInt();
            this.blue_offset = buf.getInt();
            this.blue_length = buf.getInt();
            this.green_offset = buf.getInt();
            this.green_length = buf.getInt();
            this.alpha_offset = buf.getInt();
            this.alpha_length = buf.getInt();
        } else {
            // unsupported protocol!
            return false;
        }

        return true;
    }

    /**
     * Returns the mask value for the red color.
     *
     * <p>This value is compatible with org.eclipse.swt.graphics.PaletteData
     */
    public int getRedMask() {
        return getMask(red_length, red_offset);
    }

    /**
     * Returns the mask value for the green color.
     *
     * <p>This value is compatible with org.eclipse.swt.graphics.PaletteData
     */
    public int getGreenMask() {
        return getMask(green_length, green_offset);
    }

    /**
     * Returns the mask value for the blue color.
     *
     * <p>This value is compatible with org.eclipse.swt.graphics.PaletteData
     */
    public int getBlueMask() {
        return getMask(blue_length, blue_offset);
    }

    /**
     * Returns the size of the header for a specific version of the framebuffer adb protocol.
     *
     * @param version the version of the protocol
     * @return the number of int that makes up the header.
     */
    public static int getHeaderSize(int version) {
        switch (version) {
            case 16: // compatibility mode
                return 3; // size, width, height
            case 1:
                return 12; // bpp, size, width, height, 4*(length, offset)
            case 2:
                return 13; // bpp, colorSpace, size, width, height, 4*(length, offset)
        }

        return 0;
    }

    /** Returns a rotated version of the image The image is rotated counter-clockwise. */
    public RawImage getRotated() {
        RawImage rotated = new RawImage();
        rotated.version = this.version;
        rotated.bpp = this.bpp;
        rotated.colorSpace = this.colorSpace;
        rotated.size = this.size;
        rotated.red_offset = this.red_offset;
        rotated.red_length = this.red_length;
        rotated.blue_offset = this.blue_offset;
        rotated.blue_length = this.blue_length;
        rotated.green_offset = this.green_offset;
        rotated.green_length = this.green_length;
        rotated.alpha_offset = this.alpha_offset;
        rotated.alpha_length = this.alpha_length;

        rotated.width = this.height;
        rotated.height = this.width;

        int count = this.data.length;
        rotated.data = new byte[count];

        int byteCount = this.bpp >> 3; // bpp is in bits, we want bytes to match our array
        final int w = this.width;
        final int h = this.height;
        for (int y = 0; y < h; y++) {
            for (int x = 0; x < w; x++) {
                System.arraycopy(
                        this.data,
                        (y * w + x) * byteCount,
                        rotated.data,
                        ((w - x - 1) * h + y) * byteCount,
                        byteCount);
            }
        }

        return rotated;
    }

    /** Returns an ARGB integer value for the pixel at <var>index</var> in {@link #data}. */
    public int getARGB(int index) {
        int value;
        int r, g, b, a;
        if (bpp == 16) {
            value = data[index] & 0x00FF;
            value |= (data[index + 1] << 8) & 0x0FF00;
            // RGB565 to RGB888
            // Multiply by 255/31 to convert from 5 bits (31 max) to 8 bits (255)
            r = ((value >>> 11) & 0x1f) * 255 / 31;
            g = ((value >>> 5) & 0x3f) * 255 / 63;
            b = (value & 0x1f) * 255 / 31;
            a = 0xFF; // force alpha to opaque if there's no alpha value in the framebuffer.
        } else if (bpp == 32) {
            value = data[index] & 0x00FF;
            value |= (data[index + 1] & 0x00FF) << 8;
            value |= (data[index + 2] & 0x00FF) << 16;
            value |= (data[index + 3] & 0x00FF) << 24;
            r = ((value >>> red_offset) & getMask(red_length)) << (8 - red_length);
            g = ((value >>> green_offset) & getMask(green_length)) << (8 - green_length);
            b = ((value >>> blue_offset) & getMask(blue_length)) << (8 - blue_length);
            a = ((value >>> alpha_offset) & getMask(alpha_length)) << (8 - alpha_length);
        } else {
            throw new UnsupportedOperationException(
                    "RawImage.getARGB(int) only works in 16 and 32 bit mode.");
        }

        return a << 24 | r << 16 | g << 8 | b;
    }

    public BufferedImage asBufferedImage() {
        String profileName = getProfileName();
        BufferedImage image;
        if (profileName == null) {
            //noinspection UndesirableClassUsage
            image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        } else {
            ICC_Profile profile = ICC_Profile.getInstance(ColorSpace.CS_sRGB);
            try {
                profile =
                        ICC_Profile.getInstance(
                                getClass()
                                        .getClassLoader()
                                        .getResourceAsStream("colorProfiles/" + profileName));
            } catch (IOException e) {
                // Ignore
            }
            ICC_ColorSpace colorSpace = new ICC_ColorSpace(profile);

            ColorModel colorModel =
                    new DirectColorModel(
                            colorSpace,
                            32,
                            0x00ff0000,
                            0x0000ff00,
                            0x000000ff,
                            0xff000000,
                            false,
                            DataBuffer.TYPE_INT);
            WritableRaster raster = colorModel.createCompatibleWritableRaster(width, height);

            //noinspection UndesirableClassUsage
            image = new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), null);
        }
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int argb = getARGB((x + y * width) * (bpp / 8));
                image.setRGB(x, y, argb);
            }
        }
        return image;
    }

    @Nullable
    public String getProfileName() {
        switch (colorSpace) {
            case RawImage.COLOR_SPACE_UNKNOWN:
                return null;
            case RawImage.COLOR_SPACE_SRGB:
                return "sRGB.icc";
            case RawImage.COLOR_SPACE_DISPLAY_P3:
                return "DisplayP3.icc";
        }
        return null;
    }

    /**
     * creates a mask value based on a length and offset.
     *
     * <p>This value is compatible with org.eclipse.swt.graphics.PaletteData
     */
    private int getMask(int length, int offset) {
        int res = getMask(length) << offset;

        // if the bpp is 32 bits then we need to invert it because the buffer is in little endian
        if (bpp == 32) {
            return Integer.reverseBytes(res);
        }

        return res;
    }

    /**
     * Creates a mask value based on a length.
     *
     * @param length
     * @return
     */
    private static int getMask(int length) {
        return (1 << length) - 1;
    }
}
