/*-
 * Copyright (c) 2012 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Paul Fleischer <paul@xpg.dk>
 *
 * 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.
 */

#include <sys/cdefs.h>
#include <sys/param.h>
#include <sys/device.h>
#include <sys/malloc.h>
#include <sys/fcntl.h>
#include <sys/audioio.h>

#include <sys/bus.h>

#include <dev/audio/audio_if.h>


#include <dev/ic/uda1341var.h>

#include <arch/arm/s3c2xx0/s3c2440reg.h>
#include <arch/arm/s3c2xx0/s3c2440var.h>

#include <arch/arm/s3c2xx0/s3c2440_dma.h>
#include <arch/arm/s3c2xx0/s3c2440_i2s.h>

/*#define AUDIO_MINI2440_DEBUG*/

#ifdef AUDIO_MINI2440_DEBUG
#define DPRINTF(x) do {printf x; } while (/*CONSTCOND*/0)
#else
#define DPRINTF(s) do {} while (/*CONSTCOND*/0)
#endif

struct uda_softc {
	device_t		sc_dev;
	kmutex_t		sc_lock;
	kmutex_t		sc_intr_lock;

	struct uda1341_softc	sc_uda1341;

	s3c2440_i2s_buf_t	sc_play_buf;
	s3c2440_i2s_buf_t	sc_rec_buf;

	void			*sc_i2s_handle;
};

int	uda_ssio_open(void *, int);
void	uda_ssio_close(void *);
int	uda_ssio_query_format(void *, audio_format_query_t *);
int	uda_ssio_set_format(void *, int,
		       const audio_params_t *, const audio_params_t *,
		       audio_filter_reg_t *, audio_filter_reg_t *);
int	uda_ssio_start_output(void *, void *, int, void (*)(void *),
			      void *);
int	uda_ssio_start_input(void *, void *, int, void (*)(void *),
			      void *);
int	uda_ssio_halt_output(void *);
int	uda_ssio_halt_input(void *);
int	uda_ssio_getdev(void *, struct audio_device *ret);
void*	uda_ssio_allocm(void *, int, size_t);
void	uda_ssio_freem(void *, void *, size_t);
size_t	uda_ssio_round_buffersize(void *, int, size_t);
int	uda_ssio_get_props(void *);
void	uda_ssio_get_locks(void *, kmutex_t**, kmutex_t**);

struct audio_hw_if uda1341_hw_if = {
	.open			= uda_ssio_open,
	.close			= uda_ssio_close,
	.query_format		= uda_ssio_query_format,
	.set_format		= uda_ssio_set_format,
	.start_output		= uda_ssio_start_output,
	.start_input		= uda_ssio_start_input,
	.halt_output		= uda_ssio_halt_output,
	.halt_input		= uda_ssio_halt_input,
	.getdev			= uda_ssio_getdev,
	.set_port		= uda1341_set_port,
	.get_port		= uda1341_get_port,
	.query_devinfo		= uda1341_query_devinfo,
	.allocm			= uda_ssio_allocm,
	.freem			= uda_ssio_freem,
	.round_buffersize	= uda_ssio_round_buffersize,
	.get_props		= uda_ssio_get_props,
	.get_locks		= uda_ssio_get_locks
};

static struct audio_device uda1341_device = {
	"MINI2240-UDA1341",
	"0.1",
	"uda_ssio"
};

static const struct audio_format uda_ssio_formats[] =
{
	{
		.mode		= AUMODE_PLAY | AUMODE_RECORD,
		.encoding	= AUDIO_ENCODING_SLINEAR_LE,
		.validbits	= 16,
		.precision	= 16,
		.channels	= 2,
		.channel_mask	= AUFMT_STEREO,
		.frequency_type	= 6,
		.frequency	= { 8000, 11025, 22050, 32000, 44100, 48000 },
	}
};
#define UDA_SSIO_NFORMATS __arraycount(uda_ssio_formats)

void uda_ssio_l3_write(void *,int mode, int value);

int uda_ssio_match(device_t, cfdata_t, void*);
void uda_ssio_attach(device_t, device_t, void*);

CFATTACH_DECL_NEW(udassio, sizeof(struct uda_softc),
	      uda_ssio_match, uda_ssio_attach, NULL, NULL);

int
uda_ssio_match(device_t parent, cfdata_t match, void *aux)
{
	DPRINTF(("%s\n", __func__));
	/* Not quite sure how we can detect the UDA1341 chip */
	return 1;
}

void
uda_ssio_attach(device_t parent, device_t self, void *aux)
{
	/*	struct s3c2xx0_attach_args *sa = aux;*/
	struct uda_softc *sc = device_private(self);
	struct s3c2xx0_softc *s3sc = s3c2xx0_softc; /* Shortcut */
	struct s3c2440_i2s_attach_args *aa = aux;
	uint32_t reg;

	sc->sc_dev = self;

	sc->sc_play_buf = NULL;
	sc->sc_i2s_handle = aa->i2sa_handle;

	mutex_init(&sc->sc_lock, MUTEX_DEFAULT, IPL_NONE);
	mutex_init(&sc->sc_intr_lock, MUTEX_DEFAULT, IPL_SCHED);

	s3c2440_i2s_set_intr_lock(aa->i2sa_handle, &sc->sc_intr_lock);

	/* arch/arm/s3c2xx0/s3c2440.c initializes the I2S subsystem for us */

	/* Setup GPIO pins to output for L3 communication.
	   GPB3 (L3DATA) will have to be switched to input when reading
	   from the L3 bus.

	   GPB2 - L3MODE
	   GPB3 - L3DATA
	   GPB4 - L3CLOCK
	   TODO: Make this configurable
	*/
	reg = bus_space_read_4(s3sc->sc_iot, s3sc->sc_gpio_ioh, GPIO_PBCON);
	reg = GPIO_SET_FUNC(reg, 2, 1);
	reg = GPIO_SET_FUNC(reg, 3, 1);
	reg = GPIO_SET_FUNC(reg, 4, 1);
	bus_space_write_4(s3sc->sc_iot, s3sc->sc_gpio_ioh, GPIO_PBCON, reg);

	reg = bus_space_read_4(s3sc->sc_iot, s3sc->sc_gpio_ioh, GPIO_PBDAT);
	reg = GPIO_SET_DATA(reg, 4, 1);
	reg = GPIO_SET_DATA(reg, 3, 0);
	reg = GPIO_SET_DATA(reg, 2, 1);
	bus_space_write_4(s3sc->sc_iot, s3sc->sc_gpio_ioh, GPIO_PBDAT, reg);

	printf("\n");

	/* uda1341_attach resets the uda1341 sc, so it has to be called before
	   attributes are set on the sc.*/
	uda1341_attach(&sc->sc_uda1341);

	/* Configure the UDA1341 Codec */
	sc->sc_uda1341.parent = sc;
	sc->sc_uda1341.sc_l3_write = uda_ssio_l3_write;
	sc->sc_uda1341.sc_bus_format = UDA1341_BUS_MSB;

	/* Configure I2S controller */
	s3c2440_i2s_set_bus_format(sc->sc_i2s_handle, S3C2440_I2S_BUS_MSB);
	// Attach
	audio_attach_mi(&uda1341_hw_if, &sc->sc_uda1341, self);
}

int
uda_ssio_open(void *handle, int flags)
{
	int retval;

	DPRINTF(("%s\n", __func__));

	/* We only support write operations */
	if (!(flags & FREAD) && !(flags & FWRITE))
		return EINVAL;

	/* We can't do much more at this point than to
	   ask the UDA1341 codec to initialize itself
	   (for an unknown system clock)
	*/
	retval = uda1341_open(handle, flags);
	if (retval != 0) {
		return retval;
	}

	return 0; /* SUCCESS */
}

void
uda_ssio_close(void *handle)
{

	DPRINTF(("%s\n", __func__));

	uda1341_close(handle);
}

int
uda_ssio_query_format(void *handle, audio_format_query_t *afp)
{

	return audio_query_format(uda_ssio_formats, UDA_SSIO_NFORMATS, afp);
}

int
uda_ssio_set_format(void *handle, int setmode,
		    const audio_params_t *play, const audio_params_t *rec,
		    audio_filter_reg_t *pfil, audio_filter_reg_t *rfil)
{
	struct uda1341_softc *uc = handle;
	struct uda_softc *sc = uc->parent;
	int retval;

	DPRINTF(("%s: setmode: %d\n", __func__, setmode));

	/* *play and *rec are the identical because !AUDIO_PROP_INDEPENDENT. */

	DPRINTF(("%s: %dHz, encoding: %d, precision: %d, channels: %d\n",
		 __func__, play->sample_rate, play->encoding, play->precision,
		 play->channels));

	if (setmode == AUMODE_PLAY) {
		s3c2440_i2s_set_direction(sc->sc_i2s_handle,
					  S3C2440_I2S_TRANSMIT);
	} else {
		s3c2440_i2s_set_direction(sc->sc_i2s_handle,
					  S3C2440_I2S_RECEIVE);
	}

	s3c2440_i2s_set_sample_rate(sc->sc_i2s_handle, play->sample_rate);
	s3c2440_i2s_set_sample_width(sc->sc_i2s_handle, 16);

	/* It is vital that sc_system_clock is set PRIOR to calling
	   uda1341_set_format. */
	switch (s3c2440_i2s_get_master_clock(sc->sc_i2s_handle)) {
	case 384:
		uc->sc_system_clock = UDA1341_CLOCK_384;
		break;
	case 256:
		uc->sc_system_clock = UDA1341_CLOCK_256;
		break;
	default:
		return EINVAL;
	}

	retval = uda1341_set_format(handle, setmode, play, rec, pfil, rfil);
	if (retval != 0) {
		return retval;
	}

	/* Setup and enable I2S controller */
	retval = s3c2440_i2s_commit(sc->sc_i2s_handle);
	if (retval != 0) {
		printf("Failed to setup I2S controller\n");
		return retval;
	}

	return 0;
}

int
uda_ssio_start_output(void *handle, void *block, int bsize,
		      void (*intr)(void *), void *intrarg)
{
	struct uda1341_softc *uc = handle;
	struct uda_softc *sc = uc->parent;

	return s3c2440_i2s_output(sc->sc_play_buf, block, bsize, intr, intrarg);
}

int
uda_ssio_start_input(void *handle, void *block, int bsize,
		     void (*intr)(void *), void *intrarg)
{
	struct uda1341_softc *uc = handle;
	struct uda_softc *sc = uc->parent;

	return s3c2440_i2s_input(sc->sc_rec_buf, block, bsize, intr, intrarg);
}

int
uda_ssio_halt_output(void *handle)
{
	struct uda1341_softc *uc = handle;
	struct uda_softc *sc = uc->parent;

	return s3c2440_i2s_halt_output(sc->sc_play_buf);
}

int
uda_ssio_halt_input(void *handle)
{
	DPRINTF(("%s\n", __func__));
	return 0;
}

int
uda_ssio_getdev(void *handle, struct audio_device *ret)
{
	*ret = uda1341_device;
	return 0;
}

void *
uda_ssio_allocm(void *handle, int direction, size_t size)
{
	struct uda1341_softc *uc = handle;
	struct uda_softc *sc = uc->parent;
	void *retval = NULL;

	DPRINTF(("%s\n", __func__));

	if (direction == AUMODE_PLAY ) {
		if (sc->sc_play_buf != NULL)
			return NULL;

		s3c2440_i2s_alloc(sc->sc_i2s_handle, direction, size, 0x00, &sc->sc_play_buf);
		DPRINTF(("%s: addr of ring buffer: %p\n", __func__, sc->sc_play_buf->i2b_addr));
		retval = sc->sc_play_buf->i2b_addr;
	} else if (direction == AUMODE_RECORD) {
		if (sc->sc_rec_buf != NULL)
			return NULL;

		s3c2440_i2s_alloc(sc->sc_i2s_handle, direction, size, 0x00, &sc->sc_rec_buf);
		DPRINTF(("%s: addr of ring buffer: %p\n", __func__, sc->sc_rec_buf->i2b_addr));
		retval = sc->sc_rec_buf->i2b_addr;
	}

	DPRINTF(("buffer: %p", retval));

	return retval;
}

void
uda_ssio_freem(void *handle, void *ptr, size_t size)
{
	struct uda1341_softc *uc = handle;
	struct uda_softc *sc = uc->parent;
	DPRINTF(("%s\n", __func__));

	if (ptr == sc->sc_play_buf->i2b_addr)
		s3c2440_i2s_free(sc->sc_play_buf);
	else if (ptr == sc->sc_rec_buf->i2b_addr)
		s3c2440_i2s_free(sc->sc_rec_buf);
}

size_t
uda_ssio_round_buffersize(void *handle, int direction, size_t bufsize)
{
	DPRINTF(("%s: %d\n", __func__, (int)bufsize));
	return bufsize;
}

int
uda_ssio_get_props(void *handle)
{
	return AUDIO_PROP_PLAYBACK | AUDIO_PROP_CAPTURE;
}

void
uda_ssio_get_locks(void *handle, kmutex_t **intr, kmutex_t **thread)
{
	struct uda1341_softc *uc = handle;
	struct uda_softc *sc = uc->parent;
	//struct uda_softc *sc = handle;

	*intr = &sc->sc_intr_lock;
	*thread = &sc->sc_lock;
}

void
uda_ssio_l3_write(void *cookie, int mode, int value)
{
	struct s3c2xx0_softc *s3sc = s3c2xx0_softc; /* Shortcut */
	uint32_t reg;

	/* GPB2: L3MODE
	   GPB3: L2DATA
	   GPB4: L3CLOCK */
#define L3MODE	2
#define L3DATA	3
#define L3CLOCK 4
#define READ_GPIO() bus_space_read_4(s3sc->sc_iot, s3sc->sc_gpio_ioh, GPIO_PBDAT)
#define WRITE_GPIO(val) bus_space_write_4(s3sc->sc_iot, s3sc->sc_gpio_ioh, GPIO_PBDAT, val)

#define DELAY_TIME 1

	reg = READ_GPIO();
	reg = GPIO_SET_DATA(reg, L3CLOCK, 1);
	reg = GPIO_SET_DATA(reg, L3MODE, mode);
	reg = GPIO_SET_DATA(reg, L3DATA, 0);
	WRITE_GPIO(reg);

	if (mode == 1 ) {
		reg = READ_GPIO();
		reg = GPIO_SET_DATA(reg, L3MODE, 1);
		WRITE_GPIO(reg);
	}

	DELAY(1); /* L3MODE setup time: min 190ns */

	for(int i = 0; i<8; i++) {
		char bval = (value >> i) & 0x1;

		reg = READ_GPIO();
		reg = GPIO_SET_DATA(reg, L3CLOCK, 0);
		reg = GPIO_SET_DATA(reg, L3DATA, bval);
		WRITE_GPIO(reg);

		DELAY(DELAY_TIME);

		reg = READ_GPIO();
		reg = GPIO_SET_DATA(reg, L3CLOCK, 1);
		reg = GPIO_SET_DATA(reg, L3DATA, bval);
		WRITE_GPIO(reg);

		DELAY(DELAY_TIME);
	}

	reg = READ_GPIO();
	reg = GPIO_SET_DATA(reg, L3MODE, 1);
	reg = GPIO_SET_DATA(reg, L3CLOCK, 1);
	reg = GPIO_SET_DATA(reg, L3DATA, 0);
	WRITE_GPIO(reg);

#undef L3MODE
#undef L3DATA
#undef L3CLOCK
#undef DELAY_TIME
#undef READ_GPIO
#undef WRITE_GPIO
}