/*
 * Apple Onboard Audio driver -- layout/machine id fabric
 *
 * Copyright 2006-2008 Johannes Berg <johannes@sipsolutions.net>
 *
 * GPL v2, can be found in COPYING.
 *
 *
 * This fabric module looks for sound codecs based on the
 * layout-id or device-id property in the device tree.
 */
#include <asm/prom.h>
#include <linux/list.h>
#include <linux/module.h>
#include <linux/slab.h>
#include "../aoa.h"
#include "../soundbus/soundbus.h"

MODULE_AUTHOR("Johannes Berg <johannes@sipsolutions.net>");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Layout-ID fabric for snd-aoa");

#define MAX_CODECS_PER_BUS	2

/* These are the connections the layout fabric
 * knows about. It doesn't really care about the
 * input ones, but I thought I'd separate them
 * to give them proper names. The thing is that
 * Apple usually will distinguish the active output
 * by GPIOs, while the active input is set directly
 * on the codec. Hence we here tell the codec what
 * we think is connected. This information is hard-
 * coded below ... */
#define CC_SPEAKERS	(1<<0)
#define CC_HEADPHONE	(1<<1)
#define CC_LINEOUT	(1<<2)
#define CC_DIGITALOUT	(1<<3)
#define CC_LINEIN	(1<<4)
#define CC_MICROPHONE	(1<<5)
#define CC_DIGITALIN	(1<<6)
/* pretty bogus but users complain...
 * This is a flag saying that the LINEOUT
 * should be renamed to HEADPHONE.
 * be careful with input detection! */
#define CC_LINEOUT_LABELLED_HEADPHONE	(1<<7)

struct codec_connection {
	/* CC_ flags from above */
	int connected;
	/* codec dependent bit to be set in the aoa_codec.connected field.
	 * This intentionally doesn't have any generic flags because the
	 * fabric has to know the codec anyway and all codecs might have
	 * different connectors */
	int codec_bit;
};

struct codec_connect_info {
	char *name;
	struct codec_connection *connections;
};

#define LAYOUT_FLAG_COMBO_LINEOUT_SPDIF	(1<<0)

struct layout {
	unsigned int layout_id, device_id;
	struct codec_connect_info codecs[MAX_CODECS_PER_BUS];
	int flags;

	/* if busname is not assigned, we use 'Master' below,
	 * so that our layout table doesn't need to be filled
	 * too much.
	 * We only assign these two if we expect to find more
	 * than one soundbus, i.e. on those machines with
	 * multiple layout-ids */
	char *busname;
	int pcmid;
};

MODULE_ALIAS("sound-layout-36");
MODULE_ALIAS("sound-layout-41");
MODULE_ALIAS("sound-layout-45");
MODULE_ALIAS("sound-layout-47");
MODULE_ALIAS("sound-layout-48");
MODULE_ALIAS("sound-layout-49");
MODULE_ALIAS("sound-layout-50");
MODULE_ALIAS("sound-layout-51");
MODULE_ALIAS("sound-layout-56");
MODULE_ALIAS("sound-layout-57");
MODULE_ALIAS("sound-layout-58");
MODULE_ALIAS("sound-layout-60");
MODULE_ALIAS("sound-layout-61");
MODULE_ALIAS("sound-layout-62");
MODULE_ALIAS("sound-layout-64");
MODULE_ALIAS("sound-layout-65");
MODULE_ALIAS("sound-layout-66");
MODULE_ALIAS("sound-layout-67");
MODULE_ALIAS("sound-layout-68");
MODULE_ALIAS("sound-layout-69");
MODULE_ALIAS("sound-layout-70");
MODULE_ALIAS("sound-layout-72");
MODULE_ALIAS("sound-layout-76");
MODULE_ALIAS("sound-layout-80");
MODULE_ALIAS("sound-layout-82");
MODULE_ALIAS("sound-layout-84");
MODULE_ALIAS("sound-layout-86");
MODULE_ALIAS("sound-layout-90");
MODULE_ALIAS("sound-layout-92");
MODULE_ALIAS("sound-layout-94");
MODULE_ALIAS("sound-layout-96");
MODULE_ALIAS("sound-layout-98");
MODULE_ALIAS("sound-layout-100");

MODULE_ALIAS("aoa-device-id-14");
MODULE_ALIAS("aoa-device-id-22");
MODULE_ALIAS("aoa-device-id-35");

/* onyx with all but microphone connected */
static struct codec_connection onyx_connections_nomic[] = {
	{
		.connected = CC_SPEAKERS | CC_HEADPHONE | CC_LINEOUT,
		.codec_bit = 0,
	},
	{
		.connected = CC_DIGITALOUT,
		.codec_bit = 1,
	},
	{
		.connected = CC_LINEIN,
		.codec_bit = 2,
	},
	{} /* terminate array by .connected == 0 */
};

/* onyx on machines without headphone */
static struct codec_connection onyx_connections_noheadphones[] = {
	{
		.connected = CC_SPEAKERS | CC_LINEOUT |
			     CC_LINEOUT_LABELLED_HEADPHONE,
		.codec_bit = 0,
	},
	{
		.connected = CC_DIGITALOUT,
		.codec_bit = 1,
	},
	/* FIXME: are these correct? probably not for all the machines
	 * below ... If not this will need separating. */
	{
		.connected = CC_LINEIN,
		.codec_bit = 2,
	},
	{
		.connected = CC_MICROPHONE,
		.codec_bit = 3,
	},
	{} /* terminate array by .connected == 0 */
};

/* onyx on machines with real line-out */
static struct codec_connection onyx_connections_reallineout[] = {
	{
		.connected = CC_SPEAKERS | CC_LINEOUT | CC_HEADPHONE,
		.codec_bit = 0,
	},
	{
		.connected = CC_DIGITALOUT,
		.codec_bit = 1,
	},
	{
		.connected = CC_LINEIN,
		.codec_bit = 2,
	},
	{} /* terminate array by .connected == 0 */
};

/* tas on machines without line out */
static struct codec_connection tas_connections_nolineout[] = {
	{
		.connected = CC_SPEAKERS | CC_HEADPHONE,
		.codec_bit = 0,
	},
	{
		.connected = CC_LINEIN,
		.codec_bit = 2,
	},
	{
		.connected = CC_MICROPHONE,
		.codec_bit = 3,
	},
	{} /* terminate array by .connected == 0 */
};

/* tas on machines with neither line out nor line in */
static struct codec_connection tas_connections_noline[] = {
	{
		.connected = CC_SPEAKERS | CC_HEADPHONE,
		.codec_bit = 0,
	},
	{
		.connected = CC_MICROPHONE,
		.codec_bit = 3,
	},
	{} /* terminate array by .connected == 0 */
};

/* tas on machines without microphone */
static struct codec_connection tas_connections_nomic[] = {
	{
		.connected = CC_SPEAKERS | CC_HEADPHONE | CC_LINEOUT,
		.codec_bit = 0,
	},
	{
		.connected = CC_LINEIN,
		.codec_bit = 2,
	},
	{} /* terminate array by .connected == 0 */
};

/* tas on machines with everything connected */
static struct codec_connection tas_connections_all[] = {
	{
		.connected = CC_SPEAKERS | CC_HEADPHONE | CC_LINEOUT,
		.codec_bit = 0,
	},
	{
		.connected = CC_LINEIN,
		.codec_bit = 2,
	},
	{
		.connected = CC_MICROPHONE,
		.codec_bit = 3,
	},
	{} /* terminate array by .connected == 0 */
};

static struct codec_connection toonie_connections[] = {
	{
		.connected = CC_SPEAKERS | CC_HEADPHONE,
		.codec_bit = 0,
	},
	{} /* terminate array by .connected == 0 */
};

static struct codec_connection topaz_input[] = {
	{
		.connected = CC_DIGITALIN,
		.codec_bit = 0,
	},
	{} /* terminate array by .connected == 0 */
};

static struct codec_connection topaz_output[] = {
	{
		.connected = CC_DIGITALOUT,
		.codec_bit = 1,
	},
	{} /* terminate array by .connected == 0 */
};

static struct codec_connection topaz_inout[] = {
	{
		.connected = CC_DIGITALIN,
		.codec_bit = 0,
	},
	{
		.connected = CC_DIGITALOUT,
		.codec_bit = 1,
	},
	{} /* terminate array by .connected == 0 */
};

static struct layout layouts[] = {
	/* last PowerBooks (15" Oct 2005) */
	{ .layout_id = 82,
	  .flags = LAYOUT_FLAG_COMBO_LINEOUT_SPDIF,
	  .codecs[0] = {
		.name = "onyx",
		.connections = onyx_connections_noheadphones,
	  },
	  .codecs[1] = {
		.name = "topaz",
		.connections = topaz_input,
	  },
	},
	/* PowerMac9,1 */
	{ .layout_id = 60,
	  .codecs[0] = {
		.name = "onyx",
		.connections = onyx_connections_reallineout,
	  },
	},
	/* PowerMac9,1 */
	{ .layout_id = 61,
	  .codecs[0] = {
		.name = "topaz",
		.connections = topaz_input,
	  },
	},
	/* PowerBook5,7 */
	{ .layout_id = 64,
	  .flags = LAYOUT_FLAG_COMBO_LINEOUT_SPDIF,
	  .codecs[0] = {
		.name = "onyx",
		.connections = onyx_connections_noheadphones,
	  },
	},
	/* PowerBook5,7 */
	{ .layout_id = 65,
	  .codecs[0] = {
		.name = "topaz",
		.connections = topaz_input,
	  },
	},
	/* PowerBook5,9 [17" Oct 2005] */
	{ .layout_id = 84,
	  .flags = LAYOUT_FLAG_COMBO_LINEOUT_SPDIF,
	  .codecs[0] = {
		.name = "onyx",
		.connections = onyx_connections_noheadphones,
	  },
	  .codecs[1] = {
		.name = "topaz",
		.connections = topaz_input,
	  },
	},
	/* PowerMac8,1 */
	{ .layout_id = 45,
	  .codecs[0] = {
		.name = "onyx",
		.connections = onyx_connections_noheadphones,
	  },
	  .codecs[1] = {
		.name = "topaz",
		.connections = topaz_input,
	  },
	},
	/* Quad PowerMac (analog in, analog/digital out) */
	{ .layout_id = 68,
	  .codecs[0] = {
		.name = "onyx",
		.connections = onyx_connections_nomic,
	  },
	},
	/* Quad PowerMac (digital in) */
	{ .layout_id = 69,
	  .codecs[0] = {
		.name = "topaz",
		.connections = topaz_input,
	  },
	  .busname = "digital in", .pcmid = 1 },
	/* Early 2005 PowerBook (PowerBook 5,6) */
	{ .layout_id = 70,
	  .codecs[0] = {
		.name = "tas",
		.connections = tas_connections_nolineout,
	  },
	},
	/* PowerBook 5,4 */
	{ .layout_id = 51,
	  .codecs[0] = {
		.name = "tas",
		.connections = tas_connections_nolineout,
	  },
	},
	/* PowerBook6,7 */
	{ .layout_id = 80,
	  .codecs[0] = {
		.name = "tas",
		.connections = tas_connections_noline,
	  },
	},
	/* PowerBook6,8 */
	{ .layout_id = 72,
	  .codecs[0] = {
		.name = "tas",
		.connections = tas_connections_nolineout,
	  },
	},
	/* PowerMac8,2 */
	{ .layout_id = 86,
	  .codecs[0] = {
		.name = "onyx",
		.connections = onyx_connections_nomic,
	  },
	  .codecs[1] = {
		.name = "topaz",
		.connections = topaz_input,
	  },
	},
	/* PowerBook6,7 */
	{ .layout_id = 92,
	  .codecs[0] = {
		.name = "tas",
		.connections = tas_connections_nolineout,
	  },
	},
	/* PowerMac10,1 (Mac Mini) */
	{ .layout_id = 58,
	  .codecs[0] = {
		.name = "toonie",
		.connections = toonie_connections,
	  },
	},
	{
	  .layout_id = 96,
	  .codecs[0] = {
	  	.name = "onyx",
	  	.connections = onyx_connections_noheadphones,
	  },
	},
	/* unknown, untested, but this comes from Apple */
	{ .layout_id = 41,
	  .codecs[0] = {
		.name = "tas",
		.connections = tas_connections_all,
	  },
	},
	{ .layout_id = 36,
	  .codecs[0] = {
		.name = "tas",
		.connections = tas_connections_nomic,
	  },
	  .codecs[1] = {
		.name = "topaz",
		.connections = topaz_inout,
	  },
	},
	{ .layout_id = 47,
	  .codecs[0] = {
		.name = "onyx",
		.connections = onyx_connections_noheadphones,
	  },
	},
	{ .layout_id = 48,
	  .codecs[0] = {
		.name = "topaz",
		.connections = topaz_input,
	  },
	},
	{ .layout_id = 49,
	  .codecs[0] = {
		.name = "onyx",
		.connections = onyx_connections_nomic,
	  },
	},
	{ .layout_id = 50,
	  .codecs[0] = {
		.name = "topaz",
		.connections = topaz_input,
	  },
	},
	{ .layout_id = 56,
	  .codecs[0] = {
		.name = "onyx",
		.connections = onyx_connections_noheadphones,
	  },
	},
	{ .layout_id = 57,
	  .codecs[0] = {
		.name = "topaz",
		.connections = topaz_input,
	  },
	},
	{ .layout_id = 62,
	  .codecs[0] = {
		.name = "onyx",
		.connections = onyx_connections_noheadphones,
	  },
	  .codecs[1] = {
		.name = "topaz",
		.connections = topaz_output,
	  },
	},
	{ .layout_id = 66,
	  .codecs[0] = {
		.name = "onyx",
		.connections = onyx_connections_noheadphones,
	  },
	},
	{ .layout_id = 67,
	  .codecs[0] = {
		.name = "topaz",
		.connections = topaz_input,
	  },
	},
	{ .layout_id = 76,
	  .codecs[0] = {
		.name = "tas",
		.connections = tas_connections_nomic,
	  },
	  .codecs[1] = {
		.name = "topaz",
		.connections = topaz_inout,
	  },
	},
	{ .layout_id = 90,
	  .codecs[0] = {
		.name = "tas",
		.connections = tas_connections_noline,
	  },
	},
	{ .layout_id = 94,
	  .codecs[0] = {
		.name = "onyx",
		/* but it has an external mic?? how to select? */
		.connections = onyx_connections_noheadphones,
	  },
	},
	{ .layout_id = 98,
	  .codecs[0] = {
		.name = "toonie",
		.connections = toonie_connections,
	  },
	},
	{ .layout_id = 100,
	  .codecs[0] = {
		.name = "topaz",
		.connections = topaz_input,
	  },
	  .codecs[1] = {
		.name = "onyx",
		.connections = onyx_connections_noheadphones,
	  },
	},
	/* PowerMac3,4 */
	{ .device_id = 14,
	  .codecs[0] = {
		.name = "tas",
		.connections = tas_connections_noline,
	  },
	},
	/* PowerMac3,6 */
	{ .device_id = 22,
	  .codecs[0] = {
		.name = "tas",
		.connections = tas_connections_all,
	  },
	},
	/* PowerBook5,2 */
	{ .device_id = 35,
	  .codecs[0] = {
		.name = "tas",
		.connections = tas_connections_all,
	  },
	},
	{}
};

static struct layout *find_layout_by_id(unsigned int id)
{
	struct layout *l;

	l = layouts;
	while (l->codecs[0].name) {
		if (l->layout_id == id)
			return l;
		l++;
	}
	return NULL;
}

static struct layout *find_layout_by_device(unsigned int id)
{
	struct layout *l;

	l = layouts;
	while (l->codecs[0].name) {
		if (l->device_id == id)
			return l;
		l++;
	}
	return NULL;
}

static void use_layout(struct layout *l)
{
	int i;

	for (i=0; i<MAX_CODECS_PER_BUS; i++) {
		if (l->codecs[i].name) {
			request_module("snd-aoa-codec-%s", l->codecs[i].name);
		}
	}
	/* now we wait for the codecs to call us back */
}

struct layout_dev;

struct layout_dev_ptr {
	struct layout_dev *ptr;
};

struct layout_dev {
	struct list_head list;
	struct soundbus_dev *sdev;
	struct device_node *sound;
	struct aoa_codec *codecs[MAX_CODECS_PER_BUS];
	struct layout *layout;
	struct gpio_runtime gpio;

	/* we need these for headphone/lineout detection */
	struct snd_kcontrol *headphone_ctrl;
	struct snd_kcontrol *lineout_ctrl;
	struct snd_kcontrol *speaker_ctrl;
	struct snd_kcontrol *master_ctrl;
	struct snd_kcontrol *headphone_detected_ctrl;
	struct snd_kcontrol *lineout_detected_ctrl;

	struct layout_dev_ptr selfptr_headphone;
	struct layout_dev_ptr selfptr_lineout;

	u32 have_lineout_detect:1,
	    have_headphone_detect:1,
	    switch_on_headphone:1,
	    switch_on_lineout:1;
};

static LIST_HEAD(layouts_list);
static int layouts_list_items;
/* this can go away but only if we allow multiple cards,
 * make the fabric handle all the card stuff, etc... */
static struct layout_dev *layout_device;

#define control_info	snd_ctl_boolean_mono_info

#define AMP_CONTROL(n, description)					\
static int n##_control_get(struct snd_kcontrol *kcontrol,		\
			   struct snd_ctl_elem_value *ucontrol)		\
{									\
	struct gpio_runtime *gpio = snd_kcontrol_chip(kcontrol);	\
	if (gpio->methods && gpio->methods->get_##n)			\
		ucontrol->value.integer.value[0] =			\
			gpio->methods->get_##n(gpio);			\
	return 0;							\
}									\
static int n##_control_put(struct snd_kcontrol *kcontrol,		\
			   struct snd_ctl_elem_value *ucontrol)		\
{									\
	struct gpio_runtime *gpio = snd_kcontrol_chip(kcontrol);	\
	if (gpio->methods && gpio->methods->get_##n)			\
		gpio->methods->set_##n(gpio,				\
			!!ucontrol->value.integer.value[0]);		\
	return 1;							\
}									\
static struct snd_kcontrol_new n##_ctl = {				\
	.iface = SNDRV_CTL_ELEM_IFACE_MIXER,				\
	.name = description,						\
	.access = SNDRV_CTL_ELEM_ACCESS_READWRITE,                      \
	.info = control_info,						\
	.get = n##_control_get,						\
	.put = n##_control_put,						\
}

AMP_CONTROL(headphone, "Headphone Switch");
AMP_CONTROL(speakers, "Speakers Switch");
AMP_CONTROL(lineout, "Line-Out Switch");
AMP_CONTROL(master, "Master Switch");

static int detect_choice_get(struct snd_kcontrol *kcontrol,
			     struct snd_ctl_elem_value *ucontrol)
{
	struct layout_dev *ldev = snd_kcontrol_chip(kcontrol);

	switch (kcontrol->private_value) {
	case 0:
		ucontrol->value.integer.value[0] = ldev->switch_on_headphone;
		break;
	case 1:
		ucontrol->value.integer.value[0] = ldev->switch_on_lineout;
		break;
	default:
		return -ENODEV;
	}
	return 0;
}

static int detect_choice_put(struct snd_kcontrol *kcontrol,
			     struct snd_ctl_elem_value *ucontrol)
{
	struct layout_dev *ldev = snd_kcontrol_chip(kcontrol);

	switch (kcontrol->private_value) {
	case 0:
		ldev->switch_on_headphone = !!ucontrol->value.integer.value[0];
		break;
	case 1:
		ldev->switch_on_lineout = !!ucontrol->value.integer.value[0];
		break;
	default:
		return -ENODEV;
	}
	return 1;
}

static struct snd_kcontrol_new headphone_detect_choice = {
	.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
	.name = "Headphone Detect Autoswitch",
	.info = control_info,
	.access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
	.get = detect_choice_get,
	.put = detect_choice_put,
	.private_value = 0,
};

static struct snd_kcontrol_new lineout_detect_choice = {
	.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
	.name = "Line-Out Detect Autoswitch",
	.info = control_info,
	.access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
	.get = detect_choice_get,
	.put = detect_choice_put,
	.private_value = 1,
};

static int detected_get(struct snd_kcontrol *kcontrol,
			struct snd_ctl_elem_value *ucontrol)
{
	struct layout_dev *ldev = snd_kcontrol_chip(kcontrol);
	int v;

	switch (kcontrol->private_value) {
	case 0:
		v = ldev->gpio.methods->get_detect(&ldev->gpio,
						   AOA_NOTIFY_HEADPHONE);
		break;
	case 1:
		v = ldev->gpio.methods->get_detect(&ldev->gpio,
						   AOA_NOTIFY_LINE_OUT);
		break;
	default:
		return -ENODEV;
	}
	ucontrol->value.integer.value[0] = v;
	return 0;
}

static struct snd_kcontrol_new headphone_detected = {
	.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
	.name = "Headphone Detected",
	.info = control_info,
	.access = SNDRV_CTL_ELEM_ACCESS_READ,
	.get = detected_get,
	.private_value = 0,
};

static struct snd_kcontrol_new lineout_detected = {
	.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
	.name = "Line-Out Detected",
	.info = control_info,
	.access = SNDRV_CTL_ELEM_ACCESS_READ,
	.get = detected_get,
	.private_value = 1,
};

static int check_codec(struct aoa_codec *codec,
		       struct layout_dev *ldev,
		       struct codec_connect_info *cci)
{
	const u32 *ref;
	char propname[32];
	struct codec_connection *cc;

	/* if the codec has a 'codec' node, we require a reference */
	if (codec->node && (strcmp(codec->node->name, "codec") == 0)) {
		snprintf(propname, sizeof(propname),
			 "platform-%s-codec-ref", codec->name);
		ref = of_get_property(ldev->sound, propname, NULL);
		if (!ref) {
			printk(KERN_INFO "snd-aoa-fabric-layout: "
				"required property %s not present\n", propname);
			return -ENODEV;
		}
		if (*ref != codec->node->phandle) {
			printk(KERN_INFO "snd-aoa-fabric-layout: "
				"%s doesn't match!\n", propname);
			return -ENODEV;
		}
	} else {
		if (layouts_list_items != 1) {
			printk(KERN_INFO "snd-aoa-fabric-layout: "
				"more than one soundbus, but no references.\n");
			return -ENODEV;
		}
	}
	codec->soundbus_dev = ldev->sdev;
	codec->gpio = &ldev->gpio;

	cc = cci->connections;
	if (!cc)
		return -EINVAL;

	printk(KERN_INFO "snd-aoa-fabric-layout: can use this codec\n");

	codec->connected = 0;
	codec->fabric_data = cc;

	while (cc->connected) {
		codec->connected |= 1<<cc->codec_bit;
		cc++;
	}

	return 0;
}

static int layout_found_codec(struct aoa_codec *codec)
{
	struct layout_dev *ldev;
	int i;

	list_for_each_entry(ldev, &layouts_list, list) {
		for (i=0; i<MAX_CODECS_PER_BUS; i++) {
			if (!ldev->layout->codecs[i].name)
				continue;
			if (strcmp(ldev->layout->codecs[i].name, codec->name) == 0) {
				if (check_codec(codec,
						ldev,
						&ldev->layout->codecs[i]) == 0)
					return 0;
			}
		}
	}
	return -ENODEV;
}

static void layout_remove_codec(struct aoa_codec *codec)
{
	int i;
	/* here remove the codec from the layout dev's
	 * codec reference */

	codec->soundbus_dev = NULL;
	codec->gpio = NULL;
	for (i=0; i<MAX_CODECS_PER_BUS; i++) {
	}
}

static void layout_notify(void *data)
{
	struct layout_dev_ptr *dptr = data;
	struct layout_dev *ldev;
	int v, update;
	struct snd_kcontrol *detected, *c;
	struct snd_card *card = aoa_get_card();

	ldev = dptr->ptr;
	if (data == &ldev->selfptr_headphone) {
		v = ldev->gpio.methods->get_detect(&ldev->gpio, AOA_NOTIFY_HEADPHONE);
		detected = ldev->headphone_detected_ctrl;
		update = ldev->switch_on_headphone;
		if (update) {
			ldev->gpio.methods->set_speakers(&ldev->gpio, !v);
			ldev->gpio.methods->set_headphone(&ldev->gpio, v);
			ldev->gpio.methods->set_lineout(&ldev->gpio, 0);
		}
	} else if (data == &ldev->selfptr_lineout) {
		v = ldev->gpio.methods->get_detect(&ldev->gpio, AOA_NOTIFY_LINE_OUT);
		detected = ldev->lineout_detected_ctrl;
		update = ldev->switch_on_lineout;
		if (update) {
			ldev->gpio.methods->set_speakers(&ldev->gpio, !v);
			ldev->gpio.methods->set_headphone(&ldev->gpio, 0);
			ldev->gpio.methods->set_lineout(&ldev->gpio, v);
		}
	} else
		return;

	if (detected)
		snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, &detected->id);
	if (update) {
		c = ldev->headphone_ctrl;
		if (c)
			snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, &c->id);
		c = ldev->speaker_ctrl;
		if (c)
			snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, &c->id);
		c = ldev->lineout_ctrl;
		if (c)
			snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, &c->id);
	}
}

static void layout_attached_codec(struct aoa_codec *codec)
{
	struct codec_connection *cc;
	struct snd_kcontrol *ctl;
	int headphones, lineout;
	struct layout_dev *ldev = layout_device;

	/* need to add this codec to our codec array! */

	cc = codec->fabric_data;

	headphones = codec->gpio->methods->get_detect(codec->gpio,
						      AOA_NOTIFY_HEADPHONE);
 	lineout = codec->gpio->methods->get_detect(codec->gpio,
						   AOA_NOTIFY_LINE_OUT);

	if (codec->gpio->methods->set_master) {
		ctl = snd_ctl_new1(&master_ctl, codec->gpio);
		ldev->master_ctrl = ctl;
		aoa_snd_ctl_add(ctl);
	}
	while (cc->connected) {
		if (cc->connected & CC_SPEAKERS) {
			if (headphones <= 0 && lineout <= 0)
				ldev->gpio.methods->set_speakers(codec->gpio, 1);
			ctl = snd_ctl_new1(&speakers_ctl, codec->gpio);
			ldev->speaker_ctrl = ctl;
			aoa_snd_ctl_add(ctl);
		}
		if (cc->connected & CC_HEADPHONE) {
			if (headphones == 1)
				ldev->gpio.methods->set_headphone(codec->gpio, 1);
			ctl = snd_ctl_new1(&headphone_ctl, codec->gpio);
			ldev->headphone_ctrl = ctl;
			aoa_snd_ctl_add(ctl);
			ldev->have_headphone_detect =
				!ldev->gpio.methods
					->set_notify(&ldev->gpio,
						     AOA_NOTIFY_HEADPHONE,
						     layout_notify,
						     &ldev->selfptr_headphone);
			if (ldev->have_headphone_detect) {
				ctl = snd_ctl_new1(&headphone_detect_choice,
						   ldev);
				aoa_snd_ctl_add(ctl);
				ctl = snd_ctl_new1(&headphone_detected,
						   ldev);
				ldev->headphone_detected_ctrl = ctl;
				aoa_snd_ctl_add(ctl);
			}
		}
		if (cc->connected & CC_LINEOUT) {
			if (lineout == 1)
				ldev->gpio.methods->set_lineout(codec->gpio, 1);
			ctl = snd_ctl_new1(&lineout_ctl, codec->gpio);
			if (cc->connected & CC_LINEOUT_LABELLED_HEADPHONE)
				strlcpy(ctl->id.name,
					"Headphone Switch", sizeof(ctl->id.name));
			ldev->lineout_ctrl = ctl;
			aoa_snd_ctl_add(ctl);
			ldev->have_lineout_detect =
				!ldev->gpio.methods
					->set_notify(&ldev->gpio,
						     AOA_NOTIFY_LINE_OUT,
						     layout_notify,
						     &ldev->selfptr_lineout);
			if (ldev->have_lineout_detect) {
				ctl = snd_ctl_new1(&lineout_detect_choice,
						   ldev);
				if (cc->connected & CC_LINEOUT_LABELLED_HEADPHONE)
					strlcpy(ctl->id.name,
						"Headphone Detect Autoswitch",
						sizeof(ctl->id.name));
				aoa_snd_ctl_add(ctl);
				ctl = snd_ctl_new1(&lineout_detected,
						   ldev);
				if (cc->connected & CC_LINEOUT_LABELLED_HEADPHONE)
					strlcpy(ctl->id.name,
						"Headphone Detected",
						sizeof(ctl->id.name));
				ldev->lineout_detected_ctrl = ctl;
				aoa_snd_ctl_add(ctl);
			}
		}
		cc++;
	}
	/* now update initial state */
	if (ldev->have_headphone_detect)
		layout_notify(&ldev->selfptr_headphone);
	if (ldev->have_lineout_detect)
		layout_notify(&ldev->selfptr_lineout);
}

static struct aoa_fabric layout_fabric = {
	.name = "SoundByLayout",
	.owner = THIS_MODULE,
	.found_codec = layout_found_codec,
	.remove_codec = layout_remove_codec,
	.attached_codec = layout_attached_codec,
};

static int aoa_fabric_layout_probe(struct soundbus_dev *sdev)
{
	struct device_node *sound = NULL;
	const unsigned int *id;
	struct layout *layout = NULL;
	struct layout_dev *ldev = NULL;
	int err;

	/* hm, currently we can only have one ... */
	if (layout_device)
		return -ENODEV;

	/* by breaking out we keep a reference */
	while ((sound = of_get_next_child(sdev->ofdev.dev.of_node, sound))) {
		if (sound->type && strcasecmp(sound->type, "soundchip") == 0)
			break;
	}
	if (!sound)
		return -ENODEV;

	id = of_get_property(sound, "layout-id", NULL);
	if (id) {
		layout = find_layout_by_id(*id);
	} else {
		id = of_get_property(sound, "device-id", NULL);
		if (id)
			layout = find_layout_by_device(*id);
	}

	if (!layout) {
		printk(KERN_ERR "snd-aoa-fabric-layout: unknown layout\n");
		goto outnodev;
	}

	ldev = kzalloc(sizeof(struct layout_dev), GFP_KERNEL);
	if (!ldev)
		goto outnodev;

	layout_device = ldev;
	ldev->sdev = sdev;
	ldev->sound = sound;
	ldev->layout = layout;
	ldev->gpio.node = sound->parent;
	switch (layout->layout_id) {
	case 0:  /* anything with device_id, not layout_id */
	case 41: /* that unknown machine no one seems to have */
	case 51: /* PowerBook5,4 */
	case 58: /* Mac Mini */
		ldev->gpio.methods = ftr_gpio_methods;
		printk(KERN_DEBUG
		       "snd-aoa-fabric-layout: Using direct GPIOs\n");
		break;
	default:
		ldev->gpio.methods = pmf_gpio_methods;
		printk(KERN_DEBUG
		       "snd-aoa-fabric-layout: Using PMF GPIOs\n");
	}
	ldev->selfptr_headphone.ptr = ldev;
	ldev->selfptr_lineout.ptr = ldev;
	dev_set_drvdata(&sdev->ofdev.dev, ldev);
	list_add(&ldev->list, &layouts_list);
	layouts_list_items++;

	/* assign these before registering ourselves, so
	 * callbacks that are done during registration
	 * already have the values */
	sdev->pcmid = ldev->layout->pcmid;
	if (ldev->layout->busname) {
		sdev->pcmname = ldev->layout->busname;
	} else {
		sdev->pcmname = "Master";
	}

	ldev->gpio.methods->init(&ldev->gpio);

	err = aoa_fabric_register(&layout_fabric, &sdev->ofdev.dev);
	if (err && err != -EALREADY) {
		printk(KERN_INFO "snd-aoa-fabric-layout: can't use,"
				 " another fabric is active!\n");
		goto outlistdel;
	}

	use_layout(layout);
	ldev->switch_on_headphone = 1;
	ldev->switch_on_lineout = 1;
	return 0;
 outlistdel:
	/* we won't be using these then... */
	ldev->gpio.methods->exit(&ldev->gpio);
	/* reset if we didn't use it */
	sdev->pcmname = NULL;
	sdev->pcmid = -1;
	list_del(&ldev->list);
	layouts_list_items--;
 outnodev:
 	of_node_put(sound);
 	layout_device = NULL;
 	kfree(ldev);
	return -ENODEV;
}

static int aoa_fabric_layout_remove(struct soundbus_dev *sdev)
{
	struct layout_dev *ldev = dev_get_drvdata(&sdev->ofdev.dev);
	int i;

	for (i=0; i<MAX_CODECS_PER_BUS; i++) {
		if (ldev->codecs[i]) {
			aoa_fabric_unlink_codec(ldev->codecs[i]);
		}
		ldev->codecs[i] = NULL;
	}
	list_del(&ldev->list);
	layouts_list_items--;
	of_node_put(ldev->sound);

	ldev->gpio.methods->set_notify(&ldev->gpio,
				       AOA_NOTIFY_HEADPHONE,
				       NULL,
				       NULL);
	ldev->gpio.methods->set_notify(&ldev->gpio,
				       AOA_NOTIFY_LINE_OUT,
				       NULL,
				       NULL);

	ldev->gpio.methods->exit(&ldev->gpio);
	layout_device = NULL;
	kfree(ldev);
	sdev->pcmid = -1;
	sdev->pcmname = NULL;
	return 0;
}

#ifdef CONFIG_PM
static int aoa_fabric_layout_suspend(struct soundbus_dev *sdev, pm_message_t state)
{
	struct layout_dev *ldev = dev_get_drvdata(&sdev->ofdev.dev);

	if (ldev->gpio.methods && ldev->gpio.methods->all_amps_off)
		ldev->gpio.methods->all_amps_off(&ldev->gpio);

	return 0;
}

static int aoa_fabric_layout_resume(struct soundbus_dev *sdev)
{
	struct layout_dev *ldev = dev_get_drvdata(&sdev->ofdev.dev);

	if (ldev->gpio.methods && ldev->gpio.methods->all_amps_off)
		ldev->gpio.methods->all_amps_restore(&ldev->gpio);

	return 0;
}
#endif

static struct soundbus_driver aoa_soundbus_driver = {
	.name = "snd_aoa_soundbus_drv",
	.owner = THIS_MODULE,
	.probe = aoa_fabric_layout_probe,
	.remove = aoa_fabric_layout_remove,
#ifdef CONFIG_PM
	.suspend = aoa_fabric_layout_suspend,
	.resume = aoa_fabric_layout_resume,
#endif
	.driver = {
		.owner = THIS_MODULE,
	}
};

static int __init aoa_fabric_layout_init(void)
{
	int err;

	err = soundbus_register_driver(&aoa_soundbus_driver);
	if (err)
		return err;
	return 0;
}

static void __exit aoa_fabric_layout_exit(void)
{
	soundbus_unregister_driver(&aoa_soundbus_driver);
	aoa_fabric_unregister(&layout_fabric);
}

module_init(aoa_fabric_layout_init);
module_exit(aoa_fabric_layout_exit);
