/*
 * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
 * Copyright (C) 2016 Furrtek
 *
 * This file is part of PortaPack.
 *
 * 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, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street,
 * Boston, MA 02110-1301, USA.
 */

#include "ui_widget.hpp"
#include "ui_painter.hpp"
#include "portapack.hpp"

#include <cstdint>
#include <cstddef>
#include <algorithm>

#include "chprintf.h"
#include "irq_controls.hpp"
#include "string_format.hpp"
#include "usb_serial_device_to_host.h"
#include "rtc_time.hpp"

using namespace portapack;
using namespace rtc_time;

namespace ui {

static bool ui_dirty = true;

void dirty_set() {
    ui_dirty = true;
}

void dirty_clear() {
    ui_dirty = false;
}

bool is_dirty() {
    return ui_dirty;
}

/* Widget ****************************************************************/

const std::vector<Widget*> Widget::no_children{};

Point Widget::screen_pos() {
    return screen_rect().location();
}

Size Widget::size() const {
    return _parent_rect.size();
}

Rect Widget::screen_rect() const {
    return parent() ? (parent_rect() + parent()->screen_pos()) : parent_rect();
}

Rect Widget::parent_rect() const {
    return _parent_rect;
}

void Widget::set_parent_rect(const Rect new_parent_rect) {
    _parent_rect = new_parent_rect;
    set_dirty();
}

Widget* Widget::parent() const {
    return parent_;
}

void Widget::set_parent(Widget* const widget) {
    if (widget == parent_) {
        return;
    }

    if (parent_ && !widget) {
        // We have a parent, but are losing it. Update visible status.
        dirty_overlapping_children_in_rect(screen_rect());
        visible(false);
    }

    parent_ = widget;

    set_dirty();
}

void Widget::set_dirty() {
    flags.dirty = true;
    dirty_set();
}

bool Widget::dirty() const {
    return flags.dirty;
}

void Widget::set_clean() {
    flags.dirty = false;
}

void Widget::hidden(bool hide) {
    if (hide != flags.hidden) {
        flags.hidden = hide;

        // If parent is hidden, either of these is a no-op.
        if (hide) {
            // TODO: Instead of dirtying parent entirely, dirty only children
            // that overlap with this widget.

            // parent()->dirty_overlapping_children_in_rect(parent_rect());

            /* TODO: Notify self and all non-hidden children that they're
             * now effectively hidden?
             */
        } else {
            set_dirty();
            /* TODO: Notify self and all non-hidden children that they're
             * now effectively shown?
             */
        }
    }
}

void Widget::focus() {
    context().focus_manager().set_focus_widget(this);
}

void Widget::on_focus() {
}

void Widget::blur() {
    context().focus_manager().set_focus_widget(nullptr);
}

void Widget::on_blur() {
}

bool Widget::focusable() const {
    return flags.focusable;
}

void Widget::set_focusable(const bool value) {
    flags.focusable = value;
}

bool Widget::has_focus() {
    return (context().focus_manager().focus_widget() == this);
}

bool Widget::on_key(const KeyEvent event) {
    (void)event;
    return false;
}

bool Widget::on_encoder(const EncoderEvent event) {
    (void)event;
    return false;
}

bool Widget::on_touch(const TouchEvent event) {
    (void)event;
    return false;
}
bool Widget::on_keyboard(const KeyboardEvent event) {
    (void)event;
    return false;
}

const std::vector<Widget*>& Widget::children() const {
    return no_children;
}

Context& Widget::context() const {
    chDbgAssert(parent_, "parent_ is null",
                "Check that parent isn't null before deref.");
    return parent()->context();
}

void Widget::set_style(const Style* new_style) {
    if (new_style != style_) {
        style_ = new_style;
        set_dirty();
    }
}

const Style& Widget::style() const {
    return style_ ? *style_ : parent()->style();
}

void Widget::visible(bool v) {
    if (v != flags.visible) {
        flags.visible = v;

        /* TODO: This on_show/on_hide implementation seems inelegant.
         * But I need *some* way to take/configure resources when
         * a widget becomes visible, and reverse the process when the
         * widget becomes invisible, whether the widget (or parent) is
         * hidden, or the widget (or parent) is removed from the tree.
         */
        if (v) {
            on_show();
        } else {
            on_hide();

            // Set all children invisible too.
            for (const auto child : children()) {
                child->visible(false);
            }
        }
    }
}

bool Widget::highlighted() const {
    return flags.highlighted;
}

void Widget::set_highlighted(const bool value) {
    flags.highlighted = value;
}

void Widget::dirty_overlapping_children_in_rect(const Rect& child_rect) {
    for (auto child : children()) {
        if (!child_rect.intersect(child->parent_rect()).is_empty()) {
            child->set_dirty();
        }
    }
}

void Widget::getAccessibilityText(std::string& result) {
    result = "";
}
void Widget::getWidgetName(std::string& result) {
    result = "";
}
/* View ******************************************************************/

void View::paint(Painter& painter) {
    painter.fill_rectangle(
        screen_rect(),
        style().background);
}

void View::add_child(Widget* const widget) {
    if (widget) {
        if (widget->parent() == nullptr) {
            widget->set_parent(this);
            children_.push_back(widget);
        }
    }
}

void View::add_children(const std::initializer_list<Widget*> children) {
    children_.insert(std::end(children_), children);
    for (auto child : children) {
        child->set_parent(this);
    }
}

void View::remove_child(Widget* const widget) {
    if (widget) {
        children_.erase(std::remove(children_.begin(), children_.end(), widget), children_.end());
        widget->set_parent(nullptr);
    }
}

void View::remove_children(const std::vector<Widget*>& children) {
    for (auto child : children) {
        remove_child(child);
    }
}

const std::vector<Widget*>& View::children() const {
    return children_;
}

std::string View::title() const {
    return "";
};

/* OptionTabView *********************************************************/

OptionTabView::OptionTabView(Rect parent_rect) {
    set_parent_rect(parent_rect);

    add_child(&check_enable);
    hidden(true);

    check_enable.on_select = [this](Checkbox&, bool value) {
        enabled = value;
    };
}

void OptionTabView::set_enabled(bool value) {
    check_enable.set_value(value);
}

bool OptionTabView::is_enabled() {
    return check_enable.value();
}

void OptionTabView::set_type(std::string type) {
    check_enable.set_text("Transmit " + type);
}

void OptionTabView::focus() {
    check_enable.focus();
}

/* Rectangle *************************************************************/

Rectangle::Rectangle(
    Color c)
    : Widget{},
      color{c} {
}

Rectangle::Rectangle(
    Rect parent_rect,
    Color c)
    : Widget{parent_rect},
      color{c} {
}

void Rectangle::set_color(const Color c) {
    color = c;
    set_dirty();
}

void Rectangle::set_outline(const bool outline) {
    _outline = outline;
    set_dirty();
}

void Rectangle::paint(Painter& painter) {
    if (!_outline) {
        painter.fill_rectangle(
            screen_rect(),
            color);
    } else {
        painter.draw_rectangle(
            screen_rect(),
            color);
    }
}

/* Text ******************************************************************/

Text::Text(
    Rect parent_rect,
    std::string text)
    : Widget{parent_rect},
      text{std::move(text)} {
}

Text::Text(
    Rect parent_rect)
    : Text{parent_rect, {}} {
}

void Text::set(std::string_view value) {
    text = std::string{value};
    set_dirty();
}

void Text::getAccessibilityText(std::string& result) {
    result = text;
}
void Text::getWidgetName(std::string& result) {
    result = "Text";
}
void Text::paint(Painter& painter) {
    const auto rect = screen_rect();
    auto s = has_focus() ? style().invert() : style();
    auto max_len = (unsigned)rect.width() / s.font.char_width();
    auto text_view = std::string_view{text};

    painter.fill_rectangle(rect, s.background);

    if (text_view.length() > max_len)
        text_view = text_view.substr(0, max_len);

    painter.draw_string(
        rect.location(),
        s,
        text_view);
}

/* Labels ****************************************************************/

Labels::Labels(
    std::initializer_list<Label> labels)
    : labels_{labels} {
}

void Labels::set_labels(std::initializer_list<Label> labels) {
    labels_ = labels;
    set_dirty();
}

void Labels::paint(Painter& painter) {
    for (auto& label : labels_) {
        painter.draw_string(
            label.pos + screen_pos(),
            style().font,
            label.color,
            style().background,
            label.text);
    }
}

void Labels::getAccessibilityText(std::string& result) {
    result = "";
    for (auto& label : labels_) {
        result += label.text;
        result += ", ";
    }
}
void Labels::getWidgetName(std::string& result) {
    result = "Labels";
}

/* LiveDateTime **********************************************************/

void LiveDateTime::on_tick_second() {
    rtc_time::now(datetime);
    text = "";
    if (!hide_clock) {
        if (date_enabled) {
            text = to_string_dec_uint(datetime.year(), 4, '0') + "-" +
                   to_string_dec_uint(datetime.month(), 2, '0') + "-" +
                   to_string_dec_uint(datetime.day(), 2, '0') + " ";
        } else {
            text = "           ";
        }

        text = text + to_string_dec_uint(datetime.hour(), 2, '0') + ":" + to_string_dec_uint(datetime.minute(), 2, '0');

        if (seconds_enabled) {
            text += ":";

            if (init_delay == 0)
                text += to_string_dec_uint(datetime.second(), 2, '0');
            else {
                // Placeholder while the seconds are not updated
                text += "XX";
                init_delay--;
            }
        }
    }
    set_dirty();
}

LiveDateTime::LiveDateTime(
    Rect parent_rect)
    : Widget{parent_rect} {
    signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
        this->on_tick_second();
    };
}

LiveDateTime::~LiveDateTime() {
    rtc_time::signal_tick_second -= signal_token_tick_second;
}

void LiveDateTime::paint(Painter& painter) {
    const auto rect = screen_rect();
    const auto s = style();

    on_tick_second();

    painter.fill_rectangle(rect, s.background);

    painter.draw_string(
        rect.location(),
        s,
        text);
}
void LiveDateTime::set_hide_clock(bool new_value) {
    this->hide_clock = new_value;
}

void LiveDateTime::set_date_enabled(bool new_value) {
    this->date_enabled = new_value;
}

void LiveDateTime::set_seconds_enabled(bool new_value) {
    this->seconds_enabled = new_value;
}

/* BigFrequency **********************************************************/

BigFrequency::BigFrequency(
    Rect parent_rect,
    rf::Frequency frequency)
    : Widget{parent_rect},
      _frequency{frequency} {
}

void BigFrequency::set(const rf::Frequency frequency) {
    _frequency = frequency;
    set_dirty();
}

void BigFrequency::paint(Painter& painter) {
    uint32_t i, digit_def;
    std::array<char, 7> digits;
    char digit;
    Point digit_pos;
    ui::Color segment_color;

    const auto rect = screen_rect();

    // Erase
    painter.fill_rectangle(
        {{0, rect.location().y()}, {screen_width, 52}},
        ui::Color::black());

    // Prepare digits
    if (!_frequency) {
        digits.fill(10);  // ----.---
        digit_pos = {0, rect.location().y()};
    } else {
        _frequency /= 1000;  // GMMM.KKK(uuu)

        for (i = 0; i < 7; i++) {
            digits[6 - i] = _frequency % 10;
            _frequency /= 10;
        }

        // Remove leading zeros
        for (i = 0; i < 3; i++) {
            if (!digits[i])
                digits[i] = 16;  // "Don't draw" code
            else
                break;
        }

        digit_pos = {(Coord)(240 - ((7 * digit_width) + 8) - (i * digit_width)) / 2, rect.location().y()};
    }

    segment_color = style().foreground;

    // Draw
    for (i = 0; i < 7; i++) {
        digit = digits[i];

        if (digit < 16) {
            digit_def = segment_font[(uint8_t)digit];

            for (size_t s = 0; s < 7; s++) {
                if (digit_def & 1)
                    painter.fill_rectangle({digit_pos + segments[s].location(), segments[s].size()}, segment_color);
                digit_def >>= 1;
            }
        }

        if (i == 3) {
            // Dot
            painter.fill_rectangle({digit_pos + Point(34, 48), {4, 4}}, segment_color);
            digit_pos += {(digit_width + 8), 0};
        } else {
            digit_pos += {digit_width, 0};
        }
    }
}

/* ProgressBar ***********************************************************/

ProgressBar::ProgressBar(
    Rect parent_rect)
    : Widget{parent_rect} {
}

void ProgressBar::set_max(const uint32_t max) {
    if (max == _max) return;

    if (_value > _max)
        _value = _max;

    _max = max;
    set_dirty();
}

void ProgressBar::set_value(const uint32_t value) {
    if (value == _value) return;

    if (value > _max)
        _value = _max;
    else
        _value = value;
    set_dirty();
}

void ProgressBar::getAccessibilityText(std::string& result) {
    result = to_string_dec_uint(_value) + " / " + to_string_dec_uint(_max);
}
void ProgressBar::getWidgetName(std::string& result) {
    result = "ProgressBar";
}

void ProgressBar::paint(Painter& painter) {
    int v_scaled;

    const auto sr = screen_rect();
    const auto s = style();

    v_scaled = (sr.size().width() * (uint64_t)_value) / _max;

    painter.fill_rectangle({sr.location(), {v_scaled, sr.size().height()}}, style().foreground);
    painter.fill_rectangle({{sr.location().x() + v_scaled, sr.location().y()}, {sr.size().width() - v_scaled, sr.size().height()}}, s.background);

    painter.draw_rectangle(sr, s.foreground);
}

/* ActivityDot ***********************************************************/

ActivityDot::ActivityDot(
    Rect parent_rect,
    Color color)
    : Widget{parent_rect},
      _color{color} {}

void ActivityDot::paint(Painter& painter) {
    painter.fill_rectangle(screen_rect(), _on ? _color : Color::grey());
}

void ActivityDot::toggle() {
    _on = !_on;
    set_dirty();
}

void ActivityDot::reset() {
    _on = false;
    set_dirty();
}

/* Console ***************************************************************/

Console::Console(
    Rect parent_rect)
    : Widget{parent_rect} {
}

void Console::clear(bool clear_buffer = false) {
    if (clear_buffer)
        buffer.clear();

    if (!hidden() && visible()) {
        display.fill_rectangle(
            screen_rect(),
            Color::black());
    }

    pos = {0, 0};
}

void Console::write(std::string message) {
    bool escape = false;

    if (!hidden() && visible()) {
        const Style& s = style();
        const Font& font = s.font;
        auto rect = screen_rect();
        ui::Color pen_color = s.foreground;

        for (auto c : message) {
            if (escape) {
                if (c < std::size(term_colors))
                    pen_color = term_colors[(uint8_t)c];
                else
                    pen_color = s.foreground;
                escape = false;
            } else {
                if (c == '\n') {
                    crlf();
                } else if (c == '\r') {
                    pos = {0, pos.y()};
                } else if (c == '\x1B') {
                    escape = true;
                } else {
                    auto glyph = font.glyph(c);
                    auto advance = glyph.advance();
                    // Would drawing next character be off the end? Newline.
                    if ((pos.x() + advance.x()) > rect.width())
                        crlf();

                    Point pos_glyph{
                        rect.left() + pos.x(),
                        display.scroll_area_y(pos.y())};
                    display.draw_glyph(pos_glyph, glyph, pen_color, s.background);
                    pos += {advance.x(), 0};
                }
            }
        }
        buffer = message;
    } else {
        if (buffer.size() < 256) buffer += message;
    }
}

void Console::getAccessibilityText(std::string& result) {
    result = "{" + buffer + "}";
}
void Console::getWidgetName(std::string& result) {
    result = "Console";
}

void Console::writeln(std::string message) {
    write(message + "\n");
}

void Console::paint(Painter&) {
    write(buffer);
}

void Console::on_show() {
    enable_scrolling(true);
    clear();
}

bool Console::scrolling_enabled = false;

void Console::enable_scrolling(bool enable) {
    if (enable) {
        auto sr = screen_rect();
        auto line_height = style().font.line_height();

        // Count full lines that can fit in console's rectangle.
        auto max_lines = sr.height() / line_height;  // NB: int division to floor.

        // The scroll area must be a multiple of the line_height
        // or some lines will end up vertically truncated.
        scroll_height = max_lines * line_height;

        display.scroll_set_area(sr.top(), sr.top() + scroll_height);
        display.scroll_set_position(0);
        scrolling_enabled = true;
    } else {
        display.scroll_disable();
        scrolling_enabled = false;
    }
}

void Console::on_hide() {
    /* TODO: Clear region to eliminate brief flash of content at un-shifted
     * position? */
    enable_scrolling(false);
}

void Console::crlf() {
    if (hidden() || !visible()) return;

    const auto& s = style();
    auto sr = screen_rect();
    auto line_height = s.font.line_height();

    // Advance to the next line (\n) position and "carriage return" x to 0.
    pos = {0, pos.y() + line_height};

    if (pos.y() >= scroll_height) {
        // Line is past off the "bottom", need to scroll.
        if (!scrolling_enabled)
            enable_scrolling(true);

        // See the notes in lcd_ili9341.hpp about how scrolling works.
        // The gist is that VSA will be moved to scroll the "top" off the
        // screen. The drawing code uses 'scroll_area_y' to get the actual
        // screen coordinate based on VSA. The "bottom" line is *always*
        // at 'VSA + ((max_lines - 1) * line_height)' and so is constant.
        pos = {0, scroll_height - line_height};

        // Scroll off the "top" line.
        display.scroll(-line_height);

        // Clear the new line at the "bottom".
        Rect dirty{sr.left(), display.scroll_area_y(pos.y()), sr.width(), line_height};
        display.fill_rectangle(dirty, s.background);
    }
}

/* Checkbox **************************************************************/

Checkbox::Checkbox(
    Point parent_pos,
    size_t length,
    std::string text,
    bool small)
    : Widget{},
      text_{text},
      small_{small} {
    if (!small_)
        set_parent_rect({parent_pos, {static_cast<ui::Dim>((8 * length) + 24), 24}});
    else
        set_parent_rect({parent_pos, {static_cast<ui::Dim>((8 * length) + 16), 16}});

    set_focusable(true);
}

void Checkbox::set_text(const std::string value) {
    text_ = value;
    set_dirty();
}

bool Checkbox::set_value(const bool value) {
    value_ = value;
    set_dirty();

    if (on_select) {
        on_select(*this, value_);
        return true;
    }

    return false;
}

void Checkbox::getAccessibilityText(std::string& result) {
    result = text_ + ((value_) ? " checked" : " unchecked");
}
void Checkbox::getWidgetName(std::string& result) {
    result = "Checkbox";
}

bool Checkbox::value() const {
    return value_;
}

void Checkbox::paint(Painter& painter) {
    const auto r = screen_rect();

    const auto paint_style = (has_focus() || highlighted()) ? style().invert() : style();

    const auto x = r.location().x();
    const auto y = r.location().y();

    const auto label_r = paint_style.font.size_of(text_);

    if (!small_) {
        painter.draw_rectangle({{r.location()}, {24, 24}}, style().foreground);

        painter.fill_rectangle({x + 1, y + 1, 24 - 2, 24 - 2}, style().background);

        // Highlight
        painter.draw_rectangle({x + 2, y + 2, 24 - 4, 24 - 4}, paint_style.background);

        if (value_ == true) {
            // Check
            portapack::display.draw_line({x + 2, y + 14}, {x + 6, y + 18}, ui::Color::green());
            portapack::display.draw_line({x + 6, y + 18}, {x + 20, y + 4}, ui::Color::green());
        } else {
            // Cross
            portapack::display.draw_line({x + 1, y + 1}, {x + 24 - 2, y + 24 - 2}, ui::Color::red());
            portapack::display.draw_line({x + 24 - 2, y + 1}, {x + 1, y + 24 - 2}, ui::Color::red());
        }

        painter.draw_string(
            {static_cast<Coord>(x + 24 + 4),
             static_cast<Coord>(y + (24 - label_r.height()) / 2)},
            paint_style,
            text_);
    } else {
        painter.draw_rectangle({{r.location()}, {16, 16}}, style().foreground);

        painter.fill_rectangle({x + 1, y + 1, 16 - 2, 16 - 2}, style().background);

        // Highlight
        painter.draw_rectangle({x + 1, y + 1, 16 - 2, 16 - 2}, paint_style.background);

        if (value_ == true) {
            // Check
            portapack::display.draw_line({x + 2, y + 8}, {x + 6, y + 12}, ui::Color::green());
            portapack::display.draw_line({x + 6, y + 12}, {x + 13, y + 5}, ui::Color::green());
        } else {
            // Cross
            portapack::display.draw_line({x + 1, y + 1}, {x + 16 - 2, y + 16 - 2}, ui::Color::red());
            portapack::display.draw_line({x + 16 - 2, y + 1}, {x + 1, y + 16 - 2}, ui::Color::red());
        }

        painter.draw_string(
            {static_cast<Coord>(x + 16 + 2),
             static_cast<Coord>(y + (16 - label_r.height()) / 2)},
            paint_style,
            text_);
    }
}

bool Checkbox::on_key(const KeyEvent key) {
    if (key == KeyEvent::Select)
        return set_value(not value_);

    return false;
}

bool Checkbox::on_keyboard(const KeyboardEvent event) {
    if (event == 10 || event == 32) return set_value(not value_);
    return false;
}

bool Checkbox::on_touch(const TouchEvent event) {
    switch (event.type) {
        case TouchEvent::Type::Start:
            set_highlighted(true);
            set_dirty();
            return true;

        case TouchEvent::Type::End:
            set_highlighted(false);
            value_ = not value_;
            set_dirty();
            if (on_select) {
                on_select(*this, value_);
            }
            return true;

        default:
            return false;
    }
}

/* Button ****************************************************************/

Button::Button(
    Rect parent_rect,
    std::string text,
    bool instant_exec)
    : Widget{parent_rect},
      text_{text},
      instant_exec_{instant_exec} {
    set_focusable(true);
}

void Button::set_text(const std::string value) {
    text_ = value;
    set_dirty();
}

std::string Button::text() const {
    return text_;
}

void Button::getAccessibilityText(std::string& result) {
    result = text_;
}
void Button::getWidgetName(std::string& result) {
    result = "Button";
}

void Button::paint(Painter& painter) {
    Color bg, fg;
    const auto r = screen_rect();

    if (has_focus() || highlighted()) {
        bg = style().foreground;
        fg = Color::black();
    } else {
        bg = Color::grey();
        fg = style().foreground;
    }

    const Style paint_style = {style().font, bg, fg};

    painter.draw_rectangle({r.location(), {r.size().width(), 1}}, Color::light_grey());
    painter.draw_rectangle({r.location().x(), r.location().y() + r.size().height() - 1, r.size().width(), 1}, Color::dark_grey());
    painter.draw_rectangle({r.location().x() + r.size().width() - 1, r.location().y(), 1, r.size().height()}, Color::dark_grey());

    painter.fill_rectangle(
        {r.location().x(), r.location().y() + 1, r.size().width() - 1, r.size().height() - 2},
        paint_style.background);

    const auto label_r = paint_style.font.size_of(text_);
    painter.draw_string(
        {r.location().x() + (r.size().width() - label_r.width()) / 2, r.location().y() + (r.size().height() - label_r.height()) / 2},
        paint_style,
        text_);
}

void Button::on_focus() {
    if (on_highlight)
        on_highlight(*this);
}

bool Button::on_key(const KeyEvent key) {
    if (key == KeyEvent::Select) {
        if (on_select) {
            on_select(*this);
            return true;
        }
    } else {
        if (on_dir) {
            return on_dir(*this, key);
        }
    }

    return false;
}

bool Button::on_keyboard(const KeyboardEvent event) {
    if (event == 10 || event == 32) {
        if (on_select) {
            on_select(*this);
            return true;
        }
    }
    return false;
}

bool Button::on_touch(const TouchEvent event) {
    switch (event.type) {
        case TouchEvent::Type::Start:
            set_highlighted(true);
            set_dirty();
            if (on_touch_press) {
                on_touch_press(*this);
            }
            if (on_select && instant_exec_) {
                on_select(*this);
            }
            return true;

        case TouchEvent::Type::End:
            set_highlighted(false);
            set_dirty();
            if (on_touch_release) {
                on_touch_release(*this);
            }
            if (on_select && !instant_exec_) {
                on_select(*this);
            }
            return true;

        default:
            return false;
    }
#if 0
	switch(event.type) {
	case TouchEvent::Type::Start:
		flags.highlighted = true;
		set_dirty();
		return true;

	case TouchEvent::Type::Move:
		{
			const bool new_highlighted = screen_rect().contains(event.point);
			if( flags.highlighted != new_highlighted ) {
				flags.highlighted = new_highlighted;
				set_dirty();
			}
		}
		return true;

	case TouchEvent::Type::End:
		if( flags.highlighted ) {
			flags.highlighted = false;
			set_dirty();
			if( on_select ) {
				on_select(*this);
			}
		}
		return true;

	default:
		return false;
	}
#endif
}

/* ButtonWithEncoder ****************************************************************/

ButtonWithEncoder::ButtonWithEncoder(
    Rect parent_rect,
    std::string text,
    bool instant_exec)
    : Widget{parent_rect},
      text_{text},
      instant_exec_{instant_exec} {
    set_focusable(true);
}

void ButtonWithEncoder::set_text(const std::string value) {
    text_ = value;
    set_dirty();
}
int32_t ButtonWithEncoder::get_encoder_delta() {
    return encoder_delta;
}
void ButtonWithEncoder::set_encoder_delta(const int32_t delta) {
    encoder_delta = delta;
}

std::string ButtonWithEncoder::text() const {
    return text_;
}

void ButtonWithEncoder::getAccessibilityText(std::string& result) {
    result = text_;
}
void ButtonWithEncoder::getWidgetName(std::string& result) {
    result = "ButtonWithEncoder";
}

void ButtonWithEncoder::paint(Painter& painter) {
    Color bg, fg;
    const auto r = screen_rect();

    if (has_focus() || highlighted()) {
        bg = style().foreground;
        fg = Color::black();
    } else {
        bg = Color::grey();
        fg = style().foreground;
    }

    const Style paint_style = {style().font, bg, fg};

    painter.draw_rectangle({r.location(), {r.size().width(), 1}}, Color::light_grey());
    painter.draw_rectangle({r.location().x(), r.location().y() + r.size().height() - 1, r.size().width(), 1}, Color::dark_grey());
    painter.draw_rectangle({r.location().x() + r.size().width() - 1, r.location().y(), 1, r.size().height()}, Color::dark_grey());

    painter.fill_rectangle(
        {r.location().x(), r.location().y() + 1, r.size().width() - 1, r.size().height() - 2},
        paint_style.background);

    const auto label_r = paint_style.font.size_of(text_);
    painter.draw_string(
        {r.location().x() + (r.size().width() - label_r.width()) / 2, r.location().y() + (r.size().height() - label_r.height()) / 2},
        paint_style,
        text_);
}

void ButtonWithEncoder::on_focus() {
    if (on_highlight)
        on_highlight(*this);
}

bool ButtonWithEncoder::on_key(const KeyEvent key) {
    if (key == KeyEvent::Select) {
        if (on_select) {
            on_select(*this);
            return true;
        }
    } else {
        if (on_dir) {
            return on_dir(*this, key);
        }
    }

    return false;
}

bool ButtonWithEncoder::on_keyboard(const KeyboardEvent key) {
    if (key == 32 || key == 10) {
        if (on_select) {
            on_select(*this);
            return true;
        }
    }
    return false;
}

bool ButtonWithEncoder::on_touch(const TouchEvent event) {
    switch (event.type) {
        case TouchEvent::Type::Start:
            set_highlighted(true);
            set_dirty();
            if (on_touch_press) {
                on_touch_press(*this);
            }
            if (on_select && instant_exec_) {
                on_select(*this);
            }
            return true;

        case TouchEvent::Type::End:
            set_highlighted(false);
            set_dirty();
            if (on_touch_release) {
                on_touch_release(*this);
            }
            if (on_select && !instant_exec_) {
                on_select(*this);
            }
            return true;

        default:
            return false;
    }
#if 0
	switch(event.type) {
	case TouchEvent::Type::Start:
		flags.highlighted = true;
		set_dirty();
		return true;

	case TouchEvent::Type::Move:
		{
			const bool new_highlighted = screen_rect().contains(event.point);
			if( flags.highlighted != new_highlighted ) {
				flags.highlighted = new_highlighted;
				set_dirty();
			}
		}
		return true;

	case TouchEvent::Type::End:
		if( flags.highlighted ) {
			flags.highlighted = false;
			set_dirty();
			if( on_select ) {
				on_select(*this);
			}
		}
		return true;

	default:
		return false;
	}
#endif
}
bool ButtonWithEncoder::on_encoder(const EncoderEvent delta) {
    if (delta != 0) {
        encoder_delta += delta;
        delta_change = true;
        on_change();
    } else
        delta_change = 0;
    return true;
}

/* NewButton ****************************************************************/

NewButton::NewButton(
    Rect parent_rect,
    std::string text,
    const Bitmap* bitmap)
    : NewButton{parent_rect, text, bitmap, Color::dark_cyan()} {}

NewButton::NewButton(
    Rect parent_rect,
    std::string text,
    const Bitmap* bitmap,
    Color color,
    bool vertical_center)
    : Widget{parent_rect},
      color_{color},
      text_{text},
      bitmap_{bitmap},
      vertical_center_{vertical_center} {
    set_focusable(true);
}

void NewButton::set_text(const std::string value) {
    text_ = value;
    set_dirty();
}

void NewButton::getAccessibilityText(std::string& result) {
    result = text_;
}
void NewButton::getWidgetName(std::string& result) {
    result = "NewButton";
}

std::string NewButton::text() const {
    return text_;
}

void NewButton::set_bitmap(const Bitmap* bitmap) {
    bitmap_ = bitmap;
    set_dirty();
}

const Bitmap* NewButton::bitmap() {
    return bitmap_;
}

void NewButton::set_color(Color color) {
    color_ = color;
    set_dirty();
}

void NewButton::set_bg_color(Color color) {
    bg_color_ = color;
    set_dirty();
}

void NewButton::set_vertical_center(bool value) {
    vertical_center_ = value;
    set_dirty();
}

ui::Color NewButton::color() {
    return color_;
}

void NewButton::paint(Painter& painter) {
    if (!bitmap_ && text_.empty())
        return;

    const auto r = screen_rect();
    const Style style = paint_style();

    painter.draw_rectangle({r.location(), {r.width(), 1}}, Color::light_grey());
    painter.draw_rectangle({r.left(), r.top() + r.height() - 1, r.width(), 1}, Color::dark_grey());
    painter.draw_rectangle({r.left() + r.width() - 1, r.top(), 1, r.height()}, Color::dark_grey());

    painter.fill_rectangle(
        {r.left(), r.top() + 1, r.width() - 1, r.height() - 2},
        style.background);

    int y = r.top();
    if (bitmap_) {
        int offset_y = vertical_center_ ? (r.height() / 2) - (bitmap_->size.height() / 2) : 6;
        Point bmp_pos = {r.left() + (r.width() / 2) - (bitmap_->size.width() / 2), r.top() + offset_y};
        y += bitmap_->size.height() - offset_y;

        painter.draw_bitmap(
            bmp_pos,
            *bitmap_,
            color_,
            style.background);
    }

    if (!text_.empty()) {
        const auto label_r = style.font.size_of(text_);
        painter.draw_string(
            {r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2},
            style,
            text_);
    }
}

Style NewButton::paint_style() {
    MutableStyle s{style()};

    if (has_focus() || highlighted()) {
        s.background = style().foreground;
        s.foreground = Color::black();
    } else {
        s.background = bg_color_;
        s.foreground = style().foreground;
    }

    return s;
}

void NewButton::on_focus() {
    if (on_highlight)
        on_highlight(*this);
}

bool NewButton::on_key(const KeyEvent key) {
    if (key == KeyEvent::Select) {
        if (on_select) {
            on_select();
            return true;
        }
    } else {
        if (on_dir) {
            return on_dir(*this, key);
        }
    }

    return false;
}

bool NewButton::on_keyboard(const KeyboardEvent key) {
    if (key == 32 || key == 10) {
        if (on_select) {
            on_select();
            return true;
        }
    }
    return false;
}

bool NewButton::on_touch(const TouchEvent event) {
    switch (event.type) {
        case TouchEvent::Type::Start:
            set_highlighted(true);
            set_dirty();
            return true;

        case TouchEvent::Type::End:
            set_highlighted(false);
            set_dirty();
            if (on_select) {
                on_select();
            }
            return true;

        default:
            return false;
    }
}

/* Image *****************************************************************/

Image::Image()
    : Image{{}, nullptr, Color::white(), Color::black()} {
}

Image::Image(
    const Rect parent_rect,
    const Bitmap* bitmap,
    const Color foreground,
    const Color background)
    : Widget{parent_rect},
      bitmap_{bitmap},
      foreground_{foreground},
      background_{background} {
}

void Image::set_bitmap(const Bitmap* bitmap) {
    bitmap_ = bitmap;
    set_dirty();
}

void Image::set_foreground(const Color color) {
    foreground_ = color;
    set_dirty();
}

void Image::set_background(const Color color) {
    background_ = color;
    set_dirty();
}

void Image::invert_colors() {
    Color temp;

    temp = background_;
    background_ = foreground_;
    foreground_ = temp;
    set_dirty();
}

void Image::paint(Painter& painter) {
    if (bitmap_) {
        // Code also handles ImageButton behavior.
        const bool selected = (has_focus() || highlighted());
        painter.draw_bitmap(
            screen_pos(),
            *bitmap_,
            selected ? background_ : foreground_,
            selected ? foreground_ : background_);
    }
}

/* ImageButton ***********************************************************/

// TODO: Virtually all this code is duplicated from Button. Base class?

ImageButton::ImageButton(
    const Rect parent_rect,
    const Bitmap* bitmap,
    const Color foreground,
    const Color background)
    : Image{parent_rect, bitmap, foreground, background} {
    set_focusable(true);
}

void ImageButton::getAccessibilityText(std::string& result) {
    result = "image";
}
void ImageButton::getWidgetName(std::string& result) {
    result = "ImageButton";
}

bool ImageButton::on_key(const KeyEvent key) {
    if (key == KeyEvent::Select) {
        if (on_select) {
            on_select(*this);
            return true;
        }
    }

    return false;
}

bool ImageButton::on_keyboard(const KeyboardEvent key) {
    if (key == 32 || key == 10) {
        if (on_select) {
            on_select(*this);
            return true;
        }
    }
    return false;
}

bool ImageButton::on_touch(const TouchEvent event) {
    switch (event.type) {
        case TouchEvent::Type::Start:
            set_highlighted(true);
            set_dirty();
            return true;

        case TouchEvent::Type::End:
            set_highlighted(false);
            set_dirty();
            if (on_select) {
                on_select(*this);
            }
            return true;

        default:
            return false;
    }
}

/* ImageToggle ***********************************************************/
ImageToggle::ImageToggle(
    Rect parent_rect,
    const Bitmap* bitmap_)
    : ImageToggle{parent_rect,
                  bitmap_,
                  Color::green(),
                  Color::light_grey(),
                  Color::dark_grey()} {}

ImageToggle::ImageToggle(
    Rect parent_rect,
    const Bitmap* bitmap_,
    Color foreground_true,
    Color foreground_false,
    Color background_)
    : ImageToggle{parent_rect,
                  bitmap_,
                  bitmap_,
                  foreground_true,
                  background_,
                  foreground_false,
                  background_} {}

ImageToggle::ImageToggle(
    Rect parent_rect,
    const Bitmap* bitmap_true,
    const Bitmap* bitmap_false,
    Color foreground_true,
    Color background_true,
    Color foreground_false,
    Color background_false)
    : ImageButton{parent_rect, bitmap_false, foreground_false, background_false},
      bitmap_true_{bitmap_true},
      bitmap_false_{bitmap_false},
      foreground_true_{foreground_true},
      background_true_{background_true},
      foreground_false_{foreground_false},
      background_false_{background_false},
      value_{false} {
    ImageButton::on_select = [this](ImageButton&) {
        set_value(!value());
    };
}

bool ImageToggle::value() const {
    return value_;
}

void ImageToggle::getAccessibilityText(std::string& result) {
    result = value_ ? "checked" : "unchecked";
}
void ImageToggle::getWidgetName(std::string& result) {
    result = "ImageToggle";
}

void ImageToggle::set_value(bool b) {
    if (b == value_)
        return;

    value_ = b;
    set_bitmap(b ? bitmap_true_ : bitmap_false_);
    set_foreground(b ? foreground_true_ : foreground_false_);
    set_background(b ? background_true_ : background_false_);

    if (on_change)
        on_change(b);
}

/* ImageOptionsField *****************************************************/

ImageOptionsField::ImageOptionsField(
    Rect parent_rect,
    Color foreground,
    Color background,
    options_t options)
    : Widget{parent_rect},
      options{std::move(options)},
      foreground_{foreground},
      background_{background} {
    set_focusable(true);
}

size_t ImageOptionsField::selected_index() const {
    return selected_index_;
}

size_t ImageOptionsField::selected_index_value() const {
    return options[selected_index_].second;
}

void ImageOptionsField::getAccessibilityText(std::string& result) {
    result = "selected index: " + to_string_dec_uint(selected_index_);
}
void ImageOptionsField::getWidgetName(std::string& result) {
    result = "ImageOptionsField";
}

void ImageOptionsField::set_selected_index(const size_t new_index) {
    if (new_index < options.size()) {
        if (new_index != selected_index()) {
            selected_index_ = new_index;
            if (on_change) {
                on_change(selected_index(), options[selected_index()].second);
            }
            set_dirty();
        }
    }
}

void ImageOptionsField::set_by_value(value_t v) {
    size_t new_index = 0;
    for (const auto& option : options) {
        if (option.second == v) {
            set_selected_index(new_index);
            return;
        }

        new_index++;
    }

    // No exact match was found, default to 0.
    set_selected_index(0);
}

void ImageOptionsField::set_options(options_t new_options) {
    options = std::move(new_options);

    // Set an invalid index to force on_change.
    selected_index_ = (size_t)-1;
    set_selected_index(0);
    set_dirty();
}

void ImageOptionsField::paint(Painter& painter) {
    const bool selected = (has_focus() || highlighted());
    const auto paint_style = selected ? style().invert() : style();

    painter.draw_rectangle(
        {screen_rect().location(), {screen_rect().size().width() + 4, screen_rect().size().height() + 4}},
        paint_style.background);

    painter.draw_bitmap(
        {screen_pos().x() + 2, screen_pos().y() + 2},
        *options[selected_index_].first,
        foreground_,
        background_);
}

void ImageOptionsField::on_focus() {
    if (on_show_options) {
        on_show_options();
    }
}

bool ImageOptionsField::on_encoder(const EncoderEvent delta) {
    set_selected_index(selected_index() + delta);
    return true;
}

bool ImageOptionsField::on_keyboard(const KeyboardEvent key) {
    if (key == '+' || key == ' ' || key == 10) return on_encoder(1);
    if (key == '-' || key == 8) return on_encoder(-1);
    return false;
}

bool ImageOptionsField::on_touch(const TouchEvent event) {
    if (event.type == TouchEvent::Type::Start) {
        focus();
    }
    return true;
}

/* OptionsField **********************************************************/

OptionsField::OptionsField(
    Point parent_pos,
    size_t length,
    options_t options)
    : Widget{{parent_pos, {8 * (int)length, 16}}},
      length_{length},
      options_{std::move(options)} {
    set_focusable(true);
}

size_t OptionsField::selected_index() const {
    return selected_index_;
}

const OptionsField::name_t& OptionsField::selected_index_name() const {
    return options_[selected_index_].first;
}

const OptionsField::value_t& OptionsField::selected_index_value() const {
    return options_[selected_index_].second;
}

void OptionsField::getAccessibilityText(std::string& result) {
    result = "options: ";
    bool first = true;
    for (const auto& option : options_) {
        if (!first) result += " ,";
        first = false;
        result += option.first;
    }
    result += "; selected: " + selected_index_name();
}
void OptionsField::getWidgetName(std::string& result) {
    result = "OptionsField";
}

void OptionsField::set_selected_index(const size_t new_index, bool trigger_change) {
    if (new_index < options_.size()) {
        if (new_index != selected_index() || trigger_change) {
            selected_index_ = new_index;
            if (on_change) {
                on_change(selected_index(), options_[selected_index()].second);
            }
            set_dirty();
        }
    }
}

void OptionsField::set_by_value(value_t v) {
    size_t new_index = 0;
    for (const auto& option : options_) {
        if (option.second == v) {
            set_selected_index(new_index);
            return;
        }
        new_index++;
    }

    // No exact match was found, default to 0.
    set_selected_index(0);
}

void OptionsField::set_by_nearest_value(value_t v) {
    size_t new_index = 0;
    size_t curr_index = 0;
    int32_t min_diff = INT32_MAX;

    for (const auto& option : options_) {
        auto diff = abs(v - option.second);
        if (diff < min_diff) {
            min_diff = diff;
            new_index = curr_index;
        }

        curr_index++;
    }

    set_selected_index(new_index);
}

void OptionsField::set_options(options_t new_options) {
    options_ = std::move(new_options);

    // Set an invalid index to force on_change.
    selected_index_ = (size_t)-1;
    set_selected_index(0);
    set_dirty();
}

void OptionsField::paint(Painter& painter) {
    const auto paint_style = has_focus() ? style().invert() : style();

    painter.fill_rectangle({screen_rect().location(), {(int)length_ * 8, 16}}, ui::Color::black());

    if (selected_index() < options_.size()) {
        std::string_view temp = selected_index_name();
        if (temp.length() > length_)
            temp = temp.substr(0, length_);
        painter.draw_string(
            screen_pos(),
            paint_style,
            temp);
    }
}

void OptionsField::on_focus() {
    if (on_show_options) {
        on_show_options();
    }
}

bool OptionsField::on_encoder(const EncoderEvent delta) {
    int32_t new_value = selected_index() + delta;
    if (new_value < 0)
        new_value = options_.size() - 1;
    else if ((size_t)new_value >= options_.size())
        new_value = 0;

    set_selected_index(new_value);
    return true;
}
bool OptionsField::on_keyboard(const KeyboardEvent key) {
    if (key == '+' || key == ' ' || key == 10) return on_encoder(1);
    if (key == '-' || key == 8) return on_encoder(-1);
    return false;
}

bool OptionsField::on_touch(const TouchEvent event) {
    if (event.type == TouchEvent::Type::Start) {
        focus();
    }
    return true;
}

/* TextEdit ***********************************************************/

TextEdit::TextEdit(
    std::string& str,
    size_t max_length,
    Point position,
    uint32_t length)
    : Widget{{position, {8 * static_cast<int>(length), 16}}},
      text_{str},
      max_length_{std::max<size_t>(max_length, str.length())},
      char_count_{std::max<uint32_t>(length, 1)},
      cursor_pos_{text_.length()},
      insert_mode_{true} {
    set_focusable(true);
}

const std::string& TextEdit::value() const {
    return text_;
}

void TextEdit::getAccessibilityText(std::string& result) {
    result = text_;
}
void TextEdit::getWidgetName(std::string& result) {
    result = "TextEdit";
}

void TextEdit::set_cursor(uint32_t pos) {
    cursor_pos_ = std::min<size_t>(pos, text_.length());
    set_dirty();
}

void TextEdit::set_insert_mode() {
    insert_mode_ = true;
}

void TextEdit::set_overwrite_mode() {
    insert_mode_ = false;
}

void TextEdit::char_add(char c) {
    // Don't add if inserting and at max_length and
    // don't overwrite if past the end of the text.
    if ((text_.length() >= max_length_ && insert_mode_) ||
        (cursor_pos_ >= text_.length() && !insert_mode_))
        return;

    if (insert_mode_)
        text_.insert(cursor_pos_, 1, c);
    else
        text_[cursor_pos_] = c;

    cursor_pos_++;
    set_dirty();
}

void TextEdit::char_delete() {
    if (cursor_pos_ == 0)
        return;

    cursor_pos_--;
    text_.erase(cursor_pos_, 1);
    set_dirty();
}

void TextEdit::paint(Painter& painter) {
    auto rect = screen_rect();
    auto text_style = has_focus() ? style().invert() : style();
    auto offset = 0;

    // Does the string need to be shifted?
    if (cursor_pos_ >= char_count_)
        offset = cursor_pos_ - char_count_ + 1;

    // Draw the text starting at the offset.
    for (uint32_t i = 0; i < char_count_; i++) {
        // Using draw_char to blank the rest of the line with spaces produces less flicker.
        auto c = (i + offset < text_.length()) ? text_[i + offset] : ' ';

        painter.draw_char(
            {rect.location().x() + (static_cast<int>(i) * char_width), rect.location().y()},
            text_style, c);
    }

    // Determine cursor position on screen (either the cursor position or the last char).
    int32_t cursor_x = char_width * (offset > 0 ? char_count_ - 1 : cursor_pos_);
    Point cursor_point{screen_pos().x() + cursor_x, screen_pos().y()};
    auto cursor_style = text_style.invert();

    // Invert the cursor character when in overwrite mode.
    if (!insert_mode_ && (cursor_pos_) < text_.length())
        painter.draw_char(cursor_point, cursor_style, text_[cursor_pos_]);

    // Draw the cursor.
    Rect cursor_box{cursor_point, {char_width, char_height}};
    painter.draw_rectangle(cursor_box, cursor_style.background);
}

bool TextEdit::on_key(const KeyEvent key) {
    if (key == KeyEvent::Left && cursor_pos_ > 0)
        cursor_pos_--;
    else if (key == KeyEvent::Right && cursor_pos_ < text_.length())
        cursor_pos_++;
    else if (key == KeyEvent::Select) {
        if (key_is_long_pressed(key)) {
            // Delete text to the cursor.
            text_ = text_.substr(cursor_pos_);
            set_cursor(0);
        } else {
            insert_mode_ = !insert_mode_;
        }
    } else
        return false;

    set_dirty();
    return true;
}

bool TextEdit::on_keyboard(const KeyboardEvent key) {
    // if ascii printable
    if (key >= 0x20 && key <= 0x7e) {
        char_add(key);
        return true;
    }
    if (key == 8) {
        char_delete();
        return true;
    }
    return false;
}

bool TextEdit::on_encoder(const EncoderEvent delta) {
    int32_t new_pos = cursor_pos_ + delta;

    // Let the encoder wrap around the ends of the text.
    if (new_pos < 0)
        new_pos = text_.length();
    else if (static_cast<size_t>(new_pos) > text_.length())
        new_pos = 0;

    set_cursor(new_pos);
    return true;
}

bool TextEdit::on_touch(const TouchEvent event) {
    if (event.type == TouchEvent::Type::Start)
        focus();

    set_dirty();
    return true;
}

void TextEdit::on_focus() {
    // Enable long press on "Select".
    SwitchesState config;
    config[toUType(Switch::Sel)] = true;
    set_switches_long_press_config(config);
}

void TextEdit::on_blur() {
    // Reset long press.
    SwitchesState config{};
    set_switches_long_press_config(config);
}

/* TextField *************************************************************/

TextField::TextField(Rect parent_rect, std::string text)
    : Text(parent_rect, std::move(text)) {
    set_focusable(true);
}

const std::string& TextField::get_text() const {
    return text;
}

void TextField::getAccessibilityText(std::string& result) {
    result = text;
}
void TextField::getWidgetName(std::string& result) {
    result = "TextField";
}

void TextField::set_text(std::string_view value) {
    set(value);
    if (on_change)
        on_change(*this);
}

bool TextField::on_key(KeyEvent key) {
    if (key == KeyEvent::Select && on_select) {
        on_select(*this);
        return true;
    }

    return false;
}

bool TextField::on_encoder(EncoderEvent delta) {
    if (on_encoder_change) {
        on_encoder_change(*this, delta);
        return true;
    }

    return false;
}

bool TextField::on_touch(TouchEvent event) {
    if (event.type == TouchEvent::Type::Start) {
        focus();
        return true;
    }

    return false;
}

/* BatteryIcon *************************************************************/

BatteryIcon::BatteryIcon(Rect parent_rect, uint8_t percent)
    : Widget(parent_rect) {
    this->set_battery(percent, false);
    set_focusable(true);
}

void BatteryIcon::getAccessibilityText(std::string& result) {
    result = to_string_dec_uint(percent_) + "%";
}
void BatteryIcon::getWidgetName(std::string& result) {
    result = "Battery percent";
}

void BatteryIcon::set_battery(uint8_t percentage, bool charge) {
    if (charge == charge_ && percent_ == percentage) return;
    percent_ = percentage;
    charge_ = charge;
    set_dirty();
}

bool BatteryIcon::on_key(KeyEvent key) {
    if (key == KeyEvent::Select && on_select) {
        on_select();
        return true;
    }
    return false;
}

bool BatteryIcon::on_touch(TouchEvent event) {
    if (event.type == TouchEvent::Type::Start) {
        focus();
        return true;
    }
    if (event.type == TouchEvent::Type::End && on_select) {
        on_select();
        return true;
    }
    return false;
}
void BatteryIcon::paint(Painter& painter) {
    ui::Rect rect = screen_rect();                                                                          // 10, 1 * 16
    painter.fill_rectangle(rect, has_focus() || highlighted() ? Color::light_grey() : Color::dark_grey());  // clear
    ui::Color battColor = (charge_) ? Color::cyan() : Color::green();
    // batt body:
    painter.draw_vline({rect.left() + 1, rect.top() + 2}, rect.height() - 4, battColor);
    painter.draw_vline({rect.right() - 2, rect.top() + 2}, rect.height() - 4, battColor);
    painter.draw_hline({rect.left() + 1, rect.top() + 2}, rect.width() - 2, battColor);
    painter.draw_hline({rect.left() + 1, rect.bottom() - 2}, rect.width() - 2, battColor);
    // batt cap:
    painter.draw_hline({rect.left() + 3, rect.top() + 1}, rect.width() - 6, battColor);
    painter.draw_hline({rect.left() + 3, 0}, rect.width() - 6, battColor);
    if (percent_ > 100) {  // error / unk
        painter.draw_string({rect.left() + 2, rect.top() + 3}, font::fixed_5x8, Color::white(), Color::dark_grey(), "?");
        return;
    }
    int8_t ppx = (rect.bottom() - 3) - (rect.top() + 2);                                // 11px max height to draw bars
    int8_t ptd = (int8_t)((static_cast<float>(percent_) / 100.0f) * (float)ppx + 0.5);  // pixels to draw
    int8_t pp = ppx - ptd;                                                              // pixels to start from

    if (percent_ >= 70)
        battColor = Color::green();
    else if (percent_ >= 40)
        battColor = Color::orange();
    else
        battColor = Color::red();
    // fill the bars
    for (int y = pp; y < ppx; y++) {
        painter.draw_hline({rect.left() + 2, rect.top() + 3 + y}, rect.width() - 4, battColor);
    }
}

/* BatteryTextField *************************************************************/

BatteryTextField::BatteryTextField(Rect parent_rect, uint8_t percent)
    : Widget(parent_rect) {
    this->set_battery(percent, false);
    set_focusable(true);
}

void BatteryTextField::paint(Painter& painter) {
    Color bg = has_focus() || highlighted() ? Color::light_grey() : Color::dark_grey();
    ui::Rect rect = screen_rect();     // 2 * 8, 1 * 16
    painter.fill_rectangle(rect, bg);  // clear
    std::string txt_batt = percent_ <= 100 ? to_string_dec_uint(percent_) : "UNK";
    int xdelta = 0;
    if (txt_batt.length() == 1)
        xdelta = 5;
    else if (txt_batt.length() == 2)
        xdelta = 2;
    painter.draw_string({rect.left() + xdelta, rect.top()}, font::fixed_5x8, Color::white(), bg, txt_batt);
    painter.draw_string({rect.left(), rect.top() + 8}, font::fixed_5x8, Color::white(), bg, (charge_) ? "+%" : " %");
}

void BatteryTextField::getAccessibilityText(std::string& result) {
    result = to_string_dec_uint(percent_) + "%";
}
void BatteryTextField::getWidgetName(std::string& result) {
    result = "Battery percent";
}

void BatteryTextField::set_battery(uint8_t percentage, bool charge) {
    if (charge == charge_ && percent_ == percentage) return;
    charge_ = charge;
    percent_ = percentage;
    set_dirty();
}

bool BatteryTextField::on_key(KeyEvent key) {
    if (key == KeyEvent::Select && on_select) {
        on_select();
        return true;
    }
    return false;
}

bool BatteryTextField::on_touch(TouchEvent event) {
    if (event.type == TouchEvent::Type::Start) {
        focus();
        return true;
    }
    if (event.type == TouchEvent::Type::End && on_select) {
        on_select();
        return true;
    }
    return false;
}

/* NumberField ***********************************************************/

NumberField::NumberField(
    Point parent_pos,
    int length,
    range_t range,
    int32_t step,
    char fill_char,
    bool can_loop)
    : Widget{{parent_pos, {8 * length, 16}}},
      range{range},
      step{step},
      length_{length},
      fill_char{fill_char},
      can_loop{can_loop} {
    set_focusable(true);
}

int32_t NumberField::value() const {
    return value_;
}

void NumberField::getAccessibilityText(std::string& result) {
    result = to_string_dec_int(value_);
}
void NumberField::getWidgetName(std::string& result) {
    result = "NumberField";
}

void NumberField::set_value(int32_t new_value, bool trigger_change) {
    if (can_loop) {
        if (new_value >= range.first)
            new_value = new_value % (range.second + 1);
        else
            new_value = range.second + new_value + 1;
    }

    new_value = clip(new_value, range.first, range.second);

    if (new_value != value()) {
        value_ = new_value;
        if (on_change && trigger_change) {
            on_change(value_);
        }
        set_dirty();
    }
}

void NumberField::set_range(const int32_t min, const int32_t max) {
    range.first = min;
    range.second = max;
    set_value(value(), false);
}

void NumberField::set_step(const int32_t new_step) {
    step = new_step;
}

void NumberField::paint(Painter& painter) {
    const auto text = to_string_dec_int(value_, length_, fill_char);

    const auto paint_style = has_focus() ? style().invert() : style();

    painter.draw_string(
        screen_pos(),
        paint_style,
        text);
}

bool NumberField::on_key(const KeyEvent key) {
    if (key == KeyEvent::Select) {
        if (on_select) {
            on_select(*this);
            return true;
        }
    }

    return false;
}

bool NumberField::on_encoder(const EncoderEvent delta) {
    int32_t old_value = value();
    set_value(value() + (delta * step));

    if (on_wrap) {
        if ((delta > 0) && (value() < old_value))
            on_wrap(1);
        else if ((delta < 0) && (value() > old_value))
            on_wrap(-1);
    }
    return true;
}

bool NumberField::on_keyboard(const KeyboardEvent key) {
    if (key == 10) {
        if (on_select) {
            on_select(*this);
            return true;
        }
    }
    if (key == '+' || key == ' ') {
        return on_encoder(1);
    }
    if (key == '-' || key == 8) {
        return on_encoder(-1);
    }
    return false;
}

bool NumberField::on_touch(const TouchEvent event) {
    if (event.type == TouchEvent::Type::Start) {
        focus();
    }
    return true;
}

/* SymField **************************************************************/

SymField::SymField(
    Point parent_pos,
    size_t length,
    Type type,
    bool explicit_edits)
    : Widget{{parent_pos, {char_width * (int)length, 16}}},
      type_{type},
      explicit_edits_{explicit_edits} {
    if (length == 0)
        length = 1;

    selected_ = length - 1;
    value_.resize(length);

    switch (type) {
        case Type::Oct:
            set_symbol_list("01234567");
            break;

        case Type::Dec:
            set_symbol_list("0123456789");
            break;

        case Type::Hex:
            set_symbol_list("0123456789ABCDEF");
            break;

        case Type::Alpha:
            set_symbol_list(" 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
            break;

        default:
            set_symbol_list("01");
            break;
    }

    set_focusable(true);
}

SymField::SymField(
    Point parent_pos,
    size_t length,
    std::string symbol_list,
    bool explicit_edits)
    : SymField{parent_pos, length, Type::Custom, explicit_edits} {
    set_symbol_list(std::move(symbol_list));
}

char SymField::get_symbol(size_t index) const {
    if (index >= value_.length())
        return 0;

    return value_[index];
}

void SymField::set_symbol(size_t index, char symbol) {
    if (index >= value_.length())
        return;

    set_symbol_internal(index, ensure_valid(symbol));
}

size_t SymField::get_offset(size_t index) const {
    if (index >= value_.length())
        return 0;

    // NB: Linear search - symbol lists are small.
    return symbols_.find(value_[index]);
}

void SymField::set_offset(size_t index, size_t offset) {
    if (index >= value_.length() || offset >= symbols_.length())
        return;

    set_symbol_internal(index, symbols_[offset]);
}

void SymField::set_symbol_list(std::string symbol_list) {
    if (symbol_list.length() == 0)
        return;

    symbols_ = std::move(symbol_list);
    ensure_all_symbols();
}

void SymField::set_value(uint64_t value) {
    auto v = value;
    uint8_t radix = get_radix();

    for (int i = value_.length() - 1; i >= 0; --i) {
        uint8_t temp = v % radix;
        value_[i] = uint_to_char(temp, radix);
        v /= radix;
    }

    if (on_change)
        on_change(*this);
}

void SymField::set_value(std::string_view value) {
    // Is new value too long?
    // TODO: Truncate instead? Which end?
    if (value.length() > value_.length())
        return;

    // Right-align string in field.
    auto left_padding = value_.length() - value.length();
    value_ = std::string(static_cast<size_t>(left_padding), '\0') + std::string{value};
    ensure_all_symbols();
}

uint64_t SymField::to_integer() const {
    uint64_t v = 0;
    uint64_t mul = 1;
    uint8_t radix = get_radix();

    for (int i = value_.length() - 1; i >= 0; --i) {
        auto temp = char_to_uint(value_[i], radix);
        v += temp * mul;
        mul *= radix;
    }

    return v;
}

const std::string& SymField::to_string() const {
    return value_;
}

void SymField::getAccessibilityText(std::string& result) {
    result = value_;
}
void SymField::getWidgetName(std::string& result) {
    result = "SymField";
}
void SymField::paint(Painter& painter) {
    Point p = screen_pos();

    for (size_t n = 0; n < value_.length(); n++) {
        auto c = value_[n];
        MutableStyle paint_style{style()};

        // Only highlight while focused.
        if (has_focus()) {
            if (explicit_edits_) {
                // Invert the whole field on focus if explicit edits is enabled.
                paint_style.invert();
            } else if (n == selected_) {
                // Otherwise only highlight the selected symbol.
                paint_style.invert();
            }

            if (editing_ && n == selected_) {
                // Use 'bg_blue' style to indicate in editing mode.
                paint_style.foreground = Color::white();
                paint_style.background = Color::blue();
            }
        }

        painter.draw_char(p, paint_style, c);
        p += {8, 0};
    }
}

bool SymField::on_key(KeyEvent key) {
    // If explicit edits are enabled, only Select is handled when not in edit mode.
    if (explicit_edits_ && !editing_) {
        switch (key) {
            case KeyEvent::Select:
                editing_ = true;
                set_dirty();
                return true;

            default:
                return false;
        }
    }

    switch (key) {
        case KeyEvent::Select:
            editing_ = !editing_;
            set_dirty();
            return true;

        case KeyEvent::Left:
            if (selected_ > 0) {
                selected_--;
                set_dirty();
                return true;
            }
            break;

        case KeyEvent::Right:
            if (selected_ < (value_.length() - 1)) {
                selected_++;
                set_dirty();
                return true;
            }
            break;

        case KeyEvent::Up:
            if (editing_) {
                on_encoder(1);
                return true;
            }
            break;

        case KeyEvent::Down:
            if (editing_) {
                on_encoder(-1);
                return true;
            }
            break;

        default:
            break;
    }

    return false;
}

bool SymField::on_encoder(EncoderEvent delta) {
    if (explicit_edits_ && !editing_)
        return false;

    // TODO: Wrapping or carrying might be nice.
    int offset = get_offset(selected_) + delta;

    offset = clip<int>(offset, 0, symbols_.length() - 1);
    set_offset(selected_, offset);

    return true;
}

bool SymField::on_touch(TouchEvent event) {
    if (event.type == TouchEvent::Type::Start)
        focus();

    return true;
}

char SymField::ensure_valid(char symbol) const {
    // NB: Linear search - symbol lists are small.
    auto pos = symbols_.find(symbol);
    return pos != std::string::npos ? symbol : symbols_[0];
}

void SymField::ensure_all_symbols() {
    auto temp = value_;

    for (auto& c : value_)
        c = ensure_valid(c);

    if (temp != value_) {
        if (on_change)
            on_change(*this);
        set_dirty();
    }
}

void SymField::set_symbol_internal(size_t index, char symbol) {
    if (value_[index] == symbol)
        return;

    value_[index] = symbol;
    if (on_change)
        on_change(*this);
    set_dirty();
}

uint8_t SymField::get_radix() const {
    switch (type_) {
        case Type::Oct:
            return 8;
        case Type::Dec:
            return 10;
        case Type::Hex:
            return 16;
        default:
            return 0;
    }
}

/* Waveform **************************************************************/

Waveform::Waveform(
    Rect parent_rect,
    int16_t* data,
    uint32_t length,
    uint32_t offset,
    bool digital,
    Color color)
    : Widget{parent_rect},
      data_{data},
      length_{length},
      offset_{offset},
      digital_{digital},
      color_{color} {
    // set_focusable(false);
    // previous_data.resize(length_, 0);
}

void Waveform::set_cursor(const uint32_t i, const int16_t position) {
    if (i < 2) {
        if (position != cursors[i]) {
            cursors[i] = position;
            set_dirty();
        }
        show_cursors = true;
    }
}

void Waveform::set_offset(const uint32_t new_offset) {
    if (new_offset != offset_) {
        offset_ = new_offset;
        set_dirty();
    }
}

void Waveform::set_length(const uint32_t new_length) {
    if (new_length != length_) {
        length_ = new_length;
        set_dirty();
    }
}

void Waveform::paint(Painter& painter) {
    size_t n;
    Coord y, y_offset = screen_rect().location().y();
    Coord prev_x = screen_rect().location().x(), prev_y;
    float x, x_inc;
    Dim h = screen_rect().size().height();
    const float y_scale = (float)(h - 1) / 65536.0;
    int16_t* data_start = data_ + offset_;

    if (!length_) return;

    x_inc = (float)screen_rect().size().width() / length_;

    // Clear
    painter.fill_rectangle_unrolled8(screen_rect(), Color::black());

    if (digital_) {
        // Digital waveform: each value is an horizontal line
        x = 0;
        h--;
        for (n = 0; n < length_; n++) {
            y = *(data_start++) ? h : 0;

            if (n) {
                if (y != prev_y)
                    painter.draw_vline({(Coord)x, y_offset}, h, color_);
            }

            painter.draw_hline({(Coord)x, y_offset + y}, ceil(x_inc), color_);

            prev_y = y;
            x += x_inc;
        }
    } else {
        // Analog waveform: each value is a point's Y coordinate
        x = prev_x + x_inc;
        h /= 2;
        prev_y = y_offset + h - (*(data_start++) * y_scale);
        for (n = 1; n < length_; n++) {
            y = y_offset + h - (*(data_start++) * y_scale);
            display.draw_line({prev_x, prev_y}, {(Coord)x, y}, color_);

            prev_x = x;
            prev_y = y;
            x += x_inc;
        }
    }

    // Cursors
    if (show_cursors) {
        for (n = 0; n < 2; n++) {
            painter.draw_vline(
                Point(std::min(screen_rect().size().width(), (int)cursors[n]), y_offset),
                screen_rect().size().height(),
                cursor_colors[n]);
        }
    }
}

/* VuMeter **************************************************************/

VuMeter::VuMeter(
    Rect parent_rect,
    uint32_t LEDs,
    bool show_max)
    : Widget{parent_rect},
      LEDs_{LEDs},
      show_max_{show_max} {
    // set_focusable(false);
    LED_height = std::max(1UL, parent_rect.size().height() / LEDs);
    split = 256 / LEDs;
}

void VuMeter::set_value(const uint32_t new_value) {
    if ((new_value != value_) && (new_value < 256)) {
        value_ = new_value;
        set_dirty();
    }
}

void VuMeter::set_mark(const uint32_t new_mark) {
    if ((new_mark != mark) && (new_mark < 256)) {
        mark = new_mark;
        set_dirty();
    }
}

void VuMeter::paint(Painter& painter) {
    uint32_t bar;
    Color color;
    bool lit = false;
    uint32_t bar_level;
    Point pos = screen_rect().location();
    Dim width = screen_rect().size().width() - 4;
    Dim height = screen_rect().size().height();
    Dim bottom = pos.y() + height;
    Coord marks_x = pos.x() + width;

    if (value_ != prev_value) {
        bar_level = LEDs_ - ((value_ + 1) / split);

        // Draw LEDs
        for (bar = 0; bar < LEDs_; bar++) {
            if (bar >= bar_level)
                lit = true;

            if (bar == 0)
                color = lit ? Color::red() : Color::dark_grey();
            else if (bar == 1)
                color = lit ? Color::orange() : Color::dark_grey();
            else if ((bar == 2) || (bar == 3))
                color = lit ? Color::yellow() : Color::dark_grey();
            else
                color = lit ? Color::green() : Color::dark_grey();

            painter.fill_rectangle({pos.x(), pos.y() + (Coord)(bar * (LED_height + 1)), width, (Coord)LED_height}, color);
        }
        prev_value = value_;
    }

    // Update max level
    if (show_max_) {
        if (value_ > max) {
            max = value_;
            hold_timer = 30;  // 0.5s @ 60Hz
        } else {
            if (hold_timer) {
                hold_timer--;
            } else {
                if (max) max--;  // Let it drop
            }
        }

        // Draw max level
        if (max != prev_max) {
            painter.draw_hline({marks_x, bottom - (height * prev_max) / 256}, 8, Color::black());
            painter.draw_hline({marks_x, bottom - (height * max) / 256}, 8, Color::white());
            if (prev_max == mark)
                prev_mark = 0;  // Force mark refresh
            prev_max = max;
        }
    }

    // Draw mark (forced refresh)
    if (mark) {
        painter.draw_hline({marks_x, bottom - (height * prev_mark) / 256}, 8, Color::black());
        painter.draw_hline({marks_x, bottom - (height * mark) / 256}, 8, Color::grey());
        prev_mark = mark;
    }
}

} /* namespace ui */