/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2006 Thom Johansen
 * Copyright (C) 2012 Michael Sevakis
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
#include "rbcodecconfig.h"
#include "sound.h"
#include "fixedpoint.h"
#include "fracmul.h"
#include "dsp_proc_entry.h"
#include "channel_mode.h"
#include <string.h>

#if 0
/* SOUND_CHAN_STEREO mode is a noop so has no function - just outline one for
 * completeness. */
void channel_mode_proc_stereo(struct dsp_proc_entry *this,
                              struct dsp_buffer **buf_p);
#endif
void channel_mode_proc_mono(struct dsp_proc_entry *this,
                            struct dsp_buffer **buf_p);
void channel_mode_proc_mono_left(struct dsp_proc_entry *this,
                                 struct dsp_buffer **buf_p);
void channel_mode_proc_mono_right(struct dsp_proc_entry *this,
                                  struct dsp_buffer **buf_p);
void channel_mode_proc_custom(struct dsp_proc_entry *this,
                              struct dsp_buffer **buf_p);
void channel_mode_proc_karaoke(struct dsp_proc_entry *this,
                               struct dsp_buffer **buf_p);
void channel_mode_proc_swap(struct dsp_proc_entry *this,
                            struct dsp_buffer **buf_p);

static struct channel_mode_data
{
    long  sw_gain;   /* 00h: for mode: custom */
    long  sw_cross;  /* 04h: for mode: custom */
    int   mode;
} channel_mode_data =
{
    .sw_gain = 0,
    .sw_cross = 0,
    .mode = SOUND_CHAN_STEREO
};

#if 0
/* SOUND_CHAN_STEREO mode is a noop so has no function - just outline one for
 * completeness. */
void channel_mode_proc_stereo(struct dsp_proc_entry *this,
                              struct dsp_buffer **buf_p)
{
    /* The channels are each just themselves */
    (void)this; (void)buf_p;
}
#endif

#if !defined(CPU_COLDFIRE) && !defined(CPU_ARM)
/* Unoptimized routines */
void channel_mode_proc_mono(struct dsp_proc_entry *this,
                            struct dsp_buffer **buf_p)
{
    struct dsp_buffer *buf = *buf_p;
    int32_t *sl = buf->p32[0];
    int32_t *sr = buf->p32[1];
    int count = buf->remcount;

    do
    {
        int32_t lr = *sl / 2 + *sr / 2;
        *sl++ = lr;
        *sr++ = lr;
    }
    while (--count > 0);

    (void)this;
}

void channel_mode_proc_custom(struct dsp_proc_entry *this,
                              struct dsp_buffer **buf_p)
{
    struct channel_mode_data *data = (void *)this->data;
    struct dsp_buffer *buf = *buf_p;

    int32_t *sl = buf->p32[0];
    int32_t *sr = buf->p32[1];
    int count = buf->remcount;

    const int32_t gain  = data->sw_gain;
    const int32_t cross = data->sw_cross;

    do
    {
        int32_t l = *sl;
        int32_t r = *sr;
        *sl++ = FRACMUL(l, gain) + FRACMUL(r, cross);
        *sr++ = FRACMUL(r, gain) + FRACMUL(l, cross);
    }
    while (--count > 0);
}

void channel_mode_proc_karaoke(struct dsp_proc_entry *this,
                               struct dsp_buffer **buf_p)
{
    struct dsp_buffer *buf = *buf_p;
    int32_t *sl = buf->p32[0];
    int32_t *sr = buf->p32[1];
    int count = buf->remcount;

    do
    {
        int32_t ch = *sl / 2 - *sr / 2;
        *sl++ = ch;
        *sr++ = -ch;
    }
    while (--count > 0);

    (void)this;
}
#endif /* CPU */

void channel_mode_proc_swap(struct dsp_proc_entry *this,
                            struct dsp_buffer **buf_p)
{
    struct dsp_buffer *buf = *buf_p;
    int32_t *sl = buf->p32[0];
    int32_t *sr = buf->p32[1];
    int count = buf->remcount;

    do
    {
        int32_t l = *sl;
        *sl++ = *sr;
        *sr++ = l;
    }
    while (--count > 0);

    (void)this;
}

void channel_mode_proc_mono_left(struct dsp_proc_entry *this,
                                 struct dsp_buffer **buf_p)
{
    /* Just copy over the other channel */
    struct dsp_buffer *buf = *buf_p;
    memcpy(buf->p32[1], buf->p32[0], buf->remcount * sizeof (int32_t));
    (void)this;
}

void channel_mode_proc_mono_right(struct dsp_proc_entry *this,
                                  struct dsp_buffer **buf_p)
{
    /* Just copy over the other channel */
    struct dsp_buffer *buf = *buf_p;
    memcpy(buf->p32[0], buf->p32[1], buf->remcount * sizeof (int32_t));
    (void)this;
}

void channel_mode_set_config(int value)
{
    if (value < 0 || value >= SOUND_CHAN_NUM_MODES)
        value = SOUND_CHAN_STEREO; /* Out of range */

    if (value == channel_mode_data.mode)
        return;

    channel_mode_data.mode = value;
    dsp_proc_enable(dsp_get_config(CODEC_IDX_AUDIO), DSP_PROC_CHANNEL_MODE,
                    value != SOUND_CHAN_STEREO);
}

void channel_mode_custom_set_width(int value)
{
    long width, straight, cross;

    width = value * 0x7fffff / 100;

    if (value <= 100)
    {
        straight = (0x7fffff + width) / 2;
        cross = straight - width;
    }
    else
    {
        /* straight = (1 + width) / (2 * width) */
        straight = fp_div(0x7fffff + width, width, 22);
        cross = straight - 0x7fffff;
    }

    channel_mode_data.sw_gain  = straight << 8;
    channel_mode_data.sw_cross = cross << 8;
}

static void update_process_fn(struct dsp_proc_entry *this)
{
    static const dsp_proc_fn_type fns[SOUND_CHAN_NUM_MODES] =
    {
        [SOUND_CHAN_STEREO]     = NULL,
        [SOUND_CHAN_MONO]       = channel_mode_proc_mono,
        [SOUND_CHAN_CUSTOM]     = channel_mode_proc_custom,
        [SOUND_CHAN_MONO_LEFT]  = channel_mode_proc_mono_left,
        [SOUND_CHAN_MONO_RIGHT] = channel_mode_proc_mono_right,
        [SOUND_CHAN_KARAOKE]    = channel_mode_proc_karaoke,
        [SOUND_CHAN_SWAP]       = channel_mode_proc_swap,
    };

    this->process = fns[((struct channel_mode_data *)this->data)->mode];
}

/* Handle format changes and verify the format compatibility */
static intptr_t channel_mode_new_format(struct dsp_proc_entry *this,
                                        struct dsp_config *dsp,
                                        struct sample_format *format)
{
    DSP_PRINT_FORMAT(DSP_PROC_CHANNEL_MODE, *format);

    bool active = format->num_channels >= 2;
    dsp_proc_activate(dsp, DSP_PROC_CHANNEL_MODE, active);

    if (active)
        return PROC_NEW_FORMAT_OK;

    /* Can't do this. Sleep until next change. */
    DEBUGF("  DSP_PROC_CHANNEL_MODE- deactivated\n");
    return PROC_NEW_FORMAT_DEACTIVATED;

    (void)this;
}

/* DSP message hook */
static intptr_t channel_mode_configure(struct dsp_proc_entry *this,
                                       struct dsp_config *dsp,
                                       unsigned int setting,
                                       intptr_t value)
{
    intptr_t retval = 0;

    switch (setting)
    {
    case DSP_PROC_INIT:
        if (value == 0)
            this->data = (intptr_t)&channel_mode_data;

        update_process_fn(this);
        break;

    case DSP_PROC_NEW_FORMAT:
        retval = channel_mode_new_format(this, dsp,
                                         (struct sample_format *)value);
        break;
    }

    return retval;
}

/* Database entry */
DSP_PROC_DB_ENTRY(
    CHANNEL_MODE,
    channel_mode_configure);
