/*
 * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Apache-2.0
 */
#include <string.h>
#include "esp_log.h"
#include "es8388_reg.h"
#include "es8388_codec.h"
#include "es_common.h"
#include "esp_codec_dev_vol.h"

#define TAG "ES8388"

typedef struct {
    audio_codec_if_t             base;
    const audio_codec_ctrl_if_t *ctrl_if;
    const audio_codec_gpio_if_t *gpio_if;
    bool                         is_open;
    bool                         enabled;
    esp_codec_dec_work_mode_t    codec_mode;
    int16_t                      pa_pin;
    bool                         pa_reverted;
    float                        hw_gain;
} audio_codec_es8388_t;

static const esp_codec_dev_vol_range_t vol_range = {
    .min_vol =
    {
        .vol = 0xC0,
        .db_value = -96,
    },
    .max_vol =
    {
        .vol = 0,
        .db_value = 0.0,
    },
};

static int es8388_write_reg(audio_codec_es8388_t *codec, int reg, int value)
{
    return codec->ctrl_if->write_reg(codec->ctrl_if, reg, 1, &value, 1);
}

static int es8388_read_reg(audio_codec_es8388_t *codec, int reg, int *value)
{
    *value = 0;
    return codec->ctrl_if->read_reg(codec->ctrl_if, reg, 1, value, 1);
}

static int es8388_set_adc_dac_volume(audio_codec_es8388_t *codec, esp_codec_dec_work_mode_t mode, int volume, int dot)
{
    int res = 0;
    if (volume < -96 || volume > 0) {
        if (volume < -96)
            volume = -96;
        else
            volume = 0;
    }
    dot = (dot >= 5 ? 1 : 0);
    volume = (-volume << 1) + dot;
    if (mode & ESP_CODEC_DEV_WORK_MODE_ADC) {
        res |= es8388_write_reg(codec, ES8388_ADCCONTROL8, volume);
        res |= es8388_write_reg(codec, ES8388_ADCCONTROL9, volume); // ADC Right Volume=0db
    }
    if (mode & ESP_CODEC_DEV_WORK_MODE_DAC) {
        res |= es8388_write_reg(codec, ES8388_DACCONTROL5, volume);
        res |= es8388_write_reg(codec, ES8388_DACCONTROL4, volume);
    }
    return res;
}

static int es8388_set_voice_mute(audio_codec_es8388_t *codec, bool enable)
{
    int res = 0;
    int reg = 0;
    res = es8388_read_reg(codec, ES8388_DACCONTROL3, &reg);
    reg = reg & 0xFB;
    res |= es8388_write_reg(codec, ES8388_DACCONTROL3, reg | (((int) enable) << 2));
    return res;
}

static int es8388_start(audio_codec_es8388_t *codec, esp_codec_dec_work_mode_t mode)
{
    int res = 0;
    int prev_data = 0, data = 0;
    es8388_read_reg(codec, ES8388_DACCONTROL21, &prev_data);
    if (mode == ESP_CODEC_DEV_WORK_MODE_LINE) {
        res |= es8388_write_reg(codec, ES8388_DACCONTROL16,
                                0x09); // 0x00 audio on LIN1&RIN1,  0x09 LIN2&RIN2 by pass enable
        res |= es8388_write_reg(
            codec, ES8388_DACCONTROL17,
            0x50); // left DAC to left mixer enable  and  LIN signal to left mixer enable 0db  : bupass enable
        res |= es8388_write_reg(
            codec, ES8388_DACCONTROL20,
            0x50); // right DAC to right mixer enable  and  LIN signal to right mixer enable 0db : bupass enable
        res |= es8388_write_reg(codec, ES8388_DACCONTROL21, 0xC0); // enable adc
    } else {
        res |= es8388_write_reg(codec, ES8388_DACCONTROL21, 0x80); // enable dac
    }
    es8388_read_reg(codec, ES8388_DACCONTROL21, &data);
    if (prev_data != data) {
        res |= es8388_write_reg(codec, ES8388_CHIPPOWER, 0xF0); // start state machine
        res |= es8388_write_reg(codec, ES8388_CHIPPOWER, 0x00); // start state machine
    }
    if ((mode & ESP_CODEC_DEV_WORK_MODE_ADC) || mode == ESP_CODEC_DEV_WORK_MODE_LINE) {
        res |= es8388_write_reg(codec, ES8388_ADCPOWER, 0x00); // power up adc and line in
    }
    if ((mode & ESP_CODEC_DEV_WORK_MODE_DAC) || mode == ESP_CODEC_DEV_WORK_MODE_LINE) {
        res |= es8388_write_reg(codec, ES8388_DACPOWER, 0x3c); // power up dac and line out
        res |= es8388_set_voice_mute(codec, false);
        ESP_LOGI(TAG, "Start on mode:%d", mode);
    }
    return res;
}

static int es8388_stop(audio_codec_es8388_t *codec, esp_codec_dec_work_mode_t mode)
{
    int res = 0;
    if (mode == ESP_CODEC_DEV_WORK_MODE_LINE) {
        res |= es8388_write_reg(codec, ES8388_DACCONTROL21, 0x80); // enable dac
        res |= es8388_write_reg(codec, ES8388_DACCONTROL16, 0x00); // 0x00 audio on LIN1&RIN1,  0x09 LIN2&RIN2
        res |= es8388_write_reg(codec, ES8388_DACCONTROL17, 0x90); // only left DAC to left mixer enable 0db
        res |= es8388_write_reg(codec, ES8388_DACCONTROL20, 0x90); // only right DAC to right mixer enable 0db
        return res;
    }
    if (mode & ESP_CODEC_DEV_WORK_MODE_DAC) {
        res |= es8388_write_reg(codec, ES8388_DACPOWER, 0x00);
        res |= es8388_set_voice_mute(codec, true);
    }
    if (mode & ESP_CODEC_DEV_WORK_MODE_ADC) {
        res |= es8388_write_reg(codec, ES8388_ADCPOWER, 0xFF); // power down adc and line in
    }
    if (mode == ESP_CODEC_DEV_WORK_MODE_BOTH) {
        res |= es8388_write_reg(codec, ES8388_DACCONTROL21, 0x9C); // disable mclk
    }
    return res;
}

static int es8388_config_fmt(audio_codec_es8388_t *codec, esp_codec_dec_work_mode_t mode, es_i2s_fmt_t fmt)
{
    int res = 0;
    int reg = 0;
    if (mode & ESP_CODEC_DEV_WORK_MODE_ADC) {
        res = es8388_read_reg(codec, ES8388_ADCCONTROL4, &reg);
        reg = reg & 0xfc;
        res |= es8388_write_reg(codec, ES8388_ADCCONTROL4, reg | fmt);
    }
    if (mode & ESP_CODEC_DEV_WORK_MODE_DAC) {
        res = es8388_read_reg(codec, ES8388_DACCONTROL1, &reg);
        reg = reg & 0xf9;
        res |= es8388_write_reg(codec, ES8388_DACCONTROL1, reg | (fmt << 1));
    }
    return res;
}

static int es8388_set_mic_gain(audio_codec_es8388_t *codec, float db)
{
    int gain = db > 0 ? (int) (db / 3) : 0;
    gain = (gain << 4) + gain;
    return es8388_write_reg(codec, ES8388_ADCCONTROL1, gain); // MIC PGA
}

static es_bits_length_t get_bits_enum(uint8_t bits)
{
    switch (bits) {
        case 16:
        default:
            return BIT_LENGTH_16BITS;
        case 18:
            return BIT_LENGTH_18BITS;
        case 20:
            return BIT_LENGTH_20BITS;
        case 24:
            return BIT_LENGTH_24BITS;
        case 32:
            return BIT_LENGTH_32BITS;
    }
}

static int es8388_set_bits_per_sample(audio_codec_es8388_t *codec, esp_codec_dec_work_mode_t mode, uint8_t bits_length)
{
    int res = 0;
    int reg = 0;
    int bits = (int) get_bits_enum(bits_length);

    if (mode & ESP_CODEC_DEV_WORK_MODE_ADC) {
        res = es8388_read_reg(codec, ES8388_ADCCONTROL4, &reg);
        reg = reg & 0xe3;
        res |= es8388_write_reg(codec, ES8388_ADCCONTROL4, reg | (bits << 2));
    }
    if (mode & ESP_CODEC_DEV_WORK_MODE_DAC) {
        res = es8388_read_reg(codec, ES8388_DACCONTROL1, &reg);
        reg = reg & 0xc7;
        res |= es8388_write_reg(codec, ES8388_DACCONTROL1, reg | (bits << 3));
    }
    return res;
}

static void es8388_pa_power(audio_codec_es8388_t *codec, bool enable)
{
    int16_t pa_pin = codec->pa_pin;
    if (pa_pin == -1 || codec->gpio_if == NULL) {
        return;
    }
    codec->gpio_if->setup(pa_pin, AUDIO_GPIO_DIR_OUT, AUDIO_GPIO_MODE_FLOAT);
    codec->gpio_if->set(pa_pin, codec->pa_reverted ? !enable : enable);
}

static int es8388_open(const audio_codec_if_t *h, void *cfg, int cfg_size)
{
    audio_codec_es8388_t *codec = (audio_codec_es8388_t *) h;
    es8388_codec_cfg_t *codec_cfg = (es8388_codec_cfg_t *) cfg;
    if (codec == NULL || codec_cfg->ctrl_if == NULL || cfg_size != sizeof(es8388_codec_cfg_t)) {
        return ESP_CODEC_DEV_INVALID_ARG;
    }
    int res = ESP_CODEC_DEV_OK;
    codec->ctrl_if = codec_cfg->ctrl_if;
    codec->gpio_if = codec_cfg->gpio_if;
    codec->pa_pin = codec_cfg->pa_pin;
    codec->pa_reverted = codec_cfg->pa_reverted;
    codec->codec_mode = codec_cfg->codec_mode;

    // 0x04 mute/0x00 unmute&ramp;
    res |= es8388_write_reg(codec, ES8388_DACCONTROL3, 0x04);
    /* Chip Control and Power Management */
    res |= es8388_write_reg(codec, ES8388_CONTROL2, 0x50);
    res |= es8388_write_reg(codec, ES8388_CHIPPOWER, 0x00); // normal all and power up all

    // Disable the internal DLL to improve 8K sample rate
    res |= es8388_write_reg(codec, 0x35, 0xA0);
    res |= es8388_write_reg(codec, 0x37, 0xD0);
    res |= es8388_write_reg(codec, 0x39, 0xD0);

    res |= es8388_write_reg(codec, ES8388_MASTERMODE, codec_cfg->master_mode); // CODEC IN I2S SLAVE MODE

    /* dac */
    res |= es8388_write_reg(codec, ES8388_DACPOWER, 0xC0); // disable DAC and disable Lout/Rout/1/2
    res |= es8388_write_reg(codec, ES8388_CONTROL1, 0x12); // Enfr=0,Play&Record Mode,(0x17-both of mic&paly)
    //    res |= es8388_write_reg(codec, ES8388_CONTROL2, 0);  //LPVrefBuf=0,Pdn_ana=0
    res |= es8388_write_reg(codec, ES8388_DACCONTROL1, 0x18);  // 1a 0x18:16bit iis , 0x00:24
    res |= es8388_write_reg(codec, ES8388_DACCONTROL2, 0x02);  // DACFsMode,SINGLE SPEED; DACFsRatio,256
    res |= es8388_write_reg(codec, ES8388_DACCONTROL16, 0x00); // 0x00 audio on LIN1&RIN1,  0x09 LIN2&RIN2
    res |= es8388_write_reg(codec, ES8388_DACCONTROL17, 0x90); // only left DAC to left mixer enable 0db
    res |= es8388_write_reg(codec, ES8388_DACCONTROL20, 0x90); // only right DAC to right mixer enable 0db
    // set internal ADC and DAC use the same LRCK clock, ADC LRCK as internal LRCK
    res |= es8388_write_reg(codec, ES8388_DACCONTROL21, 0x80);
    res |= es8388_write_reg(codec, ES8388_DACCONTROL23, 0x00);    // vroi=0
    res |= es8388_set_adc_dac_volume(codec, ES_MODULE_DAC, 0, 0); // 0db

    res |= es8388_write_reg(codec, ES8388_DACCONTROL24, 0x1E); // Set L1 R1 L2 R2 volume. 0x00: -30dB, 0x1E: 0dB, 0x21: 3dB
    res |= es8388_write_reg(codec, ES8388_DACCONTROL25, 0x1E);
    res |= es8388_write_reg(codec, ES8388_DACCONTROL26, 0);
    res |= es8388_write_reg(codec, ES8388_DACCONTROL27, 0);

    // TODO default use DAC_ALL
    int tmp = DAC_OUTPUT_LOUT1 | DAC_OUTPUT_LOUT2 | DAC_OUTPUT_ROUT1 | DAC_OUTPUT_ROUT2;
    res |= es8388_write_reg(codec, ES8388_DACPOWER, tmp); // 0x3c Enable DAC and Enable Lout/Rout/1/2
    /* adc */
    res |= es8388_write_reg(codec, ES8388_ADCPOWER, 0xFF);
    res |= es8388_write_reg(codec, ES8388_ADCCONTROL1, 0xbb); // MIC Left and Right channel PGA gain
    tmp = 0;
    // TODO default use ADC LINE1
    // 0x00 LINSEL & RINSEL, LIN1/RIN1 as ADC Input; DSSEL,use one DS Reg11; DSR, LINPUT1-RINPUT1
    res |= es8388_write_reg(codec, ES8388_ADCCONTROL2, ADC_INPUT_LINPUT1_RINPUT1);
    res |= es8388_write_reg(codec, ES8388_ADCCONTROL3, 0x02);
    res |= es8388_write_reg(codec, ES8388_ADCCONTROL4, 0x0c); // 16 Bits length and I2S serial audio data format
    res |= es8388_write_reg(codec, ES8388_ADCCONTROL5, 0x02); // ADCFsMode,singel SPEED,RATIO=256
    // ALC for Microphone
    res |= es8388_set_adc_dac_volume(codec, ESP_CODEC_DEV_WORK_MODE_ADC, 0, 0); // 0db
    res |= es8388_write_reg(codec, ES8388_ADCPOWER, 0x09); // Power on ADC
    if (res != 0) {
        ESP_LOGI(TAG, "Fail to write register");
        return ESP_CODEC_DEV_WRITE_FAIL;
    }
    codec->is_open = true;
    return ESP_CODEC_DEV_OK;
}

static int es8388_enable(const audio_codec_if_t *h, bool enable)
{
    audio_codec_es8388_t *codec = (audio_codec_es8388_t *) h;
    if (codec == NULL) {
        return ESP_CODEC_DEV_INVALID_ARG;
    }
    if (codec->is_open == false) {
        return ESP_CODEC_DEV_WRONG_STATE;
    }
    if (codec->enabled == enable) {
        return ESP_CODEC_DEV_OK;
    }
    int res;
    if (enable == false) {
        es8388_pa_power(codec, false);
        res = es8388_stop(codec, codec->codec_mode);
    } else {
        res = es8388_start(codec, codec->codec_mode);
        es8388_pa_power(codec, true);
    }
    if (res == ESP_CODEC_DEV_OK) {
        codec->enabled = enable;
        ESP_LOGD(TAG, "Codec is %s", enable ? "enabled" : "disabled");
    }
    return res;
}

static int es8388_mute(const audio_codec_if_t *h, bool mute)
{
    audio_codec_es8388_t *codec = (audio_codec_es8388_t *) h;
    if (codec == NULL) {
        return ESP_CODEC_DEV_INVALID_ARG;
    }
    if (codec->is_open == false) {
        return ESP_CODEC_DEV_WRONG_STATE;
    }
    return es8388_set_voice_mute(codec, mute);
}

static int es8388_set_vol(const audio_codec_if_t *h, float db_value)
{
    audio_codec_es8388_t *codec = (audio_codec_es8388_t *) h;
    if (codec == NULL) {
        return ESP_CODEC_DEV_INVALID_ARG;
    }
    if (codec->is_open == false) {
        return ESP_CODEC_DEV_WRONG_STATE;
    }
    db_value -= codec->hw_gain;
    int volume = esp_codec_dev_vol_calc_reg(&vol_range, db_value);
    int res = es8388_write_reg(codec, ES8388_DACCONTROL5, volume);
    res |= es8388_write_reg(codec, ES8388_DACCONTROL4, volume);
    ESP_LOGD(TAG, "Set volume reg:%x db:%f", volume, db_value);
    return res ? ESP_CODEC_DEV_WRITE_FAIL : ESP_CODEC_DEV_OK;
}

static int es8388_set_fs(const audio_codec_if_t *h, esp_codec_dev_sample_info_t *fs)
{
    audio_codec_es8388_t *codec = (audio_codec_es8388_t *) h;
    if (codec == NULL || fs == NULL) {
        return ESP_CODEC_DEV_INVALID_ARG;
    }
    if (codec->is_open == false) {
        return ESP_CODEC_DEV_WRONG_STATE;
    }
    int res = 0;
    res |= es8388_config_fmt(codec, ESP_CODEC_DEV_WORK_MODE_BOTH, ES_I2S_NORMAL);
    res |= es8388_set_bits_per_sample(codec, ESP_CODEC_DEV_WORK_MODE_BOTH, fs->bits_per_sample);
    return res;
}

static int es8388_set_gain(const audio_codec_if_t *h, float db)
{
    audio_codec_es8388_t *codec = (audio_codec_es8388_t *) h;
    if (codec == NULL) {
        return ESP_CODEC_DEV_INVALID_ARG;
    }
    if (codec->is_open == false) {
        return ESP_CODEC_DEV_WRONG_STATE;
    }
    return es8388_set_mic_gain(codec, db);
}

static int es8388_close(const audio_codec_if_t *h)
{
    audio_codec_es8388_t *codec = (audio_codec_es8388_t *) h;
    if (codec == NULL) {
        return ESP_CODEC_DEV_INVALID_ARG;
    }
    if (codec->is_open) {
        es8388_pa_power(codec, false);
        codec->is_open = false;
    }
    return ESP_CODEC_DEV_OK;
}

static void es8388_dump(const audio_codec_if_t *h)
{
    audio_codec_es8388_t *codec = (audio_codec_es8388_t *) h;
    if (codec == NULL || codec->is_open == false) {
        return;
    }
    for (int i = 0; i <= ES8388_DACCONTROL30; i++) {
        int value = 0;
        int ret = es8388_read_reg(codec, i, &value);
        if (ret != ESP_CODEC_DEV_OK) {
            break;
        }
        ESP_LOGI(TAG, "%02x: %02x", i, value);
    }
}

const audio_codec_if_t *es8388_codec_new(es8388_codec_cfg_t *codec_cfg)
{
    // verify param
    if (codec_cfg == NULL || codec_cfg->ctrl_if == NULL) {
        ESP_LOGE(TAG, "Wrong codec config");
        return NULL;
    }
    if (codec_cfg->ctrl_if->is_open(codec_cfg->ctrl_if) == false) {
        ESP_LOGE(TAG, "Control interface not open yet");
        return NULL;
    }
    audio_codec_es8388_t *codec = (audio_codec_es8388_t *) calloc(1, sizeof(audio_codec_es8388_t));
    if (codec == NULL) {
        CODEC_MEM_CHECK(codec);
        return NULL;
    }
    codec->ctrl_if = codec_cfg->ctrl_if;
    codec->base.open = es8388_open;
    codec->base.enable = es8388_enable;
    codec->base.set_fs = es8388_set_fs;
    codec->base.set_vol = es8388_set_vol;
    codec->base.mute = es8388_mute;
    codec->base.set_mic_gain = es8388_set_gain;
    codec->base.dump_reg = es8388_dump;
    codec->base.close = es8388_close;
    codec->hw_gain = esp_codec_dev_col_calc_hw_gain(&codec_cfg->hw_gain);
    do {
        int ret = codec->base.open(&codec->base, codec_cfg, sizeof(es8388_codec_cfg_t));
        if (ret != 0) {
            ESP_LOGE(TAG, "Fail to open");
            break;
        }
        return &codec->base;
    } while (0);
    if (codec) {
        free(codec);
    }
    return NULL;
}
