/*	$NetBSD: hdmi.h,v 1.6 2015/11/14 18:04:05 jmcneill Exp $	*/

/*-
 * Copyright (c) 2014 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Taylor R. Campbell.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#ifndef	_LINUX_HDMI_H_
#define	_LINUX_HDMI_H_

#include <sys/types.h>
#include <sys/param.h>
#include <sys/errno.h>
#include <sys/systm.h>

enum hdmi_3d_structure {
        HDMI_3D_STRUCTURE_INVALID		= -1,
        HDMI_3D_STRUCTURE_FRAME_PACKING		= 0,
        HDMI_3D_STRUCTURE_FIELD_ALTERNATIVE	= 1,
        HDMI_3D_STRUCTURE_LINE_ALTERNATIVE	= 2,
        HDMI_3D_STRUCTURE_SIDE_BY_SIDE_FULL	= 3,
        HDMI_3D_STRUCTURE_L_DEPTH		= 4,
        HDMI_3D_STRUCTURE_L_DEPTH_GFX_GFX_DEPTH	= 5,
        HDMI_3D_STRUCTURE_TOP_AND_BOTTOM	= 6,
        HDMI_3D_STRUCTURE_SIDE_BY_SIDE_HALF	= 8,
};

enum hdmi_active_aspect {
        HDMI_ACTIVE_ASPECT_16_9_TOP		= 2,
        HDMI_ACTIVE_ASPECT_14_9_TOP		= 3,
        HDMI_ACTIVE_ASPECT_16_9_CENTER		= 4,
        HDMI_ACTIVE_ASPECT_PICTURE		= 8,
        HDMI_ACTIVE_ASPECT_4_3			= 9,
        HDMI_ACTIVE_ASPECT_16_9			= 10,
        HDMI_ACTIVE_ASPECT_14_9			= 11,
        HDMI_ACTIVE_ASPECT_4_3_SP_14_9		= 13,
        HDMI_ACTIVE_ASPECT_16_9_SP_14_9		= 14,
        HDMI_ACTIVE_ASPECT_16_9_SP_4_3		= 15,
};

enum hdmi_audio_coding_type {
	HDMI_AUDIO_CODING_TYPE_STREAM		= 0,
	HDMI_AUDIO_CODING_TYPE_PCM		= 1,
	HDMI_AUDIO_CODING_TYPE_AC3		= 2,
	HDMI_AUDIO_CODING_TYPE_MPEG1		= 3,
	HDMI_AUDIO_CODING_TYPE_MP3		= 4,
	HDMI_AUDIO_CODING_TYPE_MPEG2		= 5,
	HDMI_AUDIO_CODING_TYPE_AAC_LC		= 6,
	HDMI_AUDIO_CODING_TYPE_DTS		= 7,
	HDMI_AUDIO_CODING_TYPE_ATRAC		= 8,
	HDMI_AUDIO_CODING_TYPE_DSD		= 9,
	HDMI_AUDIO_CODING_TYPE_EAC3		= 10,
	HDMI_AUDIO_CODING_TYPE_DTS_HD		= 11,
	HDMI_AUDIO_CODING_TYPE_MLP		= 12,
	HDMI_AUDIO_CODING_TYPE_DST		= 13,
	HDMI_AUDIO_CODING_TYPE_WMA_PRO		= 14,
};

enum hdmi_audio_coding_type_ext {
	HDMI_AUDIO_CODING_TYPE_EXT_STREAM	= 0,
	HDMI_AUDIO_CODING_TYPE_EXT_HE_AAC	= 1,
	HDMI_AUDIO_CODING_TYPE_EXT_HE_AAC_V2	= 2,
	HDMI_AUDIO_CODING_TYPE_EXT_MPEG_SURROUND = 3,
};

enum hdmi_audio_sample_frequency {
	HDMI_AUDIO_SAMPLE_FREQUENCY_STREAM	= 0,
	HDMI_AUDIO_SAMPLE_FREQUENCY_32000	= 1,
	HDMI_AUDIO_SAMPLE_FREQUENCY_44100	= 2,
	HDMI_AUDIO_SAMPLE_FREQUENCY_48000	= 3,
	HDMI_AUDIO_SAMPLE_FREQUENCY_88200	= 4,
	HDMI_AUDIO_SAMPLE_FREQUENCY_96000	= 5,
	HDMI_AUDIO_SAMPLE_FREQUENCY_176400	= 6,
	HDMI_AUDIO_SAMPLE_FREQUENCY_192000	= 7,
};

enum hdmi_audio_sample_size {
	HDMI_AUDIO_SAMPLE_SIZE_STREAM		= 0,
	HDMI_AUDIO_SAMPLE_SIZE_16		= 1,
	HDMI_AUDIO_SAMPLE_SIZE_20		= 2,
	HDMI_AUDIO_SAMPLE_SIZE_24		= 3,
};

enum hdmi_colorimetry {
	HDMI_COLORIMETRY_NONE			= 0,
	HDMI_COLORIMETRY_ITU_601		= 1,
	HDMI_COLORIMETRY_ITU_709		= 2,
	HDMI_COLORIMETRY_EXTENDED		= 3,
};

enum hdmi_colorspace {
	HDMI_COLORSPACE_RGB			= 0,
	HDMI_COLORSPACE_YUV422			= 1,
	HDMI_COLORSPACE_YUV444			= 2,
};

enum hdmi_content_type {
	HDMI_CONTENT_TYPE_NONE			= 0,
	HDMI_CONTENT_TYPE_PHOTO			= 1,
	HDMI_CONTENT_TYPE_CINEMA		= 2,
	HDMI_CONTENT_TYPE_GAME			= 3,
};

enum hdmi_extended_colorimetry {
	HDMI_EXTENDED_COLORIMETRY_XV_YCC_601	= 0,
	HDMI_EXTENDED_COLORIMETRY_XV_YCC_709	= 1,
	HDMI_EXTENDED_COLORIMETRY_S_YCC_601	= 2,
	HDMI_EXTENDED_COLORIMETRY_ADOBE_YCC_601	= 3,
	HDMI_EXTENDED_COLORIMETRY_ADOBE_RGB	= 4,
};

enum hdmi_nups {
	HDMI_NUPS_UNKNOWN			= 0,
	HDMI_NUPS_HORIZONTAL			= 1,
	HDMI_NUPS_VERTICAL			= 2,
	HDMI_NUPS_BOTH				= 3,
};

enum hdmi_picture_aspect {
	HDMI_PICTURE_ASPECT_NONE		= 0,
	HDMI_PICTURE_ASPECT_4_3			= 1,
	HDMI_PICTURE_ASPECT_16_9		= 2,
};

enum hdmi_quantization_range {
	HDMI_QUANTIZATION_RANGE_DEFAULT		= 0,
	HDMI_QUANTIZATION_RANGE_LIMITED		= 1,
	HDMI_QUANTIZATION_RANGE_FULL		= 2,
};

enum hdmi_scan_mode {
	HDMI_SCAN_MODE_NONE			= 0,
	HDMI_SCAN_MODE_OVERSCAN			= 1,
	HDMI_SCAN_MODE_UNDERSCAN		= 2,
};

enum hdmi_ycc_quantization_range {
	HDMI_YCC_QUANTIZATION_RANGE_LIMITED	= 0,
	HDMI_YCC_QUANTIZATION_RANGE_FULL	= 1,
};

enum hdmi_infoframe_type {
	HDMI_INFOFRAME_TYPE_VENDOR		= 0x81,
	HDMI_INFOFRAME_TYPE_AVI			= 0x82,
	HDMI_INFOFRAME_TYPE_SPD			= 0x83,
	HDMI_INFOFRAME_TYPE_AUDIO		= 0x84,
};

#define	HDMI_INFOFRAME_SIZE(TYPE)					      \
	(HDMI_INFOFRAME_HEADER_SIZE + HDMI_##TYPE##_INFOFRAME_SIZE)

#define	HDMI_INFOFRAME_HEADER_SIZE	4
struct hdmi_infoframe_header {
	enum hdmi_infoframe_type	type;
	uint8_t				version;
	uint8_t				length;
	/* checksum */
};

static inline void
hdmi_infoframe_header_init(struct hdmi_infoframe_header *header,
    enum hdmi_infoframe_type type, uint8_t vers, uint8_t length)
{

	header->type = type;
	header->version = vers;
	header->length = length;
}

static inline int
hdmi_infoframe_header_pack(const struct hdmi_infoframe_header *header,
    uint8_t length, void *buf, size_t size)
{
	uint8_t *const p = buf;

	if (length < HDMI_INFOFRAME_HEADER_SIZE)
		return -ENOSPC;
	if (size < length)
		return -ENOSPC;

	p[0] = header->type;
	p[1] = header->version;
	p[2] = (length - HDMI_INFOFRAME_HEADER_SIZE);
	p[3] = 0;		/* checksum */

	return HDMI_INFOFRAME_HEADER_SIZE;
}

static inline void
hdmi_infoframe_checksum(void *buf, size_t length)
{
	uint8_t *p = buf;
	uint8_t checksum = 0;

	while (length--)
		checksum += *p++;

	p = buf;
	p[3] = (256 - checksum);
}

#define	HDMI_AUDIO_INFOFRAME_SIZE	10
struct hdmi_audio_infoframe {
	struct hdmi_infoframe_header	header;
	uint8_t				channels;
	enum hdmi_audio_coding_type	coding_type;
	enum hdmi_audio_sample_size	sample_size;
	enum hdmi_audio_sample_frequency sample_frequency;
	enum hdmi_audio_coding_type_ext	coding_type_ext;
	uint8_t				channel_allocation;
	uint8_t				level_shift_value;
	bool				downmix_inhibit;
};

static inline int
hdmi_audio_infoframe_init(struct hdmi_audio_infoframe *frame)
{
	static const struct hdmi_audio_infoframe zero_frame;

	*frame = zero_frame;

	hdmi_infoframe_header_init(&frame->header, HDMI_INFOFRAME_TYPE_AUDIO,
	    1, HDMI_AUDIO_INFOFRAME_SIZE);

	return 0;
}

static inline ssize_t
hdmi_audio_infoframe_pack(const struct hdmi_audio_infoframe *frame, void *buf,
    size_t size)
{
	const size_t length = HDMI_INFOFRAME_HEADER_SIZE +
	    HDMI_AUDIO_INFOFRAME_SIZE;
	uint8_t channels = 0;
	uint8_t *p = buf;
	int ret;

	KASSERT(frame->header.length == HDMI_AUDIO_INFOFRAME_SIZE);

	ret = hdmi_infoframe_header_pack(&frame->header, length, p, size);
	if (ret < 0)
		return ret;
	KASSERT(ret == HDMI_INFOFRAME_HEADER_SIZE);
	p += HDMI_INFOFRAME_HEADER_SIZE;
	size -= HDMI_INFOFRAME_HEADER_SIZE;

	if (frame->channels >= 2)
		channels = frame->channels - 1;

	p[0] = __SHIFTIN(frame->coding_type, __BITS(7,4));
	p[0] |= __SHIFTIN(channels, __BITS(2,0));

	p[1] = __SHIFTIN(frame->sample_frequency, __BITS(4,2));
	p[1] |= __SHIFTIN(frame->sample_size, __BITS(1,0));

	p[2] = __SHIFTIN(frame->coding_type_ext, __BITS(5,0));

	p[3] = __SHIFTIN(frame->level_shift_value, __BITS(6, 3));

	p[4] = __SHIFTIN(frame->downmix_inhibit? 1 : 0, __BIT(7));

	/* PB6 to PB10 are reserved */
	p[5] = 0;
	p[6] = 0;
	p[7] = 0;
	p[8] = 0;
	p[9] = 0;

	CTASSERT(HDMI_AUDIO_INFOFRAME_SIZE == 10);

	hdmi_infoframe_checksum(buf, length);

	return length;
}

#define	HDMI_AVI_INFOFRAME_SIZE		13
struct hdmi_avi_infoframe {
	struct hdmi_infoframe_header	header;
	enum hdmi_colorspace		colorspace;
	enum hdmi_scan_mode		scan_mode;
	enum hdmi_colorimetry		colorimetry;
	enum hdmi_picture_aspect	picture_aspect;
	enum hdmi_active_aspect		active_aspect;
	bool				itc;
	enum hdmi_extended_colorimetry	extended_colorimetry;
	enum hdmi_quantization_range	quantization_range;
	enum hdmi_nups			nups;
	uint8_t				video_code;
	enum hdmi_ycc_quantization_range ycc_quantization_range;
	enum hdmi_content_type		content_type;
	uint8_t				pixel_repeat;
	uint16_t			top_bar;
	uint16_t			bottom_bar;
	uint16_t			left_bar;
	uint16_t			right_bar;
};

static inline int
hdmi_avi_infoframe_init(struct hdmi_avi_infoframe *frame)
{
	static const struct hdmi_avi_infoframe zero_frame;

	*frame = zero_frame;

	hdmi_infoframe_header_init(&frame->header, HDMI_INFOFRAME_TYPE_AVI, 2,
	    HDMI_AVI_INFOFRAME_SIZE);

	return 0;
}

static inline ssize_t
hdmi_avi_infoframe_pack(const struct hdmi_avi_infoframe *frame, void *buf,
    size_t size)
{
	const size_t length = HDMI_INFOFRAME_HEADER_SIZE +
	    HDMI_AVI_INFOFRAME_SIZE;
	uint8_t *p = buf;
	int ret;

	KASSERT(frame->header.length == HDMI_AVI_INFOFRAME_SIZE);

	ret = hdmi_infoframe_header_pack(&frame->header, length, p, size);
	if (ret < 0)
		return ret;
	KASSERT(ret == HDMI_INFOFRAME_HEADER_SIZE);
	p += HDMI_INFOFRAME_HEADER_SIZE;
	size -= HDMI_INFOFRAME_HEADER_SIZE;

	p[0] = __SHIFTIN(frame->colorspace, __BITS(6,5));
	p[0] |= __SHIFTIN(frame->active_aspect & 0xf? 1 : 0, __BIT(4));
	p[0] |= __SHIFTIN(frame->top_bar || frame->bottom_bar, __BIT(3));
	p[0] |= __SHIFTIN(frame->left_bar || frame->right_bar, __BIT(2));
	p[0] |= __SHIFTIN(frame->scan_mode, __BITS(1,0));

	p[1] = __SHIFTIN(frame->colorimetry, __BITS(7,6));
	p[1] |= __SHIFTIN(frame->picture_aspect, __BITS(5,4));
	p[1] |= __SHIFTIN(frame->active_aspect, __BITS(3,0));

	p[2] = __SHIFTIN(frame->itc? 1 : 0, __BIT(7));
	p[2] |= __SHIFTIN(frame->extended_colorimetry, __BITS(6,4));
	p[2] |= __SHIFTIN(frame->quantization_range, __BITS(3,2));
	p[2] |= __SHIFTIN(frame->nups, __BITS(1,0));

	p[3] = frame->video_code;

	p[4] = __SHIFTIN(frame->ycc_quantization_range, __BITS(7,6));
	p[4] |= __SHIFTIN(frame->content_type, __BITS(5,4));
	p[4] |= __SHIFTIN(frame->pixel_repeat, __BITS(3,0));

	le16enc(&p[5], frame->top_bar);
	le16enc(&p[7], frame->bottom_bar);
	le16enc(&p[9], frame->left_bar);
	le16enc(&p[11], frame->right_bar);
	CTASSERT(HDMI_AVI_INFOFRAME_SIZE == 13);

	hdmi_infoframe_checksum(buf, length);

	return length;
}

#define	HDMI_SPD_INFOFRAME_SIZE		25
struct hdmi_spd_infoframe {
	struct hdmi_infoframe_header	header;
	char				vendor[8];
	char				product[16];
	enum hdmi_spd_sdi {
	        HDMI_SPD_SDI_UNKNOWN	= 0,
		HDMI_SPD_SDI_DSTB	= 1,
		HDMI_SPD_SDI_DVDP	= 2,
		HDMI_SPD_SDI_DVHS	= 3,
		HDMI_SPD_SDI_HDDVR	= 4,
		HDMI_SPD_SDI_DVC	= 5,
		HDMI_SPD_SDI_DSC	= 6,
		HDMI_SPD_SDI_VCD	= 7,
		HDMI_SPD_SDI_GAME	= 8,
		HDMI_SPD_SDI_PC		= 9,
		HDMI_SPD_SDI_BD		= 10,
		HDMI_SPD_SDI_SACD	= 11,
		HDMI_SPD_SDI_HDDVD	= 12,
		HDMI_SPD_SDI_PMP	= 13,
	}				sdi;
};

static inline int
hdmi_spd_infoframe_init(struct hdmi_spd_infoframe *frame, const char *vendor,
    const char *product)
{
	static const struct hdmi_spd_infoframe zero_frame;

	*frame = zero_frame;

	hdmi_infoframe_header_init(&frame->header, HDMI_INFOFRAME_TYPE_SPD,
	    1, HDMI_SPD_INFOFRAME_SIZE);

	(void)strlcpy(frame->vendor, vendor, sizeof(frame->vendor));
	(void)strlcpy(frame->product, product, sizeof(frame->product));

	return 0;
}

static inline ssize_t
hdmi_spd_infoframe_pack(struct hdmi_spd_infoframe *frame, void *buf,
    size_t size)
{
	const size_t length = HDMI_INFOFRAME_HEADER_SIZE +
	    HDMI_SPD_INFOFRAME_SIZE;
	uint8_t *p = buf;
	int ret;

	KASSERT(frame->header.length == HDMI_SPD_INFOFRAME_SIZE);

	ret = hdmi_infoframe_header_pack(&frame->header, length, p, size);
	if (ret < 0)
		return ret;
	KASSERT(ret == HDMI_INFOFRAME_HEADER_SIZE);
	p += HDMI_INFOFRAME_HEADER_SIZE;
	size -= HDMI_INFOFRAME_HEADER_SIZE;

	(void)memcpy(&p[0], frame->vendor, 8);
	(void)memcpy(&p[8], frame->product, 16);
	p[24] = frame->sdi;

	hdmi_infoframe_checksum(buf, length);

	return length;
}

#define	HDMI_IEEE_OUI	0x000c03

struct hdmi_vendor_infoframe {
	struct hdmi_infoframe_header	header;
	uint32_t			oui;
	uint8_t				vic;
	enum hdmi_3d_structure		s3d_struct;
	unsigned			s3d_ext_data;
};

union hdmi_vendor_any_infoframe {
	struct {
		struct hdmi_infoframe_header	header;
		uint32_t			oui;
	}				any;
	struct hdmi_vendor_infoframe	hdmi;
};

static inline int
hdmi_vendor_infoframe_init(struct hdmi_vendor_infoframe *frame)
{
	static const struct hdmi_vendor_infoframe zero_frame;

	*frame = zero_frame;

	hdmi_infoframe_header_init(&frame->header, HDMI_INFOFRAME_TYPE_VENDOR,
	    1, 0 /* depends on s3d_struct */);

	frame->oui = HDMI_IEEE_OUI;
	frame->s3d_struct = HDMI_3D_STRUCTURE_INVALID;

	return 0;
}

static inline int
hdmi_vendor_infoframe_pack(const struct hdmi_vendor_infoframe *frame,
    void *buf, size_t size)
{
	uint8_t *p = buf;
	size_t length;
	int ret;

	/* Exactly one must be supplied.  */
	if ((frame->vic == 0) ==
	    (frame->s3d_struct == HDMI_3D_STRUCTURE_INVALID))
		return -EINVAL;

	length = HDMI_INFOFRAME_HEADER_SIZE + 6;
	if (frame->s3d_struct >= HDMI_3D_STRUCTURE_SIDE_BY_SIDE_HALF)
		length += 1;

	ret = hdmi_infoframe_header_pack(&frame->header, length, p, size);
	if (ret < 0)
		return ret;
	KASSERT(ret == HDMI_INFOFRAME_HEADER_SIZE);
	p += HDMI_INFOFRAME_HEADER_SIZE;
	size -= HDMI_INFOFRAME_HEADER_SIZE;

	p[0] = 0x03;
	p[1] = 0x0c;
	p[2] = 0x00;

	if (frame->vic == 0) {
		p[3] = __SHIFTIN(0x2, __BITS(6,5));
		p[4] = __SHIFTIN(frame->s3d_struct, __BITS(7,4));
		if (frame->s3d_struct >= HDMI_3D_STRUCTURE_SIDE_BY_SIDE_HALF)
			p[9] = __SHIFTIN(frame->s3d_ext_data, __BITS(7,4));
	} else {
		p[3] = __SHIFTIN(0x1, __BITS(6,5));
		p[4] = frame->vic;
	}

	hdmi_infoframe_checksum(buf, length);

	return length;
}

union hdmi_infoframe {
	struct hdmi_infoframe_header	any;
	struct hdmi_avi_infoframe	avi;
	struct hdmi_spd_infoframe	spd;
	union hdmi_vendor_any_infoframe	vendor;
};

static inline ssize_t
hdmi_infoframe_pack(union hdmi_infoframe *frame, void *buf, size_t size)
{

	switch (frame->any.type) {
	case HDMI_INFOFRAME_TYPE_AVI:
		return hdmi_avi_infoframe_pack(&frame->avi, buf, size);
	case HDMI_INFOFRAME_TYPE_SPD:
		return hdmi_spd_infoframe_pack(&frame->spd, buf, size);
	case HDMI_INFOFRAME_TYPE_VENDOR:
		return hdmi_vendor_infoframe_pack(&frame->vendor.hdmi, buf,
		    size);
	default:
		return -EINVAL;
	}
}

#endif	/* _LINUX_HDMI_H_ */
