/*
 * Copyright (c) 2014 Jiri Svoboda
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * - 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.
 * - The name of the author may not be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
 */

/** @addtogroup hdaudio
 * @{
 */
/** @file High Definition Audio codec
 */

#include <async.h>
#include <bitops.h>
#include <ddf/log.h>
#include <errno.h>
#include <stdlib.h>

#include "codec.h"
#include "hdactl.h"
#include "spec/codec.h"
#include "spec/fmt.h"
#include "stream.h"

static int hda_ccmd(hda_codec_t *codec, int node, uint32_t vid, uint32_t payload,
    uint32_t *resp)
{
	uint32_t verb;
	uint32_t myresp;

	if (resp == NULL)
		resp = &myresp;

	if ((vid & 0x700) != 0) {
		verb = (codec->address << 28) |
		    ((node & 0x1ff) << 20) |
		    ((vid & 0xfff) << 8) |
		    (payload & 0xff);
	} else {
		verb = (codec->address << 28) |
		    ((node & 0x1ff) << 20) |
		    ((vid & 0xf) << 16) |
		    (payload & 0xffff);
	}
	int rc = hda_cmd(codec->hda, verb, resp);

	if (resp != NULL) {
		ddf_msg(LVL_NOTE, "verb 0x%" PRIx32 " -> 0x%" PRIx32, verb,
		    *resp);
	} else {
		ddf_msg(LVL_NOTE, "verb 0x%" PRIx32, verb);
	}

	return rc;
}

static int hda_get_parameter(hda_codec_t *codec, int node, hda_param_id_t param,
    uint32_t *resp)
{
	return hda_ccmd(codec, node, hda_param_get, param, resp);
}

static int hda_get_subnc(hda_codec_t *codec, int node, int *startnode,
    int *nodecount)
{
	int rc;
	uint32_t resp;

	rc = hda_get_parameter(codec, node, hda_sub_nc, &resp);
	if (rc != EOK)
		return rc;

	*startnode = BIT_RANGE_EXTRACT(uint32_t, subnc_startnode_h,
	    subnc_startnode_l, resp);
	*nodecount = BIT_RANGE_EXTRACT(uint32_t, subnc_nodecount_h,
	    subnc_nodecount_l, resp);

	return EOK;
}

/** Get Function Group Type */
static int hda_get_fgrp_type(hda_codec_t *codec, int node, bool *unsol,
    hda_fgrp_type_t *type)
{
	int rc;
	uint32_t resp;

	rc = hda_get_parameter(codec, node, hda_fgrp_type, &resp);
	if (rc != EOK)
		return rc;

	*unsol = (resp & BIT_V(uint32_t, fgrpt_unsol)) != 0;
	*type = BIT_RANGE_EXTRACT(uint32_t, fgrpt_type_h, fgrpt_type_l, resp);

	return EOK;
}

static int hda_get_clist_len(hda_codec_t *codec, int node, bool *longform,
    int *items)
{
	int rc;
	uint32_t resp;

	rc = hda_get_parameter(codec, node, hda_clist_len, &resp);
	if (rc != EOK)
		return rc;

//	ddf_msg(LVL_NOTE, "hda_get_clist_len: resp=0x%x", resp);
	*longform = resp & BIT_V(uint32_t, cll_longform);
	*items = resp & BIT_RANGE_EXTRACT(uint32_t, cll_len_h, cll_len_l, resp);
	return EOK;
}

static int hda_get_clist_entry(hda_codec_t *codec, int node, int n, uint32_t *resp)
{
	return hda_ccmd(codec, node, hda_clist_entry_get, n, resp);
}

/** Get Suppported PCM Size, Rates */
static int hda_get_supp_rates(hda_codec_t *codec, int node, uint32_t *rates)
{
	return hda_get_parameter(codec, node, hda_supp_rates, rates);
}

/** Get Suppported Stream Formats */
static int hda_get_supp_formats(hda_codec_t *codec, int node, uint32_t *fmts)
{
	return hda_get_parameter(codec, node, hda_supp_formats, fmts);
}

static int hda_set_converter_fmt(hda_codec_t *codec, int node, uint16_t fmt)
{
	return hda_ccmd(codec, node, hda_converter_fmt_set, fmt, NULL);
}

static int hda_set_converter_ctl(hda_codec_t *codec, int node, uint8_t stream,
    uint8_t channel)
{
	uint32_t ctl;

	ctl = (stream << cctl_stream_l) | (channel << cctl_channel_l);
	return hda_ccmd(codec, node, hda_converter_ctl_set, ctl, NULL);
}

/** Get Audio Widget Capabilities */
static int hda_get_aw_caps(hda_codec_t *codec, int node,
    hda_awidget_type_t *type, uint32_t *caps)
{
	int rc;
	uint32_t resp;

	rc = hda_get_parameter(codec, node, hda_aw_caps, &resp);
	if (rc != EOK)
		return rc;

	*type = BIT_RANGE_EXTRACT(uint32_t, awc_type_h, awc_type_l, resp);
	*caps = resp;

	return EOK;
}

/** Get Configuration Default */
static int hda_get_cfg_def(hda_codec_t *codec, int node, uint32_t *cfgdef)
{
	return hda_ccmd(codec, node, hda_cfg_def_get, 0, cfgdef);
}

static int hda_get_conn_sel(hda_codec_t *codec, int node, uint32_t *conn)
{
	return hda_ccmd(codec, node, hda_conn_sel_get, 0, conn);
}

/** Get Amplifier Gain / Mute  */
static int hda_get_amp_gain_mute(hda_codec_t *codec, int node, uint16_t payload,
    uint32_t *resp)
{
//	ddf_msg(LVL_NOTE, "hda_get_amp_gain_mute(codec, %d, %x)",
//	    node, payload);
	int rc = hda_ccmd(codec, node, hda_amp_gain_mute_get, payload, resp);
//	ddf_msg(LVL_NOTE, "hda_get_amp_gain_mute(codec, %d, %x, resp=%x)",
//	    node, payload, *resp);
	return rc;
}

static int hda_set_amp_gain_mute(hda_codec_t *codec, int node, uint16_t payload)
{
//	ddf_msg(LVL_NOTE, "hda_set_amp_gain_mute(codec, %d, %x)",
//	    node, payload);
	return hda_ccmd(codec, node, hda_amp_gain_mute_set, payload, NULL);
}

static int hda_set_out_amp_max(hda_codec_t *codec, uint8_t aw)
{
	uint32_t ampcaps;
	uint32_t gmleft, gmright;
	uint32_t offset;
	int rc;

	rc = hda_get_parameter(codec, aw,
	    hda_out_amp_caps, &ampcaps);
	if (rc != EOK)
		goto error;

	offset = ampcaps & 0x7f;
	ddf_msg(LVL_NOTE, "out amp caps 0x%x (offset=0x%x)",
	    ampcaps, offset);

	rc = hda_set_amp_gain_mute(codec, aw, 0xb000 + offset/2);
	if (rc != EOK)
		goto error;

	rc = hda_get_amp_gain_mute(codec, aw, 0x8000, &gmleft);
	if (rc != EOK)
		goto error;

	rc = hda_get_amp_gain_mute(codec, aw, 0xa000, &gmright);
	if (rc != EOK)
		goto error;

	ddf_msg(LVL_NOTE, "gain/mute: L:0x%x R:0x%x", gmleft, gmright);

	return EOK;
error:
	return rc;
}

static int hda_set_in_amp_max(hda_codec_t *codec, uint8_t aw)
{
	uint32_t ampcaps;
	uint32_t gmleft, gmright;
	uint32_t offset;
	int i;
	int rc;

	rc = hda_get_parameter(codec, aw,
	    hda_out_amp_caps, &ampcaps);
	if (rc != EOK)
		goto error;

	offset = ampcaps & 0x7f;
	ddf_msg(LVL_NOTE, "in amp caps 0x%x (offset=0x%x)", ampcaps, offset);

	for (i = 0; i < 15; i++) {
		rc = hda_set_amp_gain_mute(codec, aw, 0x7000 + (i << 8) + offset/2);
		if (rc != EOK)
			goto error;

		rc = hda_get_amp_gain_mute(codec, aw, 0x0000 + i, &gmleft);
		if (rc != EOK)
			goto error;

		rc = hda_get_amp_gain_mute(codec, aw, 0x2000 + i, &gmright);
		if (rc != EOK)
			goto error;

		ddf_msg(LVL_NOTE, "in:%d gain/mute: L:0x%x R:0x%x",
		    i, gmleft, gmright);
	}

	return EOK;
error:
	return rc;
}

static int hda_clist_dump(hda_codec_t *codec, uint8_t aw)
{
	int rc;
	bool longform;
	int len;
	uint32_t resp;
	uint32_t mask;
	uint32_t cidx;
	int shift;
	int epresp;
	int i, j;

	ddf_msg(LVL_NOTE, "Connections for widget %d:", aw);

	rc = hda_get_clist_len(codec, aw, &longform, &len);
	if (rc != EOK) {
		ddf_msg(LVL_ERROR, "Failed getting connection list length.");
		return rc;
	}

	if (len > 1) {
		rc = hda_get_conn_sel(codec, aw, &cidx);
		if (rc != EOK) {
			ddf_msg(LVL_ERROR, "Failed getting connection select");
			return rc;
		}
	} else {
		cidx = 0;
	}

//	ddf_msg(LVL_NOTE, "longform:%d len:%d", longform, len);

	if (longform) {
		epresp = 2;
		mask = 0xffff;
		shift = 16;
	} else {
		epresp = 4;
		mask = 0xff;
		shift = 8;
	}

	i = 0;
	while (i < len) {
		rc = hda_get_clist_entry(codec, aw, i, &resp);
		if (rc != EOK) {
			ddf_msg(LVL_ERROR, "Failed getting connection list entry.");
			return rc;
		}

		for (j = 0; j < epresp && i < len; j++) {
			ddf_msg(LVL_NOTE, "<- %d%s", resp & mask,
			    (int)cidx == i ? " *** current *** " : "");
			resp = resp << shift;
			++i;
		}

	}

	return rc;
}

hda_codec_t *hda_codec_init(hda_t *hda, uint8_t address)
{
	hda_codec_t *codec;
	int rc;
	int sfg, nfg;
	int saw, naw;
	int fg, aw;
	bool unsol;
	hda_fgrp_type_t grptype;
	hda_awidget_type_t awtype;
	uint32_t awcaps;
	uint32_t cfgdef;
	uint32_t rates;
	uint32_t formats;

	codec = calloc(1, sizeof(hda_codec_t));
	if (codec == NULL)
		return NULL;

	codec->hda = hda;
	codec->address = address;

	rc = hda_get_subnc(codec, 0, &sfg, &nfg);
	if (rc != EOK)
		goto error;

	ddf_msg(LVL_NOTE, "hda_get_subnc -> %d", rc);
	ddf_msg(LVL_NOTE, "sfg=%d nfg=%d", sfg, nfg);

	for (fg = sfg; fg < sfg + nfg; fg++) {
		ddf_msg(LVL_NOTE, "Enumerate FG %d", fg);

		rc = hda_get_fgrp_type(codec, fg, &unsol, &grptype);
		if (rc != EOK)
			goto error;

		ddf_msg(LVL_NOTE, "hda_get_fgrp_type -> %d", rc);
		ddf_msg(LVL_NOTE, "unsol: %d, grptype: %d", unsol, grptype);

		rc = hda_get_subnc(codec, fg, &saw, &naw);
		if (rc != EOK)
			goto error;

		ddf_msg(LVL_NOTE, "hda_get_subnc -> %d", rc);
		ddf_msg(LVL_NOTE, "saw=%d baw=%d", saw, naw);

		for (aw = saw; aw < saw + naw; aw++) {
			rc = hda_get_aw_caps(codec, aw, &awtype, &awcaps);
			if (rc != EOK)
				goto error;
			ddf_msg(LVL_NOTE, "aw %d: type=0x%x caps=0x%x",
			    aw, awtype, awcaps);

			switch (awtype) {
			case awt_audio_input:
			case awt_audio_mixer:
			case awt_audio_selector:
			case awt_pin_complex:
			case awt_power_widget:
				rc = hda_clist_dump(codec, aw);
				if (rc != EOK)
					goto error;
				break;
			default:
				break;
			}

			if (awtype == awt_pin_complex) {
				rc = hda_get_cfg_def(codec, aw, &cfgdef);
				if (rc != EOK)
					goto error;
				ddf_msg(LVL_NOTE, "aw %d: PIN cdfgef=0x%x",
				    aw, cfgdef);

			} else if (awtype == awt_audio_output) {
				codec->out_aw_list[codec->out_aw_num++] = aw;

				rc = hda_get_supp_rates(codec, aw, &rates);
				if (rc != EOK)
					goto error;

				rc = hda_get_supp_formats(codec, aw, &formats);
				if (rc != EOK)
					goto error;

				ddf_msg(LVL_NOTE, "Output widget %d: rates=0x%x formats=0x%x",
				    aw, rates, formats);
			}

			if ((awcaps & BIT_V(uint32_t, awc_out_amp_present)) != 0)
				hda_set_out_amp_max(codec, aw);

			if ((awcaps & BIT_V(uint32_t, awc_in_amp_present)) != 0)
				hda_set_in_amp_max(codec, aw);
		}
	}

	hda_ctl_dump_info(hda->ctl);

	ddf_msg(LVL_NOTE, "Codec OK");
	return codec;
error:
	free(codec);
	return NULL;
}

void hda_codec_fini(hda_codec_t *codec)
{
	ddf_msg(LVL_NOTE, "hda_codec_fini()");
	free(codec);
}

int hda_out_converter_setup(hda_codec_t *codec, uint8_t sid)
{
	int rc;
	int out_aw;
	int i;

	for (i = 0; i < codec->out_aw_num; i++) {
		out_aw = codec->out_aw_list[i];

		/* XXX Choose appropriate parameters */
		uint32_t fmt;
		/* 48 kHz, 16-bits, 1 channel */
		fmt = (fmt_base_44khz << fmt_base) | (fmt_bits_16 << fmt_bits_l) | 1;

		/* Configure converter */

		ddf_msg(LVL_NOTE, "Configure converter format");
		rc = hda_set_converter_fmt(codec, out_aw, fmt);
		if (rc != EOK)
			goto error;

		ddf_msg(LVL_NOTE, "Configure converter stream, channel");
		rc = hda_set_converter_ctl(codec, out_aw, sid, 0);
		if (rc != EOK)
			goto error;
	}

	return EOK;
error:
	return rc;
}

/** @}
 */
