/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#include <assert.h>
#include <errno.h>
#include <string.h>
#include "os/mynewt.h"
#include <pwm/pwm.h>
#include <hal/hal_bsp.h>
#include <hal/hal_gpio.h>

#define BASE_FREQ MYNEWT_VAL(OS_CPUTIME_FREQ)
#define MAX_FREQ BASE_FREQ / 2
#define DEV_COUNT MYNEWT_VAL(SOFT_PWM_DEVS)
#define CHAN_COUNT MYNEWT_VAL(SOFT_PWM_CHANS)
#define NO_PIN 0xff

struct soft_pwm_channel {
    uint8_t pin;
    bool inverted;
    uint16_t fraction;
    bool running;
    struct hal_timer toggle_timer;
};

struct soft_pwm_dev {
    bool playing;
    uint32_t frequency;
    uint16_t top_value;
    uint32_t n_cycles;
    uint32_t cycle_cnt;
    user_handler_t cycle_handler;
    user_handler_t seq_end_handler;
    void* cycle_data;
    void* seq_end_data;
    struct hal_timer cycle_timer;
    struct soft_pwm_channel chans[CHAN_COUNT];
};

static struct soft_pwm_dev instances[DEV_COUNT];
static struct pwm_dev os_dev_spwm[DEV_COUNT];

/**
 * Cycle start callback
 *
 * Initializes every channel's output to high (or low accrding to its polarity).
 * Schedules toggle_cb for every channel.
 */
static void cycle_cb(void* arg)
{
    int cnum;
    bool inverted;
    struct soft_pwm_dev* instance = (struct soft_pwm_dev *) arg;
    struct soft_pwm_channel* chans = instance->chans;
    uint32_t now = os_cputime_get32();

    if (instance->n_cycles) {
        instance->cycle_cnt++;
        instance->playing = (instance->cycle_cnt < instance->n_cycles);
    }

    if (instance->playing) {
        for (cnum = 0; cnum < CHAN_COUNT; cnum++) {
            if (chans[cnum].running) {
                inverted = chans[cnum].inverted;
                hal_gpio_write(chans[cnum].pin, (inverted) ? 0 : 1);

                os_cputime_timer_start(&chans[cnum].toggle_timer,
                                   now + chans[cnum].fraction);
            }
        }
        os_cputime_timer_start(&instance->cycle_timer,
                               now + instance->top_value);

        if (instance->cycle_handler) {
            instance->cycle_handler(instance->cycle_data);
        }

    } else if (instance->seq_end_handler) {
        instance->seq_end_handler(instance->seq_end_data);
    }
}

/**
 * Channel output toggle callback
 *
 * Toggles a channel's output.
 */
static void toggle_cb(void* arg)
{
    uint32_t *pin = (uint32_t *) arg;
    hal_gpio_toggle(*pin);
}

/**
 * Open the Soft PWM device
 *
 * This function locks the device for access from other tasks.
 *
 * @param odev The OS device to open
 * @param wait The time in MS to wait.  If 0 specified, returns immediately
 *             if resource unavailable.  If OS_WAIT_FOREVER specified, blocks
 *             until resource is available.
 * @param arg
 * @return 0 on success, non-zero on failure.
 */
static int
soft_pwm_open(struct os_dev *odev, uint32_t wait, void *arg)
{
    uint32_t cnum;
    uint32_t stat = 0;
    struct pwm_dev *dev = (struct pwm_dev *) odev;
    struct soft_pwm_dev *instance = &instances[dev->pwm_instance_id];

    if (os_started()) {
        stat = os_mutex_pend(&dev->pwm_lock, wait);
        if (stat != OS_OK) {
            return (stat);
        }
    }

    if (odev->od_flags & OS_DEV_F_STATUS_OPEN) {
        os_mutex_release(&dev->pwm_lock);
        stat = OS_EBUSY;
        return (stat);
    }

    instance->frequency = 100;
    instance->top_value = BASE_FREQ / 100;
    os_cputime_timer_init(&instance->cycle_timer,
                          cycle_cb,
                          &instances[dev->pwm_instance_id]);

    instance->playing = false;
    for (cnum = 0; cnum < CHAN_COUNT; cnum++) {
        instance->chans[cnum].pin = NO_PIN;
        instance->chans[cnum].fraction = 0;
        instance->chans[cnum].inverted = false;
        instance->chans[cnum].running = false;
        os_cputime_timer_init(&instance->chans[cnum].toggle_timer,
                              toggle_cb,
                              &instance->chans[cnum].pin);
    }

    return (0);
}

/**
 * Close the Soft PWM device.
 *
 * This function unlocks the device.
 *
 * @param odev The device to close.
 */
static int
soft_pwm_close(struct os_dev *odev)
{
    uint8_t cnum;
    struct pwm_dev *dev = (struct pwm_dev *) odev;
    struct soft_pwm_dev *instance = &instances[dev->pwm_instance_id];

    os_cputime_timer_stop(&instance->cycle_timer);
    for (cnum = 0; cnum < CHAN_COUNT; cnum++) {
        os_cputime_timer_stop(&instance->chans[cnum].toggle_timer);
    }

    if (os_started()) {
        os_mutex_release(&dev->pwm_lock);
    }

    return (0);
}

/**
 * Configure a PWM device.
 *
 * @param dev The device to configure.
 * @param cfg Configuration data for this device. If NULL the device will be
 * given default configuration values.
 *
 * @return 0 on success, non-zero error code on failure.
 */
int
soft_pwm_configure_device(struct pwm_dev *dev, struct pwm_dev_cfg *cfg)
{
    struct soft_pwm_dev *instance = &instances[dev->pwm_instance_id];

    if (!cfg) {
        instance->cycle_handler = NULL;
        instance->seq_end_handler = NULL;
        instance->cycle_data = NULL;
        instance->seq_end_data = NULL;
        cfg->n_cycles = 0;
        return (0);
    }

    instance->n_cycles = (cfg->n_cycles) ? cfg->n_cycles : 0;

    /* Configure Interrupts  */
    if (cfg->cycle_handler || cfg->seq_end_handler) {
        instance->cycle_handler = (user_handler_t) cfg->cycle_handler;
        instance->seq_end_handler = (user_handler_t) cfg->seq_end_handler;
        instance->cycle_data = cfg->cycle_data;
        instance->seq_end_data = cfg->seq_end_data;
    }

    return (0);
}

/**
 * Configure a channel on the PWM device.
 *
 * @param dev The device to configure.
 * @param cnum The channel number to configure.
 * @param cfg Configuration data for this channel. If NULL the channel will be
 * disabled or given default configuration values.
 *
 * @return 0 on success, non-zero error code on failure.
 */
static int
soft_pwm_configure_channel(struct pwm_dev *dev,
                           uint8_t cnum,
                           struct pwm_chan_cfg *cfg)
{
    uint32_t last_pin;
    struct soft_pwm_dev *instance = &instances[dev->pwm_instance_id];
    struct soft_pwm_channel *chan = &instance->chans[cnum];

    last_pin = (chan->pin == NO_PIN) ?
        cfg->pin :
        chan->pin;

    /* Set the previously used pin to low */
    if (cfg->pin != last_pin) {
        if (chan->running) {
            chan->running = false;
            os_cputime_timer_stop(&chan->toggle_timer);
        }
        hal_gpio_write(last_pin, 0);
    }

    if (cfg) {
        chan->pin = cfg->pin;
        chan->inverted = cfg->inverted;
        hal_gpio_init_out(cfg->pin, (cfg->inverted) ? 1 : 0);
    } else { /* unconfigure the channel */
        chan->pin = NO_PIN;
        chan->inverted = false;
        chan->fraction = 0;
        chan->running = false;
    }

    return (0);
}

/**
 * Enable the PWM with specified duty cycle.
 *
 * This duty cycle is a fractional duty cycle where 0 == off, clk_freq/pwm_freq=on,
 * and any value in between is on for fraction clocks and off
 * for 65535-fraction clocks.
 *
 * @param dev The device to configure.
 * @param cnum The channel number. The channel should be in use.
 * @param fraction The fraction value.
 *
 * @return 0 on success, negative on error.
 */
static int
soft_pwm_enable(struct pwm_dev *dev)
{
    uint8_t cnum;
    bool level;
    struct soft_pwm_dev* instance = &instances[dev->pwm_instance_id];
    struct soft_pwm_channel* chans = instance->chans;

    /* Handling 0% and 100% duty cycle channels. */
    for (cnum = 0; cnum < CHAN_COUNT; cnum++) {
        if ((chans[cnum].pin != NO_PIN) && (!chans[cnum].running)) {
            level = (chans[cnum].inverted && chans[cnum].fraction == 0) ||
                (!chans[cnum].inverted && chans[cnum].fraction != 0);
            hal_gpio_write(chans[cnum].pin, (level) ? 1 : 0);
        }
    }

    if (instance->n_cycles) {
        instance->cycle_cnt = 0;
    }

    instance->playing = true;
    cycle_cb(instance);
    return (0);
}

/**
 * Enable the PWM with specified duty cycle.
 *
 * This duty cycle is a fractional duty cycle where 0 == off, clk_freq/pwm_freq=on,
 * and any value in between is on for fraction clocks and off
 * for 65535-fraction clocks.
 *
 * @param dev The device to configure.
 * @param cnum The channel number. The channel should be in use.
 * @param fraction The fraction value.
 *
 * @return 0 on success, negative on error.
 */
static int
soft_pwm_set_duty_cycle(struct pwm_dev *dev, uint8_t cnum, uint16_t fraction)
{
    bool level;
    struct soft_pwm_dev* instance = &instances[dev->pwm_instance_id];
    struct soft_pwm_channel* chan = &instance->chans[cnum];
    assert (chan->pin != NO_PIN);

    chan->fraction = (fraction <= instance->top_value) ?
        fraction :
        instance->top_value;

    /* Handling 0% and 100% duty cycles. */
    if ((fraction < instance->top_value) && (fraction > 0)) {
        chan->running = true;
    } else {
        chan->running = false;
        os_cputime_timer_stop(&chan->toggle_timer);
        if (instance->playing) {
            level = (chan->inverted && fraction == 0) ||
                (!chan->inverted && fraction != 0);
            hal_gpio_write(chan->pin, (level) ? 1 : 0);
        }
    }
    return (0);
}

/**
 * Check whether a PWM channel is enabled on a given device.
 *
 * @param dev The device which the channel belongs to.
 * @param cnum The channel being queried.
 *
 * @return true if enabled, false if not.
 */
static bool
soft_pwm_is_enabled(struct pwm_dev *dev)
{
    return instances[dev->pwm_instance_id].playing;
}

/**
 * Disable the PWM device, the device will stop playing while
 * remaining configured.
 *
 * @param dev The device to disable.
 *
 * @return 0 on success, negative on error.
 */
static int
soft_pwm_disable(struct pwm_dev *dev)
{
    uint8_t cnum;
    struct soft_pwm_dev* instance = &instances[dev->pwm_instance_id];
    instance->playing = false;
    os_cputime_timer_stop(&instance->cycle_timer);

    for (cnum = 0; cnum < CHAN_COUNT; cnum++) {
        if (instance->chans[cnum].pin != NO_PIN) {
            os_cputime_timer_stop(&instance->chans[cnum].toggle_timer);
            hal_gpio_write(instance->chans[cnum].pin, 0);
        }
    }
    return (0);
}

/**
 * This frequency must be between 1/2 the clock frequency and
 * the clock divided by the resolution. NOTE: This may affect
 * other PWM channels.
 *
 * @param dev The device to configure.
 * @param freq_hz The frequency value in Hz.
 *
 * @return A value is in Hz on success, negative on error.
 */
static int
soft_pwm_set_frequency(struct pwm_dev *dev, uint32_t freq_hz)
{
    freq_hz = (freq_hz > MAX_FREQ) ? MAX_FREQ : freq_hz;
    freq_hz = (freq_hz < 2) ? 2 : freq_hz;

    instances[dev->pwm_instance_id].top_value = BASE_FREQ / freq_hz;

    return BASE_FREQ;
}

/**
 * Get the underlying clock driving the PWM device.
 *
 * @param dev
 *
 * @return value is in Hz on success, negative on error.
 */
static int
soft_pwm_get_clock_freq(struct pwm_dev *dev)
{
    return BASE_FREQ;
}

/**
 * Get the top value for the cycle counter, i.e. the value which sets
 * the duty cycle to 100%.
 *
 * @param dev
 *
 * @return value in cycles on success, negative on error.
 */
int
soft_pwm_get_top_value(struct pwm_dev *dev)
{
    return (instances[dev->pwm_instance_id].top_value);
}

/**
 * Get the resolution of the PWM in bits.
 *
 * @param dev The device to query.
 *
 * @return The value in bits on success, negative on error.
 */
static int
soft_pwm_get_resolution_bits(struct pwm_dev *dev)
{
    int shamt;
    uint16_t mask = 0x8000;
    uint16_t top_val = instances[dev->pwm_instance_id].top_value;

    for (shamt = 0; shamt < 15; shamt++) {
        if (top_val & mask) {
            break;
        }
        mask >>= 1;
    }

    return (16 - shamt - 1);
}

/**
 * Callback to initialize an adc_dev structure from the os device
 * initialization callback.  This sets up a soft_pwm_device(), so
 * that subsequent lookups to this device allow us to manipulate it.
 */
int
soft_pwm_dev_init(struct os_dev *odev, void *arg)
{
    struct pwm_dev *dev;
    struct pwm_driver_funcs *pwm_funcs;

    assert(odev);
    dev = (struct pwm_dev *) odev;

    /*
     * Originally arg was expected to be a pointer to int with pwm_instance_id,
     * however this wastes sizeof(int) of memory only to keep a simple number
     * which is used only once. Instead, instance_id can be passed as int casted
     * to pointer using helper macro so let's handle both versions here:
     * - if number is valid instance_id, let's use it directly
     * - otherwise assume it's a valid pointer
     */
    if (POINTER_TO_UINT(arg) < DEV_COUNT) {
        dev->pwm_instance_id = POINTER_TO_UINT(arg);
    } else {
        dev->pwm_instance_id = *((int*) arg);
        assert(dev->pwm_instance_id < DEV_COUNT);
    }

    dev->pwm_chan_count = CHAN_COUNT;
    os_mutex_init(&dev->pwm_lock);

    OS_DEV_SETHANDLERS(odev, soft_pwm_open, soft_pwm_close);

    pwm_funcs = &dev->pwm_funcs;
    pwm_funcs->pwm_configure_device = soft_pwm_configure_device;
    pwm_funcs->pwm_configure_channel = soft_pwm_configure_channel;
    pwm_funcs->pwm_set_duty_cycle = soft_pwm_set_duty_cycle;
    pwm_funcs->pwm_enable = soft_pwm_enable;
    pwm_funcs->pwm_is_enabled = soft_pwm_is_enabled;
    pwm_funcs->pwm_set_frequency = soft_pwm_set_frequency;
    pwm_funcs->pwm_get_clock_freq = soft_pwm_get_clock_freq;
    pwm_funcs->pwm_get_top_value = soft_pwm_get_top_value;
    pwm_funcs->pwm_get_resolution_bits = soft_pwm_get_resolution_bits;
    pwm_funcs->pwm_disable = soft_pwm_disable;

    return (0);
}

void
soft_pwm_pkg_init()
{
    int rc;
    int idx;
    char *spwm_name;

    /* Ensure this function only gets called by sysinit. */
    SYSINIT_ASSERT_ACTIVE();

    for (idx = 0; idx < DEV_COUNT; idx++) {
        asprintf(&spwm_name, "spwm%d", idx);
        rc = os_dev_create(&os_dev_spwm[idx].pwm_os_dev,
                           spwm_name,
                           OS_DEV_INIT_KERNEL,
                           OS_DEV_INIT_PRIO_DEFAULT,
                           soft_pwm_dev_init,
                           UINT_TO_POINTER(idx));
        SYSINIT_PANIC_ASSERT(rc == 0);
    }
}
