void vis::EllipseTransformer::execute_stereo(pcm_stereo_sample *buffer,
                                             vis::NcursesWriter *writer)
{
    const auto win_height = NcursesUtils::get_window_height();
    const auto win_width = NcursesUtils::get_window_width();

    const auto left_half_width = win_width / 2;
    const auto right_half_width = win_width - left_half_width;

    const auto top_half_height = win_height / 2;
    const auto bottom_half_height = win_height - top_half_height;

    const auto max_color_index = static_cast<size_t>(std::floor(
        std::sqrt(win_width * win_width + 4 * win_height * win_height)));

    recalculate_colors(max_color_index, m_settings->get_colors(),
                       &m_precomputed_colors, writer);

    writer->clear();

    const auto overridden_scaling_multiplier =
        m_settings->get_scaling_multiplier();

    double x;
    double y;
    std::wstring msg{m_settings->get_ellipse_character()};
    for (auto i = 0ul; i < m_settings->get_sample_size(); ++i)
    {
        x = overridden_scaling_multiplier *
            static_cast<double>(static_cast<double>(buffer[i].l) / 32768.0) *
            (buffer[i].l < 0 ? left_half_width : right_half_width);

        y = overridden_scaling_multiplier *
            static_cast<double>(static_cast<double>(buffer[i].r) / 32768.0) *
            (buffer[i].r < 0 ? top_half_height : bottom_half_height);

        // The arguments to the to_color function roughly follow a circle
        // equation where the center is not centered around (0,0). For example
        // (x - w)^2 + (y-h)+2 = r^2 centers the circle around the point (w,h).
        // Because fonts are not all the same size, this will not always
        // generate a perfect circle, hence the name "ellipse".
        const auto color_index =
            static_cast<uint64_t>(std::floor(std::sqrt(x * x + 4 * y * y))) %
            max_color_index;

        writer->write(top_half_height + static_cast<int32_t>(y),
                      left_half_width + static_cast<int32_t>(x),
                      m_precomputed_colors[color_index], msg,
                      m_settings->get_ellipse_character());
    }

    writer->flush();
}
void vis::SpectrumTransformer::draw_bars(
    const std::vector<double> &bars, const std::vector<double> &bars_falloff,
    int32_t win_height, const bool flipped, const std::wstring &bar_row_msg,
    vis::NcursesWriter *writer)
{
    recalculate_colors(static_cast<size_t>(win_height), m_precomputed_colors,
                       writer);

    const auto full_win_width = NcursesUtils::get_window_width();
    const auto full_win_height = NcursesUtils::get_window_height();
    auto top_margin = static_cast<int32_t>(
        m_settings->get_spectrum_top_margin() * full_win_height);
    auto bottom_margin = static_cast<int32_t>(
        m_settings->get_spectrum_bottom_margin() * full_win_height);
    auto left_margin = static_cast<int32_t>(
        m_settings->get_spectrum_left_margin() * full_win_width);

    for (auto original_column_index = 0u; original_column_index < bars.size();
         ++original_column_index)
    {
        auto column_index = original_column_index;
        if (m_settings->is_spectrum_reversed())
        {
            column_index =
                static_cast<uint32_t>(bars.size()) - original_column_index - 1;
        }

        auto bar_height = 0.0;

        switch (m_settings->get_spectrum_falloff_mode())
        {
        case vis::FalloffMode::None:
            bar_height = bars[column_index];
            break;
        case vis::FalloffMode::Fill:
            bar_height = bars_falloff[column_index];
            break;
        case vis::FalloffMode::Top:
            bar_height = bars[column_index];
            break;
        }

        bar_height = std::max(0.0, bar_height);

        for (auto row_index = 0; row_index <= static_cast<int32_t>(bar_height);
             ++row_index)
        {
            int32_t row_height;

            // left channel grows up, right channel grows down
            if (flipped)
            {
                row_height = win_height - row_index - 1;
            }
            else
            {
                row_height = win_height + row_index - 1;
            }

            auto column =
                static_cast<int32_t>(original_column_index) *
                static_cast<int32_t>((bar_row_msg.size() +
                                      m_settings->get_spectrum_bar_spacing()));

            writer->write(row_height + top_margin - bottom_margin,
                          column + left_margin,
                          m_precomputed_colors[static_cast<size_t>(row_index)],
                          bar_row_msg, m_settings->get_spectrum_character());
        }

        if (m_settings->get_spectrum_falloff_mode() == vis::FalloffMode::Top)
        {
            int32_t row_index =
                static_cast<int32_t>(bars_falloff[column_index]);
            int32_t top_row_height;

            // left channel grows up, right channel grows down
            if (flipped)
            {
                top_row_height = win_height - row_index - 1;
            }
            else
            {
                top_row_height = win_height + row_index - 1;
            }

            if (row_index > 0)
            {
                auto column = static_cast<int32_t>(original_column_index) *
                              static_cast<int32_t>(
                                  (bar_row_msg.size() +
                                   m_settings->get_spectrum_bar_spacing()));

                writer->write(
                    top_row_height + top_margin - bottom_margin,
                    column + left_margin,
                    m_precomputed_colors[static_cast<size_t>(row_index)],
                    bar_row_msg, m_settings->get_spectrum_character());
            }
        }
    }
}
void vis::LorenzTransformer::execute_stereo(pcm_stereo_sample *buffer,
                                            vis::NcursesWriter *writer)
{
    const auto win_height = NcursesUtils::get_window_height();
    const auto half_height = win_height / 2;
    const auto win_width = NcursesUtils::get_window_width();

    const auto samples = m_settings->get_sample_size();

    m_rotation_count_left = m_rotation_count_left >= k_max_rotation_count
                                ? 0
                                : m_rotation_count_left;
    m_rotation_count_right = m_rotation_count_right >= k_max_rotation_count
                                 ? 0
                                 : m_rotation_count_right;

    auto average_left = 0.0;
    auto average_right = 0.0;
    for (auto i = 0u; i < samples; ++i)
    {
        average_left += std::abs(buffer[i].l);
        average_right += std::abs(buffer[i].r);
    }

    average_left = average_left / samples * 2.0;
    average_right = average_right / samples;

    const auto rotation_interval_left =
        (average_left * (m_settings->get_fps() / 65536.0));
    const auto rotation_interval_right =
        (average_right * (m_settings->get_fps() / 65536.0));

    const auto overridden_scaling_multiplier =
        m_settings->get_scaling_multiplier();
    const auto average =
        overridden_scaling_multiplier * (average_left + average_right) / 2.0;

    // lorenz_b will range from 11.7 to 64.4. Below 10 the lorenz is pretty much
    // just a disc and after 64.4 the size increases dramatically.
    // The equation was generated using linear curve fitting
    // http://www.wolframalpha.com/input/?i=quadratic+fit+%7B10%2C1%7D%2C%7B18000%2C32%7D%2C%7B45000%2C48%7D%2C%7B65536%2C64.4%7D
    const auto lorenz_b = k_lorenz_b1 + k_lorenz_b2 * average;

    // Calculate the center of the lorenz. Described here under the heading
    // Equilibria http://www.me.rochester.edu/courses/ME406/webexamp5/loreq.pdf
    const auto z_center = -1 + lorenz_b;
    const auto equilbria = std::sqrt(k_lorenz_c * lorenz_b - k_lorenz_c);

    // Calculate the scaling factor. The bounds for the complete lorenz are
    // given here http://www.me.rochester.edu/courses/ME406/webexamp5/loreq.pdf
    // Approximately the first 100 points of the lorenz are usually outliers
    // from the main body of the lorenz, so multiplying by 1.25
    // adjusts the bounds so that the main body takes up most of the height of
    // the screen.
    // Only consider max y coordinate since both max z and max x will be much
    // smaller than the max y.
    const auto scaling_multiplier =
        1.25 * (half_height) /
        std::sqrt((k_lorenz_c * lorenz_b * lorenz_b) -
                  (std::pow(z_center - lorenz_b, 2) / std::pow(lorenz_b, 2)));

    // Calculate the horizontal and vertical rotation angles. This is dependent
    // on the volume of the left and right channels.
    auto rotation_angle_x = (m_rotation_count_left * 2.0 * VisConstants::k_pi) /
                            k_max_rotation_count;
    auto rotation_angle_y =
        (m_rotation_count_right * 2.0 * VisConstants::k_pi) /
        k_max_rotation_count;

    auto deg_multiplier_cos_x = std::cos(rotation_angle_x);
    auto deg_multiplier_sin_x = std::sin(rotation_angle_x);

    auto deg_multiplier_cos_y = std::cos(rotation_angle_y);
    auto deg_multiplier_sin_y = std::sin(rotation_angle_y);

    auto x = 0.0;
    auto y = 0.0;
    auto z = 0.0;

    auto x0 = 0.1;
    auto y0 = 0.0;
    auto z0 = 0.0;

    writer->clear();

    // k_max_color_index_for_lorenz was chosen mostly through experimentation on
    // what seemed to work well.
    recalculate_colors(m_max_color_index, m_settings->get_colors(),
                       &m_precomputed_colors, writer);

    std::wstring msg{m_settings->get_lorenz_character()};
    for (auto i = 0u; i < samples; ++i)
    {
        auto x1 = x0 + k_lorenz_h * k_lorenz_a * (y0 - x0);
        auto y1 = y0 + k_lorenz_h * (x0 * (lorenz_b - z0) - y0);
        auto z1 = z0 + k_lorenz_h * (x0 * y0 - k_lorenz_c * z0);
        x0 = x1;
        y0 = y1;
        z0 = z1;

        // color points based on distance from equiliria. This must be done
        // before rotation
        auto distance_p1 =
            std::sqrt(std::pow(x0 - equilbria, 2) +
                      std::pow(y0 - equilbria, 2) + std::pow(z0 - z_center, 2));
        auto distance_p2 =
            std::sqrt(std::pow(x0 + equilbria, 2) +
                      std::pow(y0 + equilbria, 2) + std::pow(z0 - z_center, 2));
        auto color_distance =
            static_cast<size_t>(std::min(distance_p1, distance_p2));

        if (color_distance > m_max_color_index)
        {
            m_max_color_index =
                std::min(k_color_distance_limit, color_distance);
            recalculate_colors(m_max_color_index, m_settings->get_colors(),
                               &m_precomputed_colors, writer);
        }

        // We want to rotate around the center of the lorenz. so we offset zaxis
        // so that the center of the lorenz is at point (0,0,0)
        x = x0;
        y = y0;
        z = z0 - z_center;

        // Rotate around X and Y axis.
        auto xRxy = x * deg_multiplier_cos_y + z * deg_multiplier_sin_y;
        auto yRxy = x * deg_multiplier_sin_x * deg_multiplier_sin_y +
                    y * deg_multiplier_cos_x -
                    z * deg_multiplier_cos_y * deg_multiplier_sin_x;

        x = xRxy * scaling_multiplier;
        y = yRxy * scaling_multiplier;

        // Throw out any points outside the window
        if (y > (half_height * -1) && y < half_height &&
            x > ((static_cast<double>(win_width) / 2.0) * -1) &&
            x < (static_cast<double>(win_width) / 2.0))
        {
            // skip the first 100 since values under 100 stick out too much from
            // the reset of the points
            if (i > 100)
            {
                writer->write(static_cast<int32_t>(y + half_height),
                              static_cast<int32_t>(x + win_width / 2.0),
                              m_precomputed_colors[color_distance], msg,
                              m_settings->get_lorenz_character());
            }
        }
    }

    writer->flush();

    m_rotation_count_left += rotation_interval_left;
    m_rotation_count_right += rotation_interval_right;
}