/*-
* Copyright (c) 2012 The NetBSD Foundation, Inc.
* All rights reserved.
*
* This code is derived from software contributed to The NetBSD Foundation
* by Paul Fleischer <[email protected]>
*
* 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/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
}