/***************************************************************************
*             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2009 Delyan Kratunov
 *
 * 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 "plugin.h"

#include "lib/helper.h"
#include "lib/xlcd.h"
#include "math.h"
#include "fracmul.h"
#ifndef HAVE_LCD_COLOR
#include "lib/grey.h"
#endif
#include "lib/mylcd.h"

PLUGIN_HEADER

#ifndef HAVE_LCD_COLOR
GREY_INFO_STRUCT
#endif

#if CONFIG_KEYPAD == ARCHOS_AV300_PAD
#   define FFT_PREV_GRAPH   BUTTON_LEFT
#   define FFT_NEXT_GRAPH   BUTTON_RIGHT
#   define FFT_ORIENTATION  BUTTON_F3
#   define FFT_WINDOW       BUTTON_F1
#   define FFT_AMP_SCALE    BUTTON_UP
#   define FFT_QUIT         BUTTON_OFF

#elif (CONFIG_KEYPAD == IRIVER_H100_PAD) || \
      (CONFIG_KEYPAD == IRIVER_H300_PAD)
#   define FFT_PREV_GRAPH   BUTTON_LEFT
#   define FFT_NEXT_GRAPH   BUTTON_RIGHT
#   define FFT_ORIENTATION  BUTTON_REC
#   define FFT_WINDOW       BUTTON_SELECT
#   define FFT_AMP_SCALE    BUTTON_UP
#   define FFT_FREQ_SCALE   BUTTON_DOWN
#   define FFT_QUIT         BUTTON_OFF

#elif (CONFIG_KEYPAD == IPOD_4G_PAD) || \
      (CONFIG_KEYPAD == IPOD_3G_PAD) || \
      (CONFIG_KEYPAD == IPOD_1G2G_PAD)
#   define MINESWP_SCROLLWHEEL
#   define FFT_PREV_GRAPH   BUTTON_LEFT
#   define FFT_NEXT_GRAPH   BUTTON_RIGHT
#   define FFT_ORIENTATION  (BUTTON_SELECT | BUTTON_LEFT)
#   define FFT_WINDOW       (BUTTON_SELECT | BUTTON_RIGHT)
#   define FFT_AMP_SCALE    BUTTON_MENU
#   define FFT_FREQ_SCALE   BUTTON_PLAY
#   define FFT_QUIT         (BUTTON_SELECT | BUTTON_MENU)

#elif (CONFIG_KEYPAD == IAUDIO_X5M5_PAD)
#   define FFT_PREV_GRAPH   BUTTON_LEFT
#   define FFT_NEXT_GRAPH   BUTTON_RIGHT
#   define FFT_ORIENTATION  BUTTON_SELECT
#   define FFT_WINDOW       BUTTON_PLAY
#   define FFT_AMP_SCALE    BUTTON_UP
#   define FFT_FREQ_SCALE   BUTTON_DOWN
#   define FFT_QUIT         BUTTON_POWER

#elif (CONFIG_KEYPAD == GIGABEAT_PAD)
#   define FFT_PREV_GRAPH   BUTTON_LEFT
#   define FFT_NEXT_GRAPH   BUTTON_RIGHT
#   define FFT_AMP_SCALE    BUTTON_UP
#   define FFT_FREQ_SCALE   BUTTON_DOWN
#   define FFT_ORIENTATION  BUTTON_SELECT
#   define FFT_WINDOW       BUTTON_A
#   define FFT_QUIT         BUTTON_POWER

#elif (CONFIG_KEYPAD == SANSA_E200_PAD)
#   define FFT_PREV_GRAPH   BUTTON_LEFT
#   define FFT_NEXT_GRAPH   BUTTON_RIGHT
#   define FFT_ORIENTATION  BUTTON_SELECT
#   define FFT_WINDOW       BUTTON_REC
#   define FFT_AMP_SCALE    BUTTON_UP
#   define FFT_FREQ_SCALE   BUTTON_DOWN
#   define FFT_QUIT         BUTTON_POWER

#elif (CONFIG_KEYPAD == SANSA_FUZE_PAD)
#   define FFT_PREV_GRAPH   BUTTON_LEFT
#   define FFT_NEXT_GRAPH   BUTTON_RIGHT
#   define FFT_ORIENTATION  (BUTTON_SELECT | BUTTON_LEFT)
#   define FFT_WINDOW       (BUTTON_SELECT | BUTTON_RIGHT)
#   define FFT_AMP_SCALE    BUTTON_UP
#   define FFT_FREQ_SCALE   BUTTON_DOWN
#   define FFT_QUIT     (BUTTON_HOME|BUTTON_REPEAT)

#elif (CONFIG_KEYPAD == SANSA_C200_PAD)
#   define FFT_PREV_GRAPH   BUTTON_LEFT
#   define FFT_NEXT_GRAPH   BUTTON_RIGHT
#   define FFT_ORIENTATION  BUTTON_UP
#   define FFT_WINDOW       BUTTON_REC
#   define FFT_AMP_SCALE    BUTTON_SELECT
#   define FFT_QUIT         BUTTON_POWER
#elif (CONFIG_KEYPAD == SANSA_M200_PAD)
#   define FFT_PREV_GRAPH   BUTTON_LEFT
#   define FFT_NEXT_GRAPH   BUTTON_RIGHT
#   define FFT_ORIENTATION  BUTTON_UP
#   define FFT_WINDOW       BUTTON_DOWN
#   define FFT_AMP_SCALE    BUTTON_SELECT
#   define FFT_QUIT         BUTTON_POWER
#elif (CONFIG_KEYPAD == SANSA_CLIP_PAD)
#   define FFT_PREV_GRAPH   BUTTON_LEFT
#   define FFT_NEXT_GRAPH   BUTTON_RIGHT
#   define FFT_ORIENTATION  BUTTON_UP
#   define FFT_WINDOW       BUTTON_HOME
#   define FFT_AMP_SCALE    BUTTON_SELECT
#   define FFT_QUIT         BUTTON_POWER

#elif (CONFIG_KEYPAD == IRIVER_H10_PAD)
#   define FFT_PREV_GRAPH   BUTTON_LEFT
#   define FFT_NEXT_GRAPH   BUTTON_RIGHT
#   define FFT_ORIENTATION  BUTTON_FF
#   define FFT_WINDOW       BUTTON_SCROLL_UP
#   define FFT_AMP_SCALE    BUTTON_REW
#   define FFT_FREQ_SCALE   BUTTON_PLAY
#   define FFT_QUIT         BUTTON_POWER

#elif (CONFIG_KEYPAD == GIGABEAT_S_PAD)
#   define FFT_PREV_GRAPH   BUTTON_LEFT
#   define FFT_NEXT_GRAPH   BUTTON_RIGHT
#   define FFT_ORIENTATION  BUTTON_MENU
#   define FFT_WINDOW       BUTTON_PREV
#   define FFT_AMP_SCALE    BUTTON_UP
#   define FFT_FREQ_SCALE   BUTTON_DOWN
#   define FFT_QUIT         BUTTON_BACK

#elif (CONFIG_KEYPAD == MROBE100_PAD)
#   define FFT_PREV_GRAPH   BUTTON_LEFT
#   define FFT_NEXT_GRAPH   BUTTON_RIGHT
#   define FFT_ORIENTATION  BUTTON_PLAY
#   define FFT_WINDOW       BUTTON_SELECT
#   define FFT_AMP_SCALE    BUTTON_UP
#   define FFT_FREQ_SCALE   BUTTON_DOWN
#   define FFT_QUIT         BUTTON_POWER

#elif CONFIG_KEYPAD == IAUDIO_M3_PAD
#   define FFT_PREV_GRAPH   BUTTON_RC_REW
#   define FFT_NEXT_GRAPH   BUTTON_RC_FF
#   define FFT_ORIENTATION  BUTTON_RC_MODE
#   define FFT_WINDOW       BUTTON_RC_PLAY
#   define FFT_AMP_SCALE    BUTTON_RC_VOL_UP
#   define FFT_QUIT         BUTTON_RC_REC

#elif (CONFIG_KEYPAD == COWON_D2_PAD)
#   define FFT_QUIT         BUTTON_POWER
#   define FFT_PREV_GRAPH   BUTTON_PLUS
#   define FFT_NEXT_GRAPH   BUTTON_MINUS

#elif CONFIG_KEYPAD == CREATIVEZVM_PAD
#   define FFT_PREV_GRAPH   BUTTON_LEFT
#   define FFT_NEXT_GRAPH   BUTTON_RIGHT
#   define FFT_ORIENTATION  BUTTON_MENU
#   define FFT_WINDOW       BUTTON_SELECT
#   define FFT_AMP_SCALE    BUTTON_UP
#   define FFT_FREQ_SCALE   BUTTON_DOWN
#   define FFT_QUIT         BUTTON_BACK

#elif CONFIG_KEYPAD == PHILIPS_HDD1630_PAD
#   define FFT_PREV_GRAPH   BUTTON_LEFT
#   define FFT_NEXT_GRAPH   BUTTON_RIGHT
#   define FFT_ORIENTATION  BUTTON_SELECT
#   define FFT_WINDOW       BUTTON_MENU
#   define FFT_AMP_SCALE    BUTTON_UP
#   define FFT_FREQ_SCALE   BUTTON_DOWN
#   define FFT_QUIT         BUTTON_POWER

#elif (CONFIG_KEYPAD == SAMSUNG_YH_PAD)
#   define FFT_PREV_GRAPH   BUTTON_LEFT
#   define FFT_NEXT_GRAPH   BUTTON_RIGHT
#   define FFT_ORIENTATION  BUTTON_UP
#   define FFT_WINDOW       BUTTON_DOWN
#   define FFT_AMP_SCALE    BUTTON_FFWD
#   define FFT_QUIT         BUTTON_PLAY

#elif (CONFIG_KEYPAD == MROBE500_PAD)
#   define FFT_QUIT         BUTTON_POWER

#elif (CONFIG_KEYPAD == ONDAVX747_PAD)
#   define FFT_QUIT         BUTTON_POWER

#elif (CONFIG_KEYPAD == ONDAVX777_PAD)
#   define FFT_QUIT         BUTTON_POWER

#elif (CONFIG_KEYPAD == PBELL_VIBE500_PAD)
#   define FFT_PREV_GRAPH   BUTTON_PREV
#   define FFT_NEXT_GRAPH   BUTTON_NEXT
#   define FFT_ORIENTATION  BUTTON_MENU
#   define FFT_WINDOW       BUTTON_OK
#   define FFT_AMP_SCALE    BUTTON_PLAY
#   define FFT_QUIT         BUTTON_REC

#elif CONFIG_KEYPAD == MPIO_HD200_PAD
#   define FFT_PREV_GRAPH   BUTTON_PREV
#   define FFT_NEXT_GRAPH   BUTTON_NEXT
#   define FFT_ORIENTATION  BUTTON_REC
#   define FFT_WINDOW       BUTTON_SELECT
#   define FFT_AMP_SCALE    BUTTON_PLAY
#   define FFT_QUIT        (BUTTON_REC | BUTTON_PLAY)

#else
#error No keymap defined!
#endif

#ifdef HAVE_TOUCHSCREEN
#ifndef FFT_PREV_GRAPH
#   define FFT_PREV_GRAPH   BUTTON_MIDLEFT
#endif
#ifndef FFT_NEXT_GRAPH
#   define FFT_NEXT_GRAPH   BUTTON_MIDRIGHT
#endif
#ifndef FFT_ORIENTATION
#   define FFT_ORIENTATION  BUTTON_CENTER
#endif
#ifndef FFT_WINDOW
#   define FFT_WINDOW       BUTTON_TOPLEFT
#endif
#ifndef FFT_AMP_SCALE
#   define FFT_AMP_SCALE    BUTTON_TOPRIGHT
#endif
#ifndef FFT_QUIT
#   define FFT_QUIT         BUTTON_BOTTOMLEFT
#endif
#endif /* HAVE_TOUCHSCREEN */

#ifdef HAVE_LCD_COLOR
#include "pluginbitmaps/fft_colors.h"
#endif

#include "kiss_fftr.h"
#include "_kiss_fft_guts.h" /* sizeof(struct kiss_fft_state) */
#include "const.h"

#define LCD_SIZE MAX(LCD_WIDTH, LCD_HEIGHT)

#if (LCD_SIZE <= 511)
#define FFT_SIZE 1024 /* 512*2 */
#elif (LCD_SIZE <= 1023)
#define FFT_SIZE 2048 /* 1024*2 */
#else
#define FFT_SIZE 4096 /* 2048*2 */
#endif

#define ARRAYLEN_IN (FFT_SIZE)
#define ARRAYLEN_OUT (FFT_SIZE)
#define ARRAYLEN_PLOT (FFT_SIZE/2-1) /* FFT is symmetric, ignore DC */
#define BUFSIZE_FFT (sizeof(struct kiss_fft_state)+sizeof(kiss_fft_cpx)*(FFT_SIZE-1))

#define __COEFF(type,size) type##_##size
#define _COEFF(x, y) __COEFF(x,y) /* force the preprocessor to evaluate FFT_SIZE) */
#define HANN_COEFF _COEFF(hann, FFT_SIZE)
#define HAMMING_COEFF _COEFF(hamming, FFT_SIZE)

/****************************** Globals ****************************/
/* cacheline-aligned buffers with COP, otherwise word-aligned */
/* CPU/COP only applies when compiled for more than one core */

#define CACHEALIGN_UP_SIZE(type, len) \
    (CACHEALIGN_UP((len)*sizeof(type) + (sizeof(type)-1)) / sizeof(type))
/* Shared */
/* COP + CPU PCM */
static kiss_fft_cpx input[CACHEALIGN_UP_SIZE(kiss_fft_scalar, ARRAYLEN_IN)]
                            CACHEALIGN_AT_LEAST_ATTR(4);
/* CPU+COP */
#if NUM_CORES > 1
/* Output queue indexes */
static volatile int output_head SHAREDBSS_ATTR = 0;
static volatile int output_tail SHAREDBSS_ATTR = 0;
/* The result is nfft/2 complex frequency bins from DC to Nyquist. */
static kiss_fft_cpx output[2][CACHEALIGN_UP_SIZE(kiss_fft_cpx, ARRAYLEN_OUT)]
                                SHAREDBSS_ATTR;
#else
/* Only one output buffer */
#define output_head 0
#define output_tail 0
/* The result is nfft/2 complex frequency bins from DC to Nyquist. */
static kiss_fft_cpx output[1][ARRAYLEN_OUT];
#endif

/* Unshared */
/* COP */
static kiss_fft_cfg fft_state SHAREDBSS_ATTR;
static char fft_buffer[CACHEALIGN_UP_SIZE(char, BUFSIZE_FFT)]
                CACHEALIGN_AT_LEAST_ATTR(4);
/* CPU */
static int32_t plot_history[ARRAYLEN_PLOT];
static int32_t plot[ARRAYLEN_PLOT];
static struct
{
    int16_t bin;   /* integer bin number */
    uint16_t frac; /* interpolation fraction */
} binlog[ARRAYLEN_PLOT] __attribute__((aligned(4)));

enum fft_window_func
{
    FFT_WF_FIRST = 0,
    FFT_WF_HAMMING = 0,
    FFT_WF_HANN,
};
#define FFT_WF_COUNT (FFT_WF_HANN+1)

enum fft_display_mode
{
    FFT_DM_FIRST = 0,
    FFT_DM_LINES = 0,
    FFT_DM_BARS,
    FFT_DM_SPECTROGRAPH,
};
#define FFT_DM_COUNT (FFT_DM_SPECTROGRAPH+1)

static const unsigned char* const modes_text[FFT_DM_COUNT] =
{ "Lines", "Bars", "Spectrogram" };

static const unsigned char* const amp_scales_text[2] =
{ "Linear amplitude", "Logarithmic amplitude" };

static const unsigned char* const freq_scales_text[2] =
{ "Linear frequency", "Logarithmic frequency" };

static const unsigned char* const window_text[FFT_WF_COUNT] =
{ "Hamming window", "Hann window" };

static struct {
    bool orientation_vertical;
    enum fft_display_mode mode;
    bool logarithmic_amp;
    bool logarithmic_freq;
    enum fft_window_func window_func;
    int spectrogram_pos; /* row or column - only used by one at a time */
    union
    {
        struct
        {
            bool orientation : 1;
            bool mode        : 1;
            bool amp_scale   : 1;
            bool freq_scale  : 1;
            bool window_func : 1;
            bool do_clear    : 1;
        };
        bool clear_all; /* Write 'false' to clear all above */
    } changed;
} graph_settings SHAREDDATA_ATTR = 
{
     /* Defaults */
    .orientation_vertical = true,
    .mode                 = FFT_DM_LINES,
    .logarithmic_amp      = true,
    .logarithmic_freq     = true,
    .window_func          = FFT_WF_HAMMING,
    .spectrogram_pos      = 0,
    .changed              = { .clear_all = false },
};

#ifdef HAVE_LCD_COLOR
#define SHADES BMPWIDTH_fft_colors
#define SPECTROGRAPH_PALETTE(index) (fft_colors[index])
#else
#define SHADES 256
#define SPECTROGRAPH_PALETTE(index) (255 - (index))
#endif

/************************* End of globals *************************/

/************************* Math functions *************************/

/* Based on feeding-in a 0db sinewave at FS/4 */
#define QLOG_MAX 0x0009154B
/* fudge it a little or it's not very visbile */
#define QLIN_MAX (0x00002266 >> 1)

/* Apply window function to input */
static void apply_window_func(enum fft_window_func mode)
{
    int i;

    switch(mode)
    {
        case FFT_WF_HAMMING:
            for(i = 0; i < ARRAYLEN_IN; ++i)
            {
                input[i].r = (input[i].r * HAMMING_COEFF[i] + 16384) >> 15;
            } 
            break;

        case FFT_WF_HANN:
            for(i = 0; i < ARRAYLEN_IN; ++i)
            {
                input[i].r = (input[i].r * HANN_COEFF[i] + 16384) >> 15;
            }
            break;
    }
}

/* Calculates the magnitudes from complex numbers and returns the maximum */
static int32_t calc_magnitudes(bool logarithmic_amp)
{
    /* A major assumption made when calculating the Q*MAX constants 
     * is that the maximum magnitude is 29 bits long. */
    uint32_t max = 0;
    kiss_fft_cpx *this_output = output[output_head] + 1; /* skip DC */
    int i;

    /* Calculate the magnitude, discarding the phase. */
    for(i = 0; i < ARRAYLEN_PLOT; ++i)
    {
        int32_t re = this_output[i].r;
        int32_t im = this_output[i].i;

        uint32_t tmp = re*re + im*im;

        if(tmp > 0)
        {
            if(tmp > 0x7FFFFFFF) /* clip */
            {
                tmp = 0x7FFFFFFF; /* if our assumptions are correct,
                                     this should never happen. It's just
                                     a safeguard. */
            }

            if(logarithmic_amp)
            {
                if(tmp < 0x8000) /* be more precise */
                {
                    /* ln(x ^ .5) = .5*ln(x) */
                    tmp = fp16_log(tmp << 16) >> 1;
                }
                else
                {
                    tmp = isqrt(tmp); /* linear scaling, nothing
                                         bad should happen */
                    tmp = fp16_log(tmp << 16); /* the log function
                                                  expects s15.16 values */
                }
            }
            else
            {
                tmp = isqrt(tmp); /* linear scaling, nothing
                                     bad should happen */
            }
        }

        /* Length 2 moving average - last transform and this one */
        tmp = (plot_history[i] + tmp) >> 1;
        plot[i] = tmp;
        plot_history[i] = tmp;

        if(tmp > max)
            max = tmp;
    }

    return max;
}

/* Move plot bins into a logarithmic scale by sliding them towards the
 * Nyquist bin according to the translation in the binlog array. */
static void logarithmic_plot_translate(void)
{
    int i;

    for(i = ARRAYLEN_PLOT-1; i > 0; --i)
    {
        int bin;
        int s = binlog[i].bin;
        int e = binlog[i-1].bin;
        int frac = binlog[i].frac;

        bin = plot[s];

        if(frac)
        {
            /* slope < 1, Interpolate stretched bins (linear for now) */
            int diff = plot[s+1] - bin;

            do
            {
                plot[i] = bin + FRACMUL(frac << 15, diff);
                frac = binlog[--i].frac;
            }
            while(frac);
        }
        else
        {
            /* slope > 1, Find peak of two or more bins */
            while(--s > e)
            {
                int val = plot[s];

                if (val > bin)
                    bin = val;
            }
        }

        plot[i] = bin;
    }
}

/* Calculates the translation for logarithmic plot bins */
static void logarithmic_plot_init(void)
{
    int i, j;
    /*
     * log: y = round(n * ln(x) / ln(n))
     * anti: y = round(exp(x * ln(n) / n))
     */
    j = fp16_log((ARRAYLEN_PLOT - 1) << 16);
    for(i = 0; i < ARRAYLEN_PLOT; ++i)
    {
        binlog[i].bin = (fp16_exp(i * j / (ARRAYLEN_PLOT - 1)) + 32768) >> 16;
    }

    /* setup fractions for interpolation of stretched bins */
    for(i = 0; i < ARRAYLEN_PLOT-1; i = j)
    {
        j = i + 1;

        /* stop when we have two different values */
        while(binlog[j].bin == binlog[i].bin)
            j++; /* if here, local slope of curve is < 1 */

        if(j > i + 1)
        {
            /* distribute pieces evenly over stretched interval */
            int diff = j - i;
            int x = 0;
            do
            {
                binlog[i].frac = (x++ << 16) / diff;
            }
            while(++i < j);
        }
    }
}

/************************ End of math functions ***********************/

/********************* Plotting functions (modes) *********************/
static void draw_lines_vertical(void);
static void draw_lines_horizontal(void);
static void draw_bars_vertical(void);
static void draw_bars_horizontal(void);
static void draw_spectrogram_vertical(void);
static void draw_spectrogram_horizontal(void);

#define COLOR_DEFAULT_FG    MYLCD_DEFAULT_FG
#define COLOR_DEFAULT_BG    MYLCD_DEFAULT_BG

#ifdef HAVE_LCD_COLOR
#define COLOR_MESSAGE_FRAME LCD_RGBPACK(0xc6, 0x00, 0x00)
#define COLOR_MESSAGE_BG    LCD_BLACK
#define COLOR_MESSAGE_FG    LCD_WHITE
#else
#define COLOR_MESSAGE_FRAME GREY_DARKGRAY
#define COLOR_MESSAGE_BG    GREY_WHITE
#define COLOR_MESSAGE_FG    GREY_BLACK
#endif

#define POPUP_HPADDING      3 /* 3 px of horizontal padding and */
#define POPUP_VPADDING      2 /* 2 px of vertical padding */

static void draw_message_string(const unsigned char *message, bool active)
{
    int x, y;
    mylcd_getstringsize(message, &x, &y);

    /* x and y give the size of the box for the popup */
    x += POPUP_HPADDING*2;
    y += POPUP_VPADDING*2;

    /* In vertical spectrogram mode, leave space for the popup
     * before actually drawing it (if space is needed) */
    if(active &&
       graph_settings.mode == FFT_DM_SPECTROGRAPH &&
       graph_settings.orientation_vertical &&
       graph_settings.spectrogram_pos >= LCD_WIDTH - x) 
    {
        mylcd_scroll_left(graph_settings.spectrogram_pos -
                          LCD_WIDTH + x);
        graph_settings.spectrogram_pos = LCD_WIDTH - x - 1;
    }

    mylcd_set_foreground(COLOR_MESSAGE_FRAME);
    mylcd_fillrect(LCD_WIDTH - x, 0, LCD_WIDTH - 1, y);

    mylcd_set_foreground(COLOR_MESSAGE_FG);
    mylcd_set_background(COLOR_MESSAGE_BG);
    mylcd_putsxy(LCD_WIDTH - x + POPUP_HPADDING,
                 POPUP_VPADDING, message);
    mylcd_set_foreground(COLOR_DEFAULT_FG);
    mylcd_set_background(COLOR_DEFAULT_BG);
}

static void draw(const unsigned char* message)
{
    static long show_message_tick = 0;
    static const unsigned char* last_message = 0;

    if(message != NULL)
    {
        last_message = message;
        show_message_tick = (*rb->current_tick + HZ) | 1;
    }

    /* maybe take additional actions depending upon the changed setting */
    if(graph_settings.changed.orientation)
    {
       graph_settings.changed.amp_scale = true;
       graph_settings.changed.do_clear = true;
    }

    if(graph_settings.changed.mode)
    {
        graph_settings.changed.amp_scale = true;
        graph_settings.changed.do_clear = true;
    }

    if(graph_settings.changed.amp_scale)
        memset(plot_history, 0, sizeof (plot_history));

    if(graph_settings.changed.freq_scale)
        graph_settings.changed.freq_scale = true;

    mylcd_set_foreground(COLOR_DEFAULT_FG);
    mylcd_set_background(COLOR_DEFAULT_BG);

    switch (graph_settings.mode)
    {
        default:
        case FFT_DM_LINES: {

            mylcd_clear_display();

            if (graph_settings.orientation_vertical)
                draw_lines_vertical();
            else
                draw_lines_horizontal();
            break;
        }
        case FFT_DM_BARS: {

            mylcd_clear_display();

            if(graph_settings.orientation_vertical)
                draw_bars_vertical();
            else
                draw_bars_horizontal();

            break;
        }
        case FFT_DM_SPECTROGRAPH: {

            if(graph_settings.changed.do_clear)
            {
                graph_settings.spectrogram_pos = 0;
                mylcd_clear_display();
            }

            if(graph_settings.orientation_vertical)
                draw_spectrogram_vertical();
            else
                draw_spectrogram_horizontal();
            break;
        }
    }

    if(show_message_tick != 0)
    {
        if(TIME_BEFORE(*rb->current_tick, show_message_tick))
        {
    		/* We have a message to show */
            draw_message_string(last_message, true);
        }
        else
        {
            /* Stop drawing message */
            show_message_tick = 0;
        }
    }
    else if(last_message != NULL)
    {
        if(graph_settings.mode == FFT_DM_SPECTROGRAPH)
        {
    		/* Spectrogram mode - need to erase the popup */
			int x, y;
			mylcd_getstringsize(last_message, &x, &y);
			/* Recalculate the size */
			x += POPUP_HPADDING*2;
			y += POPUP_VPADDING*2;

			if(!graph_settings.orientation_vertical)
			{
				/* In horizontal spectrogram mode, just scroll up by Y lines */
				mylcd_scroll_up(y);
				graph_settings.spectrogram_pos -= y;
				if(graph_settings.spectrogram_pos < 0)
					graph_settings.spectrogram_pos = 0;
			}
			else
			{
				/* In vertical spectrogram mode, erase the popup */
                mylcd_set_foreground(COLOR_DEFAULT_BG);
				mylcd_fillrect(graph_settings.spectrogram_pos + 1, 0,
                               LCD_WIDTH, y);
                mylcd_set_foreground(COLOR_DEFAULT_FG);
			}
        }
        /* else These modes clear the screen themselves */
			
        last_message = NULL;
    }

    mylcd_update();

    graph_settings.changed.clear_all = false;
}

static void draw_lines_vertical(void)
{
    static int max = 0;

#if LCD_WIDTH < ARRAYLEN_PLOT /* graph compression */
    const int offset = 0;
    const int plotwidth = LCD_WIDTH;
#else
    const int offset = (LCD_HEIGHT - ARRAYLEN_PLOT) / 2;
    const int plotwidth = ARRAYLEN_PLOT;
#endif

    int this_max;
    int i, x;

    if(graph_settings.changed.amp_scale)
        max = 0; /* reset the graph on scaling mode change */

    this_max = calc_magnitudes(graph_settings.logarithmic_amp);

    if(this_max == 0)
    {
        mylcd_hline(0, LCD_WIDTH - 1, LCD_HEIGHT - 1); /* Draw all "zero" */
        return;
    }

    if(graph_settings.logarithmic_freq)
        logarithmic_plot_translate();

    /* take the maximum of neighboring bins if we have to scale the graph
     * horizontally */
    if(LCD_WIDTH < ARRAYLEN_PLOT) /* graph compression */
    {
        int bins_acc = LCD_WIDTH / 2;
        int bins_max = 0;
        
        i = 0, x = 0;

        for(;;)
        {
            int bin = plot[i++];

            if(bin > bins_max)
                bins_max = bin;

            bins_acc += LCD_WIDTH;

            if(bins_acc >= ARRAYLEN_PLOT)
            {
                plot[x] = bins_max;
                
                if(bins_max > max)
                    max = bins_max;

                if(++x >= LCD_WIDTH)
                    break;

                bins_acc -= ARRAYLEN_PLOT;
                bins_max = 0;
            }
        }
    }
    else
    {
        if(this_max > max)
            max = this_max;
    }

    for(x = 0; x < plotwidth; ++x)
    {
        int h = LCD_HEIGHT*plot[x] / max;
        mylcd_vline(x + offset, LCD_HEIGHT - h, LCD_HEIGHT-1);
    }
}

static void draw_lines_horizontal(void)
{
    static int max = 0;

#if LCD_WIDTH < ARRAYLEN_PLOT /* graph compression */
    const int offset = 0;
    const int plotwidth = LCD_HEIGHT;
#else
    const int offset = (LCD_HEIGHT - ARRAYLEN_PLOT) / 2;
    const int plotwidth = ARRAYLEN_PLOT;
#endif

    int this_max;
    int y;

    if(graph_settings.changed.amp_scale)
        max = 0; /* reset the graph on scaling mode change */

    this_max = calc_magnitudes(graph_settings.logarithmic_amp);

    if(this_max == 0)
    {
        mylcd_vline(0, 0, LCD_HEIGHT-1); /* Draw all "zero" */
        return;
    }

    if(graph_settings.logarithmic_freq)
        logarithmic_plot_translate();

    /* take the maximum of neighboring bins if we have to scale the graph
     * horizontally */
    if(LCD_HEIGHT < ARRAYLEN_PLOT) /* graph compression */
    {
        int bins_acc = LCD_HEIGHT / 2;
        int bins_max = 0;
        int i = 0;

        y = 0;

        for(;;)
        {
            int bin = plot[i++];

            if (bin > bins_max)
                bins_max = bin;

            bins_acc += LCD_HEIGHT;

            if(bins_acc >= ARRAYLEN_PLOT)
            {
                plot[y] = bins_max;

                if(bins_max > max)
                    max = bins_max;

                if(++y >= LCD_HEIGHT)
                    break;

                bins_acc -= ARRAYLEN_PLOT;
                bins_max = 0;
            }
        }
    }
    else
    {
        if(this_max > max)
            max = this_max;
    }

    for(y = 0; y < plotwidth; ++y)
    {
        int w = LCD_WIDTH*plot[y] / max;
        mylcd_hline(0, w - 1, y + offset);
    }
}

static void draw_bars_vertical(void)
{
    static int max = 0;

#if LCD_WIDTH < LCD_HEIGHT
    const int bars = 15;
#else
    const int bars = 20;
#endif
    const int border = 2;
    const int barwidth = LCD_WIDTH / (bars + border);
    const int width = barwidth - border;
    const int offset = (LCD_WIDTH - bars*barwidth) / 2;

    if(graph_settings.changed.amp_scale)
        max = 0; /* reset the graph on scaling mode change */

    mylcd_hline(0, LCD_WIDTH-1, LCD_HEIGHT-1); /* Draw baseline */

    if(calc_magnitudes(graph_settings.logarithmic_amp) == 0)
        return; /* nothing more to draw */

    if(graph_settings.logarithmic_freq)
        logarithmic_plot_translate();

    int bins_acc = bars / 2;
    int bins_max = 0;
    int x = 0, i = 0;

    for(;;)
    {
        int bin = plot[i++];

        if(bin > bins_max)
            bins_max = bin;

        bins_acc += bars;

        if(bins_acc >= ARRAYLEN_PLOT)
        {
            plot[x] = bins_max;

            if(bins_max > max)
                max = bins_max;

            if(++x >= bars)
                break;

            bins_acc -= ARRAYLEN_PLOT;
            bins_max = 0;
        }
    }

    for(i = 0, x = offset; i < bars; ++i, x += barwidth)
    {
        int h = LCD_HEIGHT * plot[i] / max;
        mylcd_fillrect(x, LCD_HEIGHT - h, width, h - 1);
    }
}

static void draw_bars_horizontal(void)
{
    static int max = 0;

#if LCD_WIDTH < LCD_HEIGHT
    const int bars = 20;
#else
    const int bars = 15;
#endif
    const int border = 2;
    const int barwidth = LCD_HEIGHT / (bars + border);
    const int height = barwidth - border;
    const int offset = (LCD_HEIGHT - bars*barwidth) / 2;

    if(graph_settings.changed.amp_scale)
        max = 0; /* reset the graph on scaling mode change */

    mylcd_vline(0, 0, LCD_HEIGHT-1); /* Draw baseline */

    if(calc_magnitudes(graph_settings.logarithmic_amp) == 0)
        return; /* nothing more to draw */

    if(graph_settings.logarithmic_freq)
        logarithmic_plot_translate();

    int bins_acc = bars / 2;
    int bins_max = 0;
    int y = 0, i = 0;

    for(;;)
    {
        int bin = plot[i++];

        if (bin > bins_max)
            bins_max = bin;

        bins_acc += bars;

        if(bins_acc >= ARRAYLEN_PLOT)
        {
            plot[y] = bins_max;

            if(bins_max > max)
                max = bins_max;

            if(++y >= bars)
                break;

            bins_acc -= ARRAYLEN_PLOT;
            bins_max = 0;
        }
    }

    for(i = 0, y = offset; i < bars; ++i, y += barwidth)
    {
        int w = LCD_WIDTH * plot[i] / max;
        mylcd_fillrect(1, y, w, height);
    }
}

static void draw_spectrogram_vertical(void)
{
    const int32_t scale_factor = MIN(LCD_HEIGHT, ARRAYLEN_PLOT);

    calc_magnitudes(graph_settings.logarithmic_amp);

    if(graph_settings.logarithmic_freq)
        logarithmic_plot_translate();

    int bins_acc = scale_factor / 2;
    int bins_max = 0;
    int y = 0, i = 0;

    for(;;)
    {
        int bin = plot[i++];

        if(bin > bins_max)
            bins_max = bin;

        bins_acc += scale_factor;

        if(bins_acc >= ARRAYLEN_PLOT)
        {
            unsigned index;

            if(graph_settings.logarithmic_amp)
                index = (SHADES-1)*bins_max / QLOG_MAX;
            else
                index = (SHADES-1)*bins_max / QLIN_MAX;

            /* These happen because we exaggerate the graph a little for
             * linear mode */
            if(index >= SHADES)
                index = SHADES-1;

            mylcd_set_foreground(SPECTROGRAPH_PALETTE(index));
            mylcd_drawpixel(graph_settings.spectrogram_pos,
                            scale_factor-1 - y);

            if(++y >= scale_factor)
                break;

            bins_acc -= ARRAYLEN_PLOT;
            bins_max = 0;
        }
    }

    if(graph_settings.spectrogram_pos < LCD_WIDTH-1)
        graph_settings.spectrogram_pos++;
    else
        mylcd_scroll_left(1);
}

static void draw_spectrogram_horizontal(void)
{
    const int32_t scale_factor = MIN(LCD_WIDTH, ARRAYLEN_PLOT);

    calc_magnitudes(graph_settings.logarithmic_amp);

    if(graph_settings.logarithmic_freq)
        logarithmic_plot_translate();

    int bins_acc = scale_factor / 2;
    int bins_max = 0;
    int x = 0, i = 0;

    for(;;)
    {
        int bin = plot[i++];

        if(bin > bins_max)
            bins_max = bin;

        bins_acc += scale_factor;

        if(bins_acc >= ARRAYLEN_PLOT)
        {
            unsigned index;

            if(graph_settings.logarithmic_amp)
                index = (SHADES-1)*bins_max / QLOG_MAX;
            else
                index = (SHADES-1)*bins_max / QLIN_MAX;

            /* These happen because we exaggerate the graph a little for
             * linear mode */
            if(index >= SHADES)
                index = SHADES-1;

            mylcd_set_foreground(SPECTROGRAPH_PALETTE(index));
            mylcd_drawpixel(x, graph_settings.spectrogram_pos);

            if(++x >= scale_factor)
                break;

            bins_acc -= ARRAYLEN_PLOT;
            bins_max = 0;
        }
    }

    if(graph_settings.spectrogram_pos < LCD_HEIGHT-1)
        graph_settings.spectrogram_pos++;
    else
        mylcd_scroll_up(1);
}

/********************* End of plotting functions (modes) *********************/

/****************************** FFT functions ********************************/

/** functions use in single/multi configuration **/
static inline bool fft_init_fft_lib(void)
{
    size_t size = sizeof(fft_buffer);
    fft_state = kiss_fft_alloc(FFT_SIZE, 0, fft_buffer, &size);

    if(fft_state == NULL)
    {
        DEBUGF("needed data: %i", (int) size);
        return false;
    }

    return true;
}

static inline bool fft_get_fft(void)
{
    int count;
    int16_t *value = (int16_t *) rb->pcm_get_peak_buffer(&count);
    /* This block can introduce discontinuities in our data. Meaning, the
     * FFT will not be done a continuous segment of the signal. Which can
     * be bad. Or not.
     * 
     * Anyway, this is a demo, not a scientific tool. If you want accuracy,
     * do a proper spectrum analysis.*/

    /* there are cases when we don't have enough data to fill the buffer */
    if(count != ARRAYLEN_IN)
    {
        if(count < ARRAYLEN_IN)
            return false;

        count = ARRAYLEN_IN;  /* too much - limit */
    }

    int fft_idx = 0; /* offset in 'input' */

    do
    {
        kiss_fft_scalar left = *value++;
        kiss_fft_scalar right = *value++;
        input[fft_idx].r = (left + right) >> 1; /* to mono */
    } while (fft_idx++, --count > 0);

    apply_window_func(graph_settings.window_func);

    rb->yield();

    kiss_fft(fft_state, input, output[output_tail]);

    rb->yield();

    return true;
}

#if NUM_CORES > 1
/* use a worker thread if there is another processor core */
static volatile bool fft_thread_run SHAREDDATA_ATTR = false;
static unsigned long fft_thread;

static long fft_thread_stack[CACHEALIGN_UP(DEFAULT_STACK_SIZE*4/sizeof(long))]
                             CACHEALIGN_AT_LEAST_ATTR(4);

static void fft_thread_entry(void)
{
    if (!fft_init_fft_lib())
    {
        output_tail = -1; /* tell that we bailed */
        fft_thread_run = true;
        return;
    }

    fft_thread_run = true;

    while(fft_thread_run)
	{
        if (!rb->pcm_is_playing())
        {
            rb->sleep(HZ/5);
            continue;
        }

        if (!fft_get_fft())
        {
            rb->sleep(0);    /* not enough - ease up */
            continue;
        }

#if NUM_CORES > 1
        /* write back output for other processor and invalidate for next frame read */
        rb->cpucache_invalidate();
#endif
        int new_tail = output_tail ^ 1;

        /* if full, block waiting until reader has freed a slot */
        while(fft_thread_run)
        {
            if(new_tail != output_head)
            {
                output_tail = new_tail;
                break;
            }

            rb->sleep(0);
        }
	}
}

static bool fft_have_fft(void)
{
    return output_head != output_tail;
}

/* Call only after fft_have_fft() has returned true */
static inline void fft_free_fft_output(void)
{
    output_head ^= 1; /* finished with this */
}

static bool fft_init_fft(void)
{
    /* create worker thread - on the COP for dual-core targets */
    fft_thread = rb->create_thread(fft_thread_entry,
            fft_thread_stack, sizeof(fft_thread_stack), 0, "fft output thread"
            IF_PRIO(, PRIORITY_USER_INTERFACE+1) IF_COP(, COP));

    if(fft_thread == 0)
    {
        rb->splash(HZ, "FFT thread failed create");
        return false;
    }

    /* wait for it to indicate 'ready' */
    while(fft_thread_run == false)
        rb->sleep(0);

    if(output_tail == -1)
    {
        /* FFT thread bailed-out like The Fed */
        rb->thread_wait(fft_thread);
        rb->splash(HZ, "FFT thread failed to init");
        return false;
    }

    return true;
}

static void fft_close_fft(void)
{
    /* Handle our FFT thread. */
    fft_thread_run = false;
    rb->thread_wait(fft_thread);
#if NUM_CORES > 1
    rb->cpucache_invalidate();
#endif
}
#else /* NUM_CORES == 1 */
/* everything serialize on single-core and FFT gets to use IRAM main stack if
 * target uses IRAM */
static bool fft_have_fft(void)
{
    return rb->pcm_is_playing() && fft_get_fft();
}

static inline void fft_free_fft_output(void)
{
    /* nothing to do */
}

static bool fft_init_fft(void)
{
    return fft_init_fft_lib();
}

static inline void fft_close_fft(void)
{
    /* nothing to do */
}
#endif /* NUM_CORES */
/*************************** End of FFT functions ****************************/

enum plugin_status plugin_start(const void* parameter)
{
    /* Defaults */
    bool run = true;
    bool showing_warning = false;

    if (!fft_init_fft())
        return PLUGIN_ERROR;

#ifndef HAVE_LCD_COLOR
    unsigned char *gbuf;
    size_t  gbuf_size = 0;
    /* get the remainder of the plugin buffer */
    gbuf = (unsigned char *) rb->plugin_get_buffer(&gbuf_size);

    /* initialize the greyscale buffer.*/
    if (!grey_init(gbuf, gbuf_size, GREY_ON_COP | GREY_BUFFERED,
                   LCD_WIDTH, LCD_HEIGHT, NULL))
    {
        rb->splash(HZ, "Couldn't init greyscale display");
        fft_close_fft();
        return PLUGIN_ERROR;
    }
    grey_show(true); 
#endif

    logarithmic_plot_init();

#if LCD_DEPTH > 1
    rb->lcd_set_backdrop(NULL);
    mylcd_clear_display();
    mylcd_update();
#endif
    backlight_force_on();

#ifdef HAVE_ADJUSTABLE_CPU_FREQ
    rb->cpu_boost(true);
#endif

    while (run)
    {
        /* Unless otherwise specified, HZ/50 is around the window length
         * and quite fast. We want to be done with drawing by this time. */
        long next_frame_tick = *rb->current_tick + HZ/50;
        int button;

        while (!fft_have_fft())
		{
            int timeout;

            if(!rb->pcm_is_playing())
            {
                showing_warning = true;
                mylcd_clear_display();
                draw_message_string("No audio playing", false);
                mylcd_update();
                timeout = HZ/5;
            }
            else
            {
                if(showing_warning)
                {
                    showing_warning = false;
                    mylcd_clear_display();
                    mylcd_update();
                }

                timeout = HZ/100; /* 'till end of curent tick, don't use 100% CPU */
            }

            /* Make sure the FFT has produced something before doing anything
             * but watching for buttons. Music might not be playing or things
             * just aren't going well for picking up buffers so keys are
             * scanned to avoid lockup.  */
             button = rb->button_get_w_tmo(timeout);
             if (button != BUTTON_NONE)
                 goto read_button;
		}

	    draw(NULL);

        fft_free_fft_output(); /* COP only */

        long tick = *rb->current_tick;
        if(TIME_BEFORE(tick, next_frame_tick))
        {
            tick = next_frame_tick - tick;
        }
        else
        {
            rb->yield(); /* tmo = 0 won't yield */
            tick = 0;
        }

		button = rb->button_get_w_tmo(tick);
    read_button:
        switch (button)
        {
            case FFT_QUIT:
                run = false;
                break;
            case FFT_PREV_GRAPH: {
                if (graph_settings.mode-- <= FFT_DM_FIRST)
                    graph_settings.mode = FFT_DM_COUNT-1;
                graph_settings.changed.mode = true;
                draw(modes_text[graph_settings.mode]);
                break;
            }
            case FFT_NEXT_GRAPH: {
                if (++graph_settings.mode >= FFT_DM_COUNT)
                    graph_settings.mode = FFT_DM_FIRST;
                graph_settings.changed.mode = true;
                draw(modes_text[graph_settings.mode]);
                break;
            }
            case FFT_WINDOW: {
                if(++graph_settings.window_func >= FFT_WF_COUNT)
                    graph_settings.window_func = FFT_WF_FIRST;
                graph_settings.changed.window_func = true;
                draw(window_text[graph_settings.window_func]);
                break;
            }
            case FFT_AMP_SCALE: {
                graph_settings.logarithmic_amp = !graph_settings.logarithmic_amp;
                graph_settings.changed.amp_scale = true;
                draw(amp_scales_text[graph_settings.logarithmic_amp ? 1 : 0]);
                break;
            }
#ifdef FFT_FREQ_SCALE /* 'Till all keymaps are defined */
            case FFT_FREQ_SCALE: {
                graph_settings.logarithmic_freq = !graph_settings.logarithmic_freq;
                graph_settings.changed.freq_scale = true;
                draw(freq_scales_text[graph_settings.logarithmic_freq ? 1 : 0]);
                break;
            }
#endif
            case FFT_ORIENTATION: {
                graph_settings.orientation_vertical =
                    !graph_settings.orientation_vertical;
                graph_settings.changed.orientation = true;
                draw(NULL);
                break;
            }
            default: {
                if (rb->default_event_handler(button) == SYS_USB_CONNECTED)
                    return PLUGIN_USB_CONNECTED;
            }

        }
    }

    fft_close_fft();	

#ifdef HAVE_ADJUSTABLE_CPU_FREQ
    rb->cpu_boost(false);
#endif
#ifndef HAVE_LCD_COLOR
    grey_release();
#endif
    backlight_use_settings();
    return PLUGIN_OK;
    (void)parameter;
}
