#include "messages.h"

#include "chatlog.h"
#include "file_transfers.h"
#include "filesys.h"
#include "flist.h"
#include "friend.h"
#include "groups.h"
#include "debug.h"
#include "macros.h"
#include "self.h"
#include "settings.h"
#include "text.h"
#include "theme.h"
#include "tox.h"
#include "utox.h"

#include "ui/contextmenu.h"
#include "ui/draw.h"
#include "ui/scrollable.h"
#include "ui/svg.h"
#include "ui/text.h"

#include "layout/friend.h"
#include "layout/group.h"

#include "native/clipboard.h"
// TODO including native .h files should never be needed, refactor filesys.h to provide necessary API
#include "native/filesys.h"
#include "native/image.h"
#include "native/keyboard.h"
#include "native/os.h"

#include <stdlib.h>
#include <string.h>

#define UTOX_MAX_BACKLOG_MESSAGES 256

pthread_mutex_t messages_lock;

/** Appends a messages from self or friend to the message list;
 * will realloc or trim messages as needed;
 *
 * also handles auto scrolling selections with messages
 *
 * accepts: MESSAGES *pointer, MESSAGE *pointer, MSG_DATA *pointer
 */

static int get_time_width() {
    return SCALE(settings.use_long_time_msg ? TIME_WIDTH_LONG : TIME_WIDTH);
}

static int msgheight(MSG_HEADER *msg, int width) {
    switch (msg->msg_type) {
        case MSG_TYPE_NULL: {
            LOG_ERR("Messages", "Invalid message type in msgheight.");
            return 0;
        }

        case MSG_TYPE_TEXT:
        case MSG_TYPE_ACTION_TEXT:
        case MSG_TYPE_NOTICE:
        case MSG_TYPE_NOTICE_DAY_CHANGE: {
            int  theight = text_height(abs(width - SCALE(MESSAGES_X) - get_time_width()), font_small_lineheight,
                                       msg->via.txt.msg, msg->via.txt.length);
            return (theight == 0) ? 0 : theight + MESSAGES_SPACING;
        }

        case MSG_TYPE_IMAGE: {
            uint32_t maxwidth = width - SCALE(MESSAGES_X) - get_time_width();
            if (msg->via.img.zoom || msg->via.img.w <= maxwidth) {
                return msg->via.img.h + MESSAGES_SPACING;
            }

            return msg->via.img.h * maxwidth / msg->via.img.w + MESSAGES_SPACING;
        }

        case MSG_TYPE_FILE: {
            return FILE_TRANSFER_BOX_HEIGHT + MESSAGES_SPACING;
        }
    }

    return 0;
}

static int msgheight_group(MSG_HEADER *msg, int width) {
    switch (msg->msg_type) {
        case MSG_TYPE_TEXT:
        case MSG_TYPE_ACTION_TEXT:
        case MSG_TYPE_NOTICE:
        case MSG_TYPE_NOTICE_DAY_CHANGE: {
            int theight = text_height(abs(width - SCALE(MESSAGES_X) - get_time_width()), font_small_lineheight,
                                      msg->via.grp.msg, msg->via.grp.length);
            return (theight == 0) ? 0 : theight + MESSAGES_SPACING;
        }

        default: {
            LOG_TRACE("Messages", "Error, can't set this group message height" );
        }
    }

    return 0;
}

static int message_setheight(MESSAGES *m, MSG_HEADER *msg) {
    if (m->width == 0) {
        return 0;
    }

    setfont(FONT_TEXT);

    if (m->is_groupchat) {
        msg->height    = msgheight_group(msg, m->width);
    } else {
        msg->height = msgheight(msg, m->width);
    }

    return msg->height;
}

static void message_updateheight(MESSAGES *m, MSG_HEADER *msg) {
    if (m->width == 0) {
        return;
    }

    setfont(FONT_TEXT);

    m->height   -= msg->height;
    msg->height  = message_setheight(m, msg);
    m->height   += msg->height;
}

static uint32_t message_add(MESSAGES *m, MSG_HEADER *msg) {
    pthread_mutex_lock(&messages_lock);

    if (m->number < UTOX_MAX_BACKLOG_MESSAGES) {
        if (!m->data || m->extra <= 0) {
            if (m->data) {
                m->data = realloc(m->data, (m->number + 10) * sizeof(void *));
                m->extra += 10;
            } else {
                m->number = 0;
                m->data = calloc(20, sizeof(void *));
                m->extra = 20;
            }

            if (!m->data) {
                LOG_FATAL_ERR(EXIT_MALLOC, "Messages", "\n\n\nFATAL ERROR TRYING TO REALLOC FOR MESSAGES.\nTHIS IS A BUG, PLEASE REPORT!\n\n\n");
            }
        }
        m->data[m->number++] = msg;
        m->extra--;
    } else {
        m->height -= m->data[0]->height;
        message_free(m->data[0]);
        memmove(m->data, m->data + 1, (UTOX_MAX_BACKLOG_MESSAGES - 1) * sizeof(MSG_HEADER *));
        m->data[UTOX_MAX_BACKLOG_MESSAGES - 1] = msg;

        // Scroll selection up so that it stays over the same messages.
        if (m->sel_start_msg != UINT32_MAX) {
            if (0 < m->sel_start_msg) {
                m->sel_start_msg--;
            } else {
                m->sel_start_position = 0;
            }
        }

        if (m->sel_end_msg != UINT32_MAX) {
            if (0 < m->sel_end_msg) {
                m->sel_end_msg--;
            } else {
                m->sel_end_position = 0;
            }
        }

        if (m->cursor_down_msg != UINT32_MAX) {
            if (0 < m->cursor_down_msg) {
                m->cursor_down_msg--;
            } else {
                m->cursor_down_position = 0;
            }
        }
        if (m->cursor_over_msg != UINT32_MAX) {
            if (0 < m->cursor_over_msg) {
                m->cursor_over_msg--;
            } else {
                m->cursor_over_position = 0;
            }
        }
    }

    message_updateheight(m, msg);

    if (m->is_groupchat) {
        const GROUPCHAT *groupchat = flist_get_sel_group();
        if (groupchat && groupchat == get_group(m->id)) {
            m->panel.content_scroll->content_height = m->height;
        }
    } else {
        const FRIEND *friend = flist_get_sel_friend();
        if (friend && friend == get_friend(m->id)) {
            m->panel.content_scroll->content_height = m->height;
        }
    }

    pthread_mutex_unlock(&messages_lock);
    return m->number;
}

static bool msg_add_day_notice(MESSAGES *m, time_t last, time_t next) {
    /* The tm struct is shared, we have to do it this way */
    int ltime_year = 0, ltime_mon = 0, ltime_day = 0;

    struct tm *msg_time = localtime(&last);

    ltime_year = msg_time->tm_year;
    ltime_mon  = msg_time->tm_mon;
    ltime_day  = msg_time->tm_mday;
    msg_time   = localtime(&next);

    if (ltime_year >= msg_time->tm_year
        && (ltime_year != msg_time->tm_year || ltime_mon >= msg_time->tm_mon)
        && (ltime_year != msg_time->tm_year || ltime_mon != msg_time->tm_mon || ltime_day >= msg_time->tm_mday))
    {
        return false;
    }

    MSG_HEADER *msg = calloc(1, sizeof(MSG_HEADER));
    if (!msg) {
        LOG_FATAL_ERR(EXIT_MALLOC, "Messages", "Couldn't allocate memory for day notice.");
    }

    time(&msg->time);
    msg->our_msg       = 0;
    msg->msg_type      = MSG_TYPE_NOTICE_DAY_CHANGE;

    msg->via.notice_day.msg    = calloc(1, 256);
    if (!msg->via.notice_day.msg) {
        LOG_FATAL_ERR(EXIT_MALLOC, "Messages", "Couldn't allocate memory for day notice.");
    }
    msg->via.notice_day.length = strftime((char *)msg->via.notice_day.msg, 256,
                                   "Day has changed to %A %B %d %Y", msg_time);
    if (0 == msg->via.notice_day.length) {
        LOG_ERR("Messages", "Couldn't compose day notice message.");
        free(msg->via.notice_day.msg);
        free(msg);
        return false;
    }

    message_add(m, msg);
    return true;
}

/* TODO leaving this here is a little hacky, but it was the fastest way
 * without considering if I should expose messages_add */
uint32_t message_add_group(MESSAGES *m, MSG_HEADER *msg) {
    return message_add(m, msg);
}

/* TODO This function and message_add_type_action() are essentially pasta. */
uint32_t message_add_type_text(MESSAGES *m, bool auth, const char *msgtxt, uint16_t length, bool log, bool send) {
    FRIEND *f = get_friend(m->id);
    if (!f) {
        LOG_ERR("Messages", "Could not get friend with id: %u", m->id);
        return UINT32_MAX;
    }

    MSG_HEADER *msg = calloc(1, sizeof(MSG_HEADER));
    if (!msg) {
        LOG_FATAL_ERR(EXIT_MALLOC, "Messages", "Could not allocate memory for a message.");
    }

    msg->via.txt.length = length;
    msg->via.txt.msg    = calloc(1, length);
    if (!msg->via.txt.msg) {
        LOG_FATAL_ERR(EXIT_MALLOC, "Messages", "Could not allocate memory for message.");
    }
    memcpy(msg->via.txt.msg, msgtxt, length);

    time(&msg->time);
    msg->our_msg  = auth;
    msg->msg_type = MSG_TYPE_TEXT;

    if (auth) {
        msg->via.txt.author_length = self.name_length;
        if (!send) {
            msg->receipt      = 0;
            msg->receipt_time = 1;
        }
    } else {
        msg->via.txt.author_length = f->name_length;
    }

    if (m->data && m->number) {
        MSG_HEADER *day_msg = m->data[m->number ? m->number - 1 : 0];
        msg_add_day_notice(m, day_msg->time, msg->time);
    }

    if (log) {
        message_log_to_disk(m, msg);
    }

    if (auth && send) {
        postmessage_toxcore(TOX_SEND_MESSAGE, m->id, length, msg);
    }

    return message_add(m, msg);
}

uint32_t message_add_type_action(MESSAGES *m, bool auth, const char *msgtxt, uint16_t length, bool log, bool send) {
    FRIEND *f = get_friend(m->id);
    if (!f) {
        LOG_ERR("Messages", "Could not get friend with number: %u", m->id);
        return UINT32_MAX;
    }

    MSG_HEADER *msg = calloc(1, sizeof(MSG_HEADER));
    if (!msg) {
        LOG_FATAL_ERR(EXIT_MALLOC, "Messages", "Could not get the message header.");
    }

    msg->via.action.length = length;
    msg->via.action.msg = calloc(1, length);
    if (!msg->via.action.msg) {
        LOG_FATAL_ERR(EXIT_MALLOC, "Messages", "Could not allocate memory for message.");
    }
    memcpy(msg->via.action.msg, msgtxt, length);

    time(&msg->time);
    msg->our_msg  = auth;
    msg->msg_type = MSG_TYPE_ACTION_TEXT;

    if (auth) {
        msg->via.txt.author_length = self.name_length;
        if (!send) {
            msg->receipt      = 0;
            msg->receipt_time = 1;
        }
    } else {
        msg->via.txt.author_length = f->name_length;
    }

    if (log) {
        message_log_to_disk(m, msg);
    }

    if (auth && send) {
        postmessage_toxcore(TOX_SEND_ACTION, f->number, length, msg);
    }

    return message_add(m, msg);
}

uint32_t message_add_type_notice(MESSAGES *m, const char *msgtxt, uint16_t length, bool log) {
    MSG_HEADER *msg = calloc(1, sizeof(MSG_HEADER));
    if (!msg) {
        LOG_FATAL_ERR(EXIT_MALLOC, "Messages", "Couldn't allocate memory for notice.");
    }

    msg->via.notice.length = length;
    msg->via.notice.msg = calloc(1, length);
    if (!msg->via.notice.msg) {
        LOG_FATAL_ERR(EXIT_MALLOC, "Messages", "Couldn't allocate memory for notice.");
    }
    memcpy(msg->via.notice.msg, msgtxt, length);

    time(&msg->time);
    msg->our_msg       = 0;
    msg->msg_type      = MSG_TYPE_NOTICE;
    msg->via.txt.author_length = self.name_length;
    msg->receipt_time  = time(NULL);

    if (log) {
        message_log_to_disk(m, msg);
    }

    return message_add(m, msg);
}

uint32_t message_add_type_image(MESSAGES *m, bool auth, NATIVE_IMAGE *img, uint16_t width, uint16_t height, bool UNUSED(log)) {
    if (!NATIVE_IMAGE_IS_VALID(img)) {
        return 0;
    }

    MSG_HEADER *msg = calloc(1, sizeof(MSG_HEADER));
    if (!msg) {
        LOG_FATAL_ERR(EXIT_MALLOC, "Messages", "Could not allocate memory for message header.");
    }

    time(&msg->time);
    msg->our_msg  = auth;
    msg->msg_type = MSG_TYPE_IMAGE;

    msg->via.img.w        = width;
    msg->via.img.h        = height;
    msg->via.img.zoom     = 0;
    msg->via.img.image    = img;
    msg->via.img.position = 0.0;

    return message_add(m, msg);
}

/* TODO FIX THIS SECTION TO MATCH ABOVE! */
/* Called by new file transfer to add a new message to the msg list */
MSG_HEADER *message_add_type_file(MESSAGES *m, uint32_t file_number, bool incoming, bool image, uint8_t status,
                                const uint8_t *name, size_t name_size, size_t target_size, size_t current_size)
{
    MSG_HEADER *msg = calloc(1, sizeof(MSG_HEADER));
    if (!msg) {
        LOG_FATAL_ERR(EXIT_MALLOC, "Messages", "Could not allocate memory for message header.");
    }

    time(&msg->time);
    msg->our_msg     = !incoming;
    msg->msg_type    = MSG_TYPE_FILE;

    msg->via.ft.file_status = status;

    msg->via.ft.file_number = file_number;

    msg->via.ft.size       = target_size;
    msg->via.ft.progress   = current_size;
    msg->via.ft.speed      = 0;
    msg->via.ft.inline_png = image;

    msg->via.ft.name_length = name_size;
    msg->via.ft.name = calloc(1, name_size + 1);
    if (!msg->via.ft.name) {
        LOG_FATAL_ERR(EXIT_MALLOC, "Messages", "Could not allocate memory for the file name.");
    }
    memcpy(msg->via.ft.name, name, msg->via.ft.name_length);

    if (image) {
        msg->via.ft.path = NULL;
    } else { // It's a file
        msg->via.ft.path = calloc(1, UTOX_FILE_NAME_LENGTH);
        if (!msg->via.ft.path) {
            LOG_FATAL_ERR(EXIT_MALLOC, "Messages", "Could not allocate memory for the file path.");
        }
    }

    message_add(m, msg);

    return msg;
}

bool message_log_to_disk(MESSAGES *m, MSG_HEADER *msg) {
    if (m->is_groupchat) {
        /* We don't support logging groupchats yet */
        return false;
    }

    if (!settings.logging_enabled) {
        return false;
    }

    FRIEND *f = get_friend(m->id);
    if (!f) {
        LOG_ERR("Messages", "Could not get friend with number: %u", m->id);
        return false;
    }

    if (f->skip_msg_logging) {
        return false;
    }

    LOG_FILE_MSG_HEADER header;
    memset(&header, 0, sizeof(header));

    switch (msg->msg_type) {
        case MSG_TYPE_TEXT:
        case MSG_TYPE_ACTION_TEXT:
        case MSG_TYPE_NOTICE: {
            size_t author_length;
            char   *author;
            if (msg->our_msg) {
                author_length = self.name_length;
                author        = self.name;
            } else {
                author_length = f->name_length;
                author        = f->name;
            }

            header.log_version   = LOGFILE_SAVE_VERSION;
            header.time          = msg->time;
            header.author_length = author_length;
            header.msg_length    = msg->via.txt.length;
            header.author        = msg->our_msg;
            header.receipt       = !!msg->receipt_time; // bool only
            header.msg_type      = msg->msg_type;

            size_t length = sizeof(header) + msg->via.txt.length + author_length + 1; /* extra \n char*/
            uint8_t *data = calloc(1, length);
            if (!data) {
                LOG_FATAL_ERR(EXIT_MALLOC, "Messages", "Can't calloc for chat logging data. size:%lu", length);
            }
            memcpy(data, &header, sizeof(header));
            memcpy(data + sizeof(header), author, author_length);
            memcpy(data + sizeof(header) + author_length, msg->via.txt.msg, msg->via.txt.length);
            strcpy2(data + length - 1, "\n");

            msg->disk_offset = utox_save_chatlog(f->id_str, data, length);

            free(data);
            return true;
        }
        default: {
            LOG_NOTE("Messages", "uTox Logging:\tUnsupported message type %i", msg->msg_type);
        }
    }
    return false;
}

bool messages_read_from_log(uint32_t friend_number) {
    size_t    actual_count = 0;

    FRIEND *f = get_friend(friend_number);
    if (!f) {
        LOG_ERR("Messages", "Could not get friend with number: %u", friend_number);
        return false;
    }

    MSG_HEADER **data = utox_load_chatlog(f->id_str, &actual_count, UTOX_MAX_BACKLOG_MESSAGES, 0);
    if (!data) {
        if (actual_count > 0) {
            LOG_ERR("Messages", "uTox Logging:\tFound chat log entries, but couldn't get any data. This is a problem.");
        }
        return false;
    }

    MSG_HEADER **p = data;
    MSG_HEADER *msg;
    time_t last = 0;
    while (actual_count--) {
        msg = *p++;
        if (!msg) {
            continue;
        }

        if (msg_add_day_notice(&f->msg, last, msg->time)) {
            last = msg->time;
        }
        message_add(&f->msg, msg);
    }

    free(data);
    return true;
}

void messages_send_from_queue(MESSAGES *m, uint32_t friend_number) {
    uint32_t start    = m->number;
    uint8_t  seek_num = 3; /* this magic number is the number of messages we'll skip looking for the first unsent */

    pthread_mutex_lock(&messages_lock);

    int queue_count = 0;
    /* seek back to find first queued message
     * I hate this nest too, but it's readable */
    while (start) {
        --start;

        if (++queue_count > 25) {
            break;
        }

        if (m->data[start]) {
            MSG_HEADER *msg = m->data[start];
            if (msg->msg_type == MSG_TYPE_TEXT || msg->msg_type == MSG_TYPE_ACTION_TEXT) {
                if (msg->our_msg) {
                    if (msg->receipt_time) {
                        if (!seek_num--) {
                            break;
                        }
                    }
                }
            }
        }
    }

    int sent_count = 0;
    /* start sending messages, hopefully in order */
    while (start < m->number && sent_count <= 25) {
        if (m->data[start]) {
            MSG_HEADER *msg = m->data[start];
            if (msg->msg_type == MSG_TYPE_TEXT || msg->msg_type == MSG_TYPE_ACTION_TEXT) {
                if (msg->our_msg && !msg->receipt_time) {
                    postmessage_toxcore((msg->msg_type == MSG_TYPE_TEXT ? TOX_SEND_MESSAGE : TOX_SEND_ACTION),
                                        friend_number, msg->via.txt.length, msg);
                    ++sent_count;
                }
            }
        }
        ++start;
    }
    pthread_mutex_unlock(&messages_lock);
}

void messages_clear_receipt(MESSAGES *m, uint32_t receipt_number) {
    pthread_mutex_lock(&messages_lock);

    uint32_t start = m->number;
    while (start--) {
        if (!m->data[start]) {
            continue;
        }

        MSG_HEADER *msg = m->data[start];
        if (msg->msg_type != MSG_TYPE_TEXT &&
            msg->msg_type != MSG_TYPE_ACTION_TEXT) {
            continue;
        }

        if (msg->receipt != receipt_number) {
            continue;
        }

        msg->receipt = -1;
        time(&msg->receipt_time);

        LOG_FILE_MSG_HEADER header;
        memset(&header, 0, sizeof(header));

        header.log_version   = LOGFILE_SAVE_VERSION;
        header.time          = msg->time;
        header.author_length = msg->via.txt.author_length;
        header.msg_length    = msg->via.txt.length;
        header.author        = 1;
        header.receipt       = 1;
        header.msg_type      = msg->msg_type;

        size_t length = sizeof(header);
        uint8_t *data = calloc(1, length);
        if (!data) {
            LOG_FATAL_ERR(EXIT_MALLOC, "Messages", "Couldn't allocate memory for message.");
        }
        memcpy(data, &header, length);

        char *hex = get_friend(m->id)->id_str;
        if (msg->disk_offset) {
            LOG_TRACE("Messages", "Updating message -> disk_offset is %lu" , msg->disk_offset);
            utox_update_chatlog(hex, msg->disk_offset, data, length);
        } else if (msg->disk_offset == 0 && start <= 1 && receipt_number == 1) {
            /* This could get messy if receipt is 1, msg position is 0, and the offset is actually wrong,
             * But I couldn't come up with any other way to verify the rare case of a bad offset
             * start <= 1 to offset for the day change notification                                    */
            LOG_TRACE("Messages", "Updating first message -> disk_offset is %lu" , msg->disk_offset);
            utox_update_chatlog(hex, msg->disk_offset, data, length);
        } else {
            LOG_ERR("Messages",
                    "Messages:\tUnable to update this message...\n"
                    "\t\tmsg->disk_offset %lu && m->number %u receipt_number %u \n",
                    msg->disk_offset, m->number, receipt_number);
        }
        free(data);

        postmessage_utox(FRIEND_MESSAGE_UPDATE, 0, 0, NULL); /* Used to redraw the screen */
        pthread_mutex_unlock(&messages_lock);
        return;
    }

    LOG_ERR("Messages", "Received a receipt for a message we don't have a record of. %u", receipt_number);
    pthread_mutex_unlock(&messages_lock);
}

static void messages_draw_timestamp(int x, int y, const time_t *time) {
    struct tm *ltime = localtime(time);

    char     timestr[9];
    uint16_t len;


    if (settings.use_long_time_msg) {
        snprintf(timestr, sizeof(timestr), "%.2u:%.2u:%.2u", ltime->tm_hour, ltime->tm_min, ltime->tm_sec);
        x -= textwidth("24:60:00", sizeof "24:60:00" - 1);
    } else {
        snprintf(timestr, sizeof(timestr), "%u:%.2u", ltime->tm_hour, ltime->tm_min);
        x -= textwidth("24:60", sizeof "24:60" - 1);
    }
    len = strnlen(timestr, sizeof(timestr) - 1);

    setcolor(COLOR_MAIN_TEXT_SUBTEXT);
    setfont(FONT_MISC);
    drawtext(x - MESSAGES_SPACING, y, timestr, len);
}

static void messages_draw_author(int x, int y, int w, char *name, uint32_t length, uint32_t color) {
    setcolor(color);
    setfont(FONT_TEXT);
    drawtextwidth_right(x, w, y, name, length);
}

static int messages_draw_text(const char *msg, size_t length, uint32_t msg_height, uint8_t msg_type, bool author,
                              bool receipt, uint16_t highlight_start, uint16_t highlight_end,
                              int x, int y, int w, int UNUSED(h))
{
    switch (msg_type) {
        case MSG_TYPE_TEXT: {
            if (author) {
                if (receipt) {
                    setcolor(COLOR_MSG_USER);
                } else {
                    setcolor(COLOR_MSG_USER_PEND);
                }
            } else {
                setcolor(COLOR_MSG_CONTACT);
            }
            break;
        }
        case MSG_TYPE_NOTICE:
        case MSG_TYPE_NOTICE_DAY_CHANGE:
        case MSG_TYPE_ACTION_TEXT: {
            setcolor(COLOR_MAIN_TEXT_ACTION);
            break;
        }
    }

    setfont(FONT_TEXT);

    int ny = utox_draw_text_multiline_within_box(x, y, w + x, MAIN_TOP, y + msg_height, font_small_lineheight, msg,
                                                 length, highlight_start, highlight_end, 0, 0, 1);

    if (ny < y || (uint32_t)(ny - y) + MESSAGES_SPACING != msg_height) {
        LOG_TRACE("Messages", "Text Draw Error:\ty %i | ny %i | mheight %u | width %i " , y, ny, msg_height, w);
    }

    return ny;
}

/* draws an inline image at rect (x,y,width,height)
 *  maxwidth is maximum width the image can take in
 *  zoom is whether the image is currently zoomed in
 *  position is the y position along the image the player has scrolled */
static int messages_draw_image(MSG_IMG *img, int x, int y, uint32_t maxwidth) {
    image_set_filter(img->image, FILTER_BILINEAR);

    if (!img->zoom && img->w > maxwidth) {
        image_set_scale(img->image, (double)maxwidth / img->w);

        draw_image(img->image, x, y, maxwidth, img->h * maxwidth / img->w, 0, 0);

        image_set_scale(img->image, 1.0);
    } else if (img->w > maxwidth) {
        draw_image(img->image, x, y, maxwidth, img->h, (int)((double)(img->w - maxwidth) * img->position), 0);
    } else {
        draw_image(img->image, x, y, img->w, img->h, 0, 0);
    }

    return (img->zoom || img->w <= maxwidth) ? img->h : img->h * maxwidth / img->w;
}

/* Draw macros added, to reduce future line edits. */
#define DRAW_FT_RECT(color) draw_rect_fill(dx, y, d_width, FILE_TRANSFER_BOX_HEIGHT, color)

#define DRAW_FT_PROG(color) draw_rect_fill(dx, y, prog_bar, FILE_TRANSFER_BOX_HEIGHT, color)

#define DRAW_FT_CAP(bg, fg)                                                                                 \
    do {                                                                                                    \
        drawalpha(BM_FT_CAP, dx - room_for_clip, y, BM_FT_CAP_WIDTH, BM_FTB_HEIGHT, bg);                    \
        drawalpha(BM_FILE, dx - room_for_clip + SCALE(4), y + SCALE(4), BM_FILE_WIDTH, BM_FILE_HEIGHT, fg); \
    } while (0)

/* Always first */
#define DRAW_FT_NO_BTN()                                                                        \
    do {                                                                                        \
        drawalpha(BM_FTB1, btnx, tbtn_bg_y, btn_bg_w, tbtn_bg_h,                                \
                  (mouse_left_btn ? COLOR_BTN_DANGER_BKGRND_HOVER : COLOR_BTN_SUCCESS_BKGRND)); \
        drawalpha(BM_NO, btnx + ((btn_bg_w - btnw) / 2), tbtn_y, btnw, btnh,                    \
                  (mouse_left_btn ? COLOR_BTN_DANGER_TEXT_HOVER : COLOR_BTN_DANGER_TEXT));      \
    } while (0)

/* Always last */
#define DRAW_FT_YES_BTN()                                                                           \
    do {                                                                                            \
        drawalpha(BM_FTB2, btnx + btn_bg_w + SCALE(2), tbtn_bg_y, btn_bg_w, tbtn_bg_h,              \
                  (mouse_rght_btn ? COLOR_BTN_SUCCESS_BKGRND_HOVER : COLOR_BTN_SUCCESS_BKGRND));    \
        drawalpha(BM_YES, btnx + btn_bg_w + SCALE(2) + ((btn_bg_w - btnw) / 2), tbtn_y, btnw, btnh, \
                  (mouse_rght_btn ? COLOR_BTN_SUCCESS_TEXT_HOVER : COLOR_BTN_SUCCESS_TEXT));        \
    } while (0)

#define DRAW_FT_PAUSE_BTN()                                                                           \
    do {                                                                                              \
        drawalpha(BM_FTB2, btnx + btn_bg_w + SCALE(2), tbtn_bg_y, btn_bg_w, tbtn_bg_h,                \
                  (mouse_rght_btn ? COLOR_BTN_SUCCESS_BKGRND_HOVER : COLOR_BTN_SUCCESS_BKGRND));      \
        drawalpha(BM_PAUSE, btnx + btn_bg_w + SCALE(2) + ((btn_bg_w - btnw) / 2), tbtn_y, btnw, btnh, \
                  (mouse_rght_btn ? COLOR_BTN_SUCCESS_TEXT_HOVER : COLOR_BTN_SUCCESS_TEXT));          \
    } while (0)

#define DRAW_FT_RESUME_BTN()                                                                           \
    do {                                                                                               \
        drawalpha(BM_FTB2, btnx + btn_bg_w + SCALE(2), tbtn_bg_y, btn_bg_w, tbtn_bg_h,                 \
                  (mouse_rght_btn ? COLOR_BTN_SUCCESS_BKGRND_HOVER : COLOR_BTN_SUCCESS_BKGRND));       \
        drawalpha(BM_RESUME, btnx + btn_bg_w + SCALE(2) + ((btn_bg_w - btnw) / 2), tbtn_y, btnw, btnh, \
                  (mouse_rght_btn ? COLOR_BTN_SUCCESS_TEXT_HOVER : COLOR_BTN_SUCCESS_TEXT));           \
    } while (0)

#define DRAW_FT_TEXT_RIGHT(str, len)                   \
    do {                                               \
        wbound -= (textwidth(str, len) + (SCALE(12))); \
        drawtext(wbound, y + SCALE(8), str, len);      \
    } while (0)

#define DRAW_FT_ALPH_RIGHT(bm, col)                     \
    do {                                                \
        wbound -= btnw + (SCALE(12));                   \
        drawalpha(bm, wbound, tbtn_y, btnw, btnh, col); \
    } while (0)

#define DRAWSTR_FT_RIGHT(t) DRAW_FT_TEXT_RIGHT(S(t), SLEN(t))


static void messages_draw_filetransfer(MESSAGES *m, MSG_FILE *file, uint32_t i, int x, int y, int w, int UNUSED(h)) {
    // Used in macros.
    int room_for_clip = BM_FT_CAP_WIDTH + SCALE(2);
    int dx            = x + SCALE(MESSAGES_X) + room_for_clip;
    int d_width       = w - SCALE(MESSAGES_X) - get_time_width() - room_for_clip;
    /* Mouse Positions */
    bool mo             = (m->cursor_over_msg == i);
    bool mouse_over     = (mo && m->cursor_over_position) ? 1 : 0;
    bool mouse_rght_btn = (mo && m->cursor_over_position == 2) ? 1 : 0;
    bool mouse_left_btn = (mo && m->cursor_over_position == 1) ? 1 : 0;

    /* Button Background */
    int btn_bg_w = BM_FTB_WIDTH;
    /* Button Background heights */
    int tbtn_bg_y = y;
    int tbtn_bg_h = BM_FTB_HEIGHT;
    /* Top button info */
    int btnx   = dx + d_width - (btn_bg_w * 2) - SCALE(2);
    int tbtn_y = y + SCALE(8);
    int btnw   = BM_FB_WIDTH;
    int btnh   = BM_FB_HEIGHT;

    long double file_percent = (double)file->progress / (double)file->size;
    if (file->progress > file->size) {
        file->progress = file->size;
        file_percent = 1.0;
    }

    char ft_text[file->name_length + 128];
    size_t ft_text_length;

    snprintf(ft_text, sizeof(ft_text), "%.*s ", (int)file->name_length, file->name);
    ft_text_length = strnlen(ft_text, sizeof(ft_text) - 1);
    ft_text_length += sprint_humanread_bytes(ft_text + ft_text_length, sizeof(ft_text) - ft_text_length, file->size);

    setfont(FONT_MISC);
    setcolor(COLOR_BKGRND_MAIN);

    int wbound = dx + d_width - SCALE(6);

    switch (file->file_status) {
        case FILE_TRANSFER_STATUS_NONE:
        case FILE_TRANSFER_STATUS_ACTIVE:
        case FILE_TRANSFER_STATUS_PAUSED_US:
        case FILE_TRANSFER_STATUS_PAUSED_BOTH:
        case FILE_TRANSFER_STATUS_PAUSED_THEM: {
            int ftb_allowance = (BM_FTB_WIDTH * 2) + (SCALE(4));
            d_width -= ftb_allowance;
            wbound -= ftb_allowance;
            break;
        }

        default: {
            // we'll round the corner even without buttons.
            d_width -= btn_bg_w;
            break;
        }
    }

    // progress rectangle
    uint32_t prog_bar = (file->size == 0) ? 0 : ((long double)d_width * file_percent);

    switch (file->file_status) {
        case FILE_TRANSFER_STATUS_COMPLETED: {
            /* If mouse over use hover color */
            uint32_t text       = mouse_over ? COLOR_BTN_SUCCESS_TEXT_HOVER : COLOR_BTN_SUCCESS_TEXT,
                     background = mouse_over ? COLOR_BTN_SUCCESS_BKGRND_HOVER : COLOR_BTN_SUCCESS_BKGRND;

            setcolor(text);
            DRAW_FT_CAP(background, text);
            DRAW_FT_RECT(background);
            drawalpha(BM_FTB2, dx + d_width, tbtn_bg_y, btn_bg_w, tbtn_bg_h, background);

            if (file->inline_png) {
                DRAWSTR_FT_RIGHT(CLICKTOSAVE);
            } else {
                DRAWSTR_FT_RIGHT(CLICKTOOPEN);
            }
            DRAW_FT_ALPH_RIGHT(BM_YES, text);
            break;
        }

        case FILE_TRANSFER_STATUS_KILLED: {
            setcolor(COLOR_BTN_DANGER_TEXT);
            DRAW_FT_CAP(COLOR_BTN_DANGER_BACKGROUND, COLOR_BTN_DANGER_TEXT);
            DRAW_FT_RECT(COLOR_BTN_DANGER_BACKGROUND);
            drawalpha(BM_FTB2, dx + d_width, tbtn_bg_y, btn_bg_w, tbtn_bg_h, COLOR_BTN_DANGER_BACKGROUND);

            DRAWSTR_FT_RIGHT(TRANSFER_CANCELLED);
            DRAW_FT_ALPH_RIGHT(BM_NO, COLOR_BTN_DANGER_TEXT);
            break;
        }

        case FILE_TRANSFER_STATUS_BROKEN: {
            setcolor(COLOR_BTN_DANGER_TEXT);
            DRAW_FT_CAP(COLOR_BTN_DANGER_BACKGROUND, COLOR_BTN_DANGER_TEXT);
            DRAW_FT_RECT(COLOR_BTN_DANGER_BACKGROUND);
            drawalpha(BM_FTB2, dx + d_width, tbtn_bg_y, btn_bg_w, tbtn_bg_h, COLOR_BTN_DANGER_BACKGROUND);

            DRAWSTR_FT_RIGHT(TRANSFER_BROKEN);
            DRAW_FT_ALPH_RIGHT(BM_NO, COLOR_BTN_DANGER_TEXT);
            break;
        }

        case FILE_TRANSFER_STATUS_NONE: {
            /* ↑ used for incoming transfers */
            setcolor(COLOR_BTN_DISABLED_TRANSFER);
            DRAW_FT_CAP(COLOR_BTN_DISABLED_BKGRND, COLOR_BTN_DISABLED_TRANSFER);
            DRAW_FT_RECT(COLOR_BTN_DISABLED_BKGRND);

            DRAW_FT_NO_BTN();
            DRAW_FT_YES_BTN();

            DRAW_FT_PROG(COLOR_BTN_DISABLED_FORGRND);
            break;
        }

        case FILE_TRANSFER_STATUS_ACTIVE: {
            setcolor(COLOR_BTN_INPROGRESS_TEXT);
            DRAW_FT_CAP(COLOR_BTN_INPROGRESS_BKGRND, COLOR_BTN_INPROGRESS_TEXT);
            DRAW_FT_RECT(COLOR_BTN_INPROGRESS_BKGRND);

            DRAW_FT_NO_BTN();
            DRAW_FT_PAUSE_BTN();

            char speed[32] = {0};
            size_t speed_len;
            speed_len = sprint_humanread_bytes(speed, sizeof(speed), file->speed);
            snprintf(speed + speed_len, sizeof(speed) - speed_len, "/s %lus",
                     file->speed ? (file->size - file->progress) / file->speed : 0);
            speed_len = strnlen(speed, sizeof(speed) - 1);

            DRAW_FT_TEXT_RIGHT(speed, speed_len);
            DRAW_FT_PROG(COLOR_BTN_INPROGRESS_FORGRND);
            break;
        }

        case FILE_TRANSFER_STATUS_PAUSED_US:
        case FILE_TRANSFER_STATUS_PAUSED_BOTH:
        case FILE_TRANSFER_STATUS_PAUSED_THEM: {
            setcolor(COLOR_BTN_DISABLED_TRANSFER);

            DRAW_FT_CAP(COLOR_BTN_DISABLED_BKGRND, COLOR_BTN_DISABLED_TRANSFER);
            DRAW_FT_RECT(COLOR_BTN_DISABLED_BKGRND);

            DRAW_FT_NO_BTN();

            if (file->file_status == FILE_TRANSFER_STATUS_PAUSED_BOTH
                || file->file_status == FILE_TRANSFER_STATUS_PAUSED_US) {
                /* Paused by at least us */
                DRAW_FT_RESUME_BTN();
            } else {
                /* Paused only by them */
                DRAW_FT_PAUSE_BTN();
            }

            DRAW_FT_PROG(COLOR_BTN_DISABLED_FORGRND);
            break;
        }
    }

    setfont(FONT_TEXT);
    drawtextrange(dx + SCALE(10), wbound - SCALE(10), y + SCALE(6), ft_text, ft_text_length);
}

/* This is a bit hacky, and likely would benefit from being moved to a whole new section including separating
 * group messages/functions from friend messages and functions from inside ui.c.
 *
 * Ideally group and friend messages wouldn't even need to know about each other.   */
static int messages_draw_group(MESSAGES *m, MSG_HEADER *msg, uint32_t curr_msg_i, int x, int y, int width, int height) {
    uint32_t h1 = UINT32_MAX, h2 = UINT32_MAX;
    if ((m->sel_start_msg > curr_msg_i && m->sel_end_msg > curr_msg_i)
        || (m->sel_start_msg < curr_msg_i && m->sel_end_msg < curr_msg_i)) {
        /* Out side the highlight area */
        h1 = UINT32_MAX;
        h2 = UINT32_MAX;
    } else {
        if (m->sel_start_msg < curr_msg_i) {
            h1 = 0;
        } else {
            h1 = m->sel_start_position;
        }

        if (m->sel_end_msg > curr_msg_i) {
            h2 = msg->via.grp.length;
        } else {
            h2 = m->sel_end_position;
        }
    }

    /* error check */
    if ((m->sel_start_msg == m->sel_end_msg && m->sel_start_position == m->sel_end_position) || h1 == h2) {
        h1 = UINT32_MAX;
        h2 = UINT32_MAX;
    }

    messages_draw_author(x, y, SCALE(MESSAGES_X - NAME_OFFSET), msg->via.grp.author, msg->via.grp.author_length, msg->via.grp.author_color);
    messages_draw_timestamp(x + width, y, &msg->time);
    return messages_draw_text(msg->via.grp.msg, msg->via.grp.length, msg->height, msg->msg_type, msg->our_msg, 1,
                              h1, h2, x + SCALE(MESSAGES_X), y, width - get_time_width() - SCALE(MESSAGES_X), height)
           + MESSAGES_SPACING;
}

static int messages_time_change(MESSAGES *m, MSG_HEADER *msg, size_t index, int x, int y, int width, int height) {
    uint32_t h1 = UINT32_MAX, h2 = UINT32_MAX;

    if ((m->sel_start_msg > index && m->sel_end_msg > index)
        || (m->sel_start_msg < index && m->sel_end_msg < index)) {
        /* Out side the highlight area */
        h1 = UINT32_MAX;
        h2 = UINT32_MAX;
    } else {
        if (m->sel_start_msg < index) {
            h1 = 0;
        } else {
            h1 = m->sel_start_position;
        }

        if (m->sel_end_msg > index) {
            h2 = msg->via.notice.length;
        } else {
            h2 = m->sel_end_position;
        }
    }

                /* error check */
    if ((m->sel_start_msg == m->sel_end_msg && m->sel_start_position == m->sel_end_position) || h1 == h2) {
        h1 = UINT32_MAX;
        h2 = UINT32_MAX;
    }

    /* text.c is super broken, so we have to be hacky here */
    if (h2 != msg->via.notice.length) {
        if (m->sel_end_msg != index) {
            h2 = msg->via.notice.length - h2;
        } else {
            h2 -= h1;
        }
    }

    return messages_draw_text(msg->via.notice.msg, msg->via.notice.length, msg->height,
                              msg->msg_type, msg->our_msg, msg->receipt_time,
                              h1, h2, x + SCALE(MESSAGES_X), y, width - get_time_width() - SCALE(MESSAGES_X), height);
}

/** Formats all messages from self and friends, and then call draw functions
 * to write them to the UI.
 *
 * accepts: messages struct *pointer, int x,y positions, int width,height
 */
void messages_draw(PANEL *panel, int x, int y, int width, int height) {
    if (width - SCALE(MESSAGES_X) - get_time_width() <= 0) {
        return;
    }

    pthread_mutex_lock(&messages_lock);

    MESSAGES *m = panel->object;
    // Do not draw author name next to every message
    uint8_t lastauthor = 0xFF;

    // Message iterator
    MSG_HEADER **p = m->data;
    uint32_t n = m->number;

    if (m->width != width) {
        m->width = width;
        messages_updateheight(m, width - SCALE(MESSAGES_X) + get_time_width());
        y -= scroll_gety(panel->content_scroll, height);
    }

    // Go through messages
    for (size_t curr_msg_i = 0; curr_msg_i != n; curr_msg_i++) {
        MSG_HEADER *msg = *p++;

        /* Decide if we should even bother drawing this message. */
        if (msg->height == 0) {
            /* Empty message */
            pthread_mutex_unlock(&messages_lock);
            return;
        } else if (y + msg->height <= (unsigned)SCALE(MAIN_TOP)) {
            /* message is exclusively above the viewing window */
            y += msg->height;
            continue;
        } else if (y >= height + SCALE(100)) { // NOTE: should not be constant 100
            /* Message is exclusively below the viewing window */
            break;
        }

        // Draw the names for groups or friends
        if (m->is_groupchat) {
            y = messages_draw_group(m, msg, curr_msg_i, x, y, width, height);
            continue;
        } else {
            bool draw_author = true;
            switch (msg->msg_type) {
                case MSG_TYPE_NULL: {
                    // This shouldn't happen.
                    LOG_ERR("Messages", "Invalid message type in messages_draw.");
                    break;
                }

                case MSG_TYPE_ACTION_TEXT: {
                    // Always draw name next to action message
                    lastauthor = ~0;
                    break;
                }

                case MSG_TYPE_TEXT:
                case MSG_TYPE_IMAGE:
                case MSG_TYPE_FILE: {
                    draw_author = true;
                    break;
                }

                case MSG_TYPE_NOTICE:
                case MSG_TYPE_NOTICE_DAY_CHANGE: {
                    draw_author = false;
                    break;
                }
            }

            if (draw_author) {
                if (msg->our_msg != lastauthor || y < SCALE(MAIN_TOP) + font_small_lineheight) {
                    int msg_y = y;

                    // If previous author label is invisible (i.e. above top side of the messages window)
                    // than clear its old place by drawing a rectangle with background colour.
                    // After that we are able to draw a new author label at the same place.
                    if (y < SCALE(MAIN_TOP) + font_small_lineheight) {
                        msg_y = SCALE(MAIN_TOP);

                        // MAIN_TOP + 1 because otherwise it cuts off one pixel from TOP FRAME somehow
                        draw_rect_fill(x, SCALE(MAIN_TOP) + 1, SCALE(MESSAGES_X), font_small_lineheight, COLOR_BKGRND_MAIN);
                    }

                    FRIEND *f = get_friend(m->id);
                    if (msg->our_msg) {
                        messages_draw_author(x, msg_y, SCALE(MESSAGES_X - NAME_OFFSET), self.name, self.name_length,
                                             COLOR_MAIN_TEXT_SUBTEXT);
                    } else if (f->alias) {
                        messages_draw_author(x, msg_y, SCALE(MESSAGES_X - NAME_OFFSET), f->alias, f->alias_length,
                                             COLOR_MAIN_TEXT_CHAT);
                    } else {
                        messages_draw_author(x, msg_y, SCALE(MESSAGES_X - NAME_OFFSET), f->name, f->name_length,
                                             COLOR_MAIN_TEXT_CHAT);
                    }
                    lastauthor = msg->our_msg;
                }
            }
        }

        // Draw message contents
        switch (msg->msg_type) {
            case MSG_TYPE_NULL: {
                LOG_ERR("Messages", "Error msg type is null");
                break;
            }

            case MSG_TYPE_TEXT:
            case MSG_TYPE_ACTION_TEXT:
            case MSG_TYPE_NOTICE: {
                // Draw timestamps
                messages_draw_timestamp(x + width, y, &msg->time);
                y = messages_time_change(m, msg, curr_msg_i, x, y, width, height);
                break;
            }
            case MSG_TYPE_NOTICE_DAY_CHANGE: {
                y = messages_time_change(m, msg, curr_msg_i, x, y, width, height);
                break;
            }

            // Draw image
            case MSG_TYPE_IMAGE: {
                y += messages_draw_image(&msg->via.img, x + SCALE(MESSAGES_X), y, width - SCALE(MESSAGES_X) - get_time_width());
                break;
            }

            // Draw file transfer
            case MSG_TYPE_FILE: {
                messages_draw_filetransfer(m, &msg->via.ft, curr_msg_i, x, y, width, height);
                y += FILE_TRANSFER_BOX_HEIGHT;
                break;
            }
        }

        y += MESSAGES_SPACING;
    }

    pthread_mutex_unlock(&messages_lock);
}

static bool messages_mmove_text(MESSAGES *m, int width, int mx, int my, int dy, char *message, uint32_t msg_height,
                                uint16_t msg_length)
{
    if (mx < width - get_time_width()) {
        cursor = CURSOR_TEXT;
    }

    m->cursor_over_position = hittextmultiline(mx - SCALE(MESSAGES_X), width - SCALE(MESSAGES_X) - get_time_width(), (my < 0 ? 0 : my),
                                               msg_height, font_small_lineheight, message, msg_length, 1);

    if (my < 0 || my >= dy || mx < SCALE(MESSAGES_X) || m->cursor_over_position == msg_length) {
        m->cursor_over_uri = UINT32_MAX;
        return 0;
    }

    bool prev_cursor_down_uri = m->cursor_down_uri;

    if (m->cursor_over_uri != UINT32_MAX) {
        m->cursor_down_uri = 0;
        m->cursor_over_uri = UINT32_MAX;
    }

    /* Seek back to the last word/line break */
    char *str = message + m->cursor_over_position;
    while (str != message) {
        str--;
        if (*str == ' ' || *str == '\n') {
            str++;
            break;
        }
    }

    /* Check if it's a URI we handle TODO: handle moar! */
    char *end = message + msg_length;
    while (str != end && *str != ' ' && *str != '\n') {
        if (str == message || *(str - 1) == '\n' || *(str - 1) == ' ') {
            if (m->cursor_over_uri == UINT32_MAX && end - str >= 7 && (strncmp(str, "http://", 7) == 0)) {
                cursor             = CURSOR_HAND;
                m->cursor_over_uri = str - message;
            } else if (m->cursor_over_uri == UINT32_MAX && end - str >= 8 && (strncmp(str, "https://", 8) == 0)) {
                cursor             = CURSOR_HAND;
                m->cursor_over_uri = str - message;
            } else if (m->cursor_over_uri == UINT32_MAX && end - str >= 4 && (strncmp(str, "tox:", 4) == 0)) {
                cursor             = CURSOR_HAND;
                m->cursor_over_uri = str - message;
            }
        }
        str++;
    }

    if (m->cursor_over_uri != UINT32_MAX) {
        m->urllen          = (str - message) - m->cursor_over_uri;
        m->cursor_down_uri = prev_cursor_down_uri;
        LOG_TRACE("Messages", "urllen %u" , m->urllen);
    }

    return 0;
}

static bool messages_mmove_image(MSG_IMG *image, uint32_t max_width, int mx, int my) {
    if (image->w > max_width) {
        mx -= SCALE(MESSAGES_X);
        int w = image->w > max_width ? max_width : image->w;
        int h = (image->zoom || image->w <= max_width) ? image->h : image->h * max_width / image->w;

        if (mx >= 0 && my >= 0 && mx < w && my < h) {
            cursor = CURSOR_ZOOM_IN + image->zoom;
            return 1;
        }
    }
    return 0;
}

static uint8_t messages_mmove_filetransfer(int mx, int my, int width) {
    mx -= SCALE(10); /* Why? */
    if (mx >= 0 && mx < width && my >= 0 && my < FILE_TRANSFER_BOX_HEIGHT) {
        if (mx >= width - get_time_width() - (BM_FTB_WIDTH * 2) - SCALE(2) - SCROLL_WIDTH
            && mx <= width - get_time_width() - SCROLL_WIDTH) {
            if (mx >= width - get_time_width() - BM_FTB_WIDTH - SCROLL_WIDTH) {
                // mouse is over the right button (pause / accept)
                return 2;
            } else {
                // mouse is over the left button  (cancel)
                return 1;
            }
        }
        return 3;
    }
    return 0;
}

bool messages_mmove(PANEL *panel, int UNUSED(px), int UNUSED(py), int width, int UNUSED(height), int mx, int my, int dx,
                    int UNUSED(dy))
{
    MESSAGES *m = panel->object;

    m->cursor_over_time = inrect(mx, my, width - get_time_width(), 0, get_time_width(), m->height);

    if (m->cursor_down_msg < m->number) {
        uint32_t maxwidth = width - SCALE(MESSAGES_X) - get_time_width();
        MSG_HEADER *msg = m->data[m->cursor_down_msg];
        if ((msg->msg_type == MSG_TYPE_IMAGE) && (msg->via.img.w > maxwidth)) {
            msg->via.img.position -= (double)dx / (double)(msg->via.img.w - maxwidth);
            if (msg->via.img.position > 1.0) {
                msg->via.img.position = 1.0;
            } else if (msg->via.img.position < 0.0) {
                msg->via.img.position = 0.0;
            }
            cursor = CURSOR_ZOOM_OUT;
            return true;
        }
    }

    if (mx < 0 || my < 0 || my > m->height) {
        if (m->cursor_over_msg != UINT32_MAX) {
            m->cursor_over_msg = UINT32_MAX;
            return true;
        }
        return false;
    }

    setfont(FONT_TEXT);

    MSG_HEADER **p = m->data;
    uint32_t i = 0;
    bool     need_redraw = false;

    while (i < m->number) {
        MSG_HEADER *msg = *p++;

        int dy = msg->height; /* dy is the wrong name here, you should change it! */

        if (my >= 0 && my < dy) {
            m->cursor_over_msg = i;

            switch (msg->msg_type) {
                case MSG_TYPE_NULL: {
                    LOG_ERR("Messages", "Invalid message type in messages_mmove.");
                    return false;
                }

                case MSG_TYPE_TEXT:
                case MSG_TYPE_ACTION_TEXT:
                case MSG_TYPE_NOTICE:
                case MSG_TYPE_NOTICE_DAY_CHANGE: {
                    if (m->is_groupchat) {
                        messages_mmove_text(m, width, mx, my, dy, msg->via.grp.msg,
                                            msg->height, msg->via.grp.length);
                    } else {
                        messages_mmove_text(m, width, mx, my, dy, msg->via.txt.msg,
                                            msg->height, msg->via.txt.length);
                    }
                    if (m->cursor_down_msg != UINT32_MAX
                        && (m->cursor_down_position != m->cursor_over_position
                            || m->cursor_down_msg != m->cursor_over_msg))
                    {
                        m->selecting_text = 1;
                    }
                    break;
                }

                case MSG_TYPE_IMAGE: {
                    m->cursor_over_position = messages_mmove_image(&msg->via.img, (width - SCALE(MESSAGES_X) - get_time_width()), mx, my);
                    break;
                }

                case MSG_TYPE_FILE: {
                    m->cursor_over_position = messages_mmove_filetransfer(mx, my, width);
                    if (m->cursor_over_position) {
                        need_redraw = true;
                    }
                    break;
                }
            }

            if (i != m->cursor_over_msg && m->cursor_over_msg != UINT32_MAX
                && (msg->msg_type == MSG_TYPE_FILE || m->data[m->cursor_over_msg]->msg_type == MSG_TYPE_FILE)) {
                need_redraw = true; // Redraw file on hover-in/out.
            }

            if (m->selecting_text) {
                need_redraw = true;

                if (m->cursor_down_msg != m->cursor_over_msg || m->cursor_down_position <= m->cursor_over_position) {
                    m->sel_start_position = m->cursor_down_position;
                    m->sel_end_position   = m->cursor_over_position;
                } else {
                    m->sel_start_position = m->cursor_over_position;
                    m->sel_end_position   = m->cursor_down_position;
                }

                if (m->cursor_down_msg <= m->cursor_over_msg) {
                    m->sel_start_msg = m->cursor_down_msg;
                    m->sel_end_msg   = m->cursor_over_msg;
                } else {
                    m->sel_start_msg      = m->cursor_over_msg;
                    m->sel_end_msg        = m->cursor_down_msg;
                    m->sel_start_position = m->cursor_over_position;
                    m->sel_end_position   = m->cursor_down_position;
                }
            }

            return need_redraw;
        }

        my -= dy;

        i++;
    }

    return false;
}

bool messages_mdown(PANEL *panel) {
    MESSAGES *m        = panel->object;
    m->cursor_down_msg = UINT32_MAX;

    if (m->cursor_over_msg != UINT32_MAX) {
        MSG_HEADER *msg = m->data[m->cursor_over_msg];
        switch (msg->msg_type) {
            case MSG_TYPE_NULL: {
                LOG_ERR("Messages", "Invalid message type in messages_mdown.");
                return false;
            }

            case MSG_TYPE_TEXT:
            case MSG_TYPE_ACTION_TEXT:
            case MSG_TYPE_NOTICE:
            case MSG_TYPE_NOTICE_DAY_CHANGE: {
                if (m->cursor_over_uri != UINT32_MAX) {
                    m->cursor_down_uri = m->cursor_over_uri;
                    LOG_TRACE("Messages", "mdn dURI %u, oURI %u" , m->cursor_down_uri, m->cursor_over_uri);
                }

                m->sel_start_msg = m->sel_end_msg = m->cursor_down_msg = m->cursor_over_msg;
                m->sel_start_position = m->sel_end_position = m->cursor_down_position = m->cursor_over_position;
                break;
            }

            case MSG_TYPE_IMAGE: {
                if (m->cursor_over_position) {
                    if (!msg->via.img.zoom) {
                        msg->via.img.zoom = 1;
                        message_updateheight(m, msg);
                    } else {
                        m->cursor_down_msg = m->cursor_over_msg;
                    }
                }
                break;
            }

            case MSG_TYPE_FILE: {
                if (m->cursor_over_position == 0) {
                    break;
                }

                FRIEND *f = get_friend(m->id);
                FILE_TRANSFER *ft;
                uint32_t ft_number = msg->via.ft.file_number;
                if (ft_number >= (1 << 16)) {
                    ft = &f->ft_incoming[(ft_number >> 16) - 1]; // TODO, abstraction needed
                } else {
                    ft = &f->ft_outgoing[ft_number]; // TODO, abstraction needed
                }

                if (msg->via.ft.file_status == FILE_TRANSFER_STATUS_COMPLETED) {
                    if (m->cursor_over_position) {
                        if (msg->via.ft.inline_png) {
                            file_save_inline_image_png(msg);
                        } else {
                            openurl((char *)msg->via.ft.path);
                        }
                    }

                    return true;
                }

                if (m->cursor_over_position == 2) { // Right button, should be accept/pause/resume
                    if (!msg->our_msg && msg->via.ft.file_status == FILE_TRANSFER_STATUS_NONE) {
                        native_select_dir_ft(m->id, msg->via.ft.file_number, ft);
                        return true;
                    }

                    if (msg->via.ft.file_status == FILE_TRANSFER_STATUS_ACTIVE) {
                        postmessage_toxcore(TOX_FILE_PAUSE, m->id, msg->via.ft.file_number, ft);
                    } else {
                        postmessage_toxcore(TOX_FILE_RESUME, m->id, msg->via.ft.file_number, ft);
                    }
                } else if (m->cursor_over_position == 1) { // Should be cancel
                    postmessage_toxcore(TOX_FILE_CANCEL, m->id, msg->via.ft.file_number, ft);
                }

                return true;
            }
        }

        return true;
    } else if (m->sel_start_msg != m->sel_end_msg || m->sel_start_position != m->sel_end_position) {
        m->sel_start_msg      = 0;
        m->sel_end_msg        = 0;
        m->sel_start_position = 0;
        m->sel_end_position   = 0;

        return true;
    }

    return false;
}

bool messages_dclick(PANEL *panel, bool triclick) {
    MESSAGES *m = panel->object;

    if (m->cursor_over_time) {
        settings.use_long_time_msg = !settings.use_long_time_msg;
        return true;
    }

    if (m->cursor_over_msg == UINT32_MAX) {
        return false;
    }

    MSG_HEADER *msg = m->data[m->cursor_over_msg];
    switch (msg->msg_type) {
        case MSG_TYPE_NULL: {
            LOG_ERR("Messages", "Invalid message type in messages_dclick.");
            return false;
        }

        case MSG_TYPE_FILE:
        case MSG_TYPE_NOTICE:
        case MSG_TYPE_NOTICE_DAY_CHANGE: {
            return false;
        }

        case MSG_TYPE_TEXT:
        case MSG_TYPE_ACTION_TEXT: {
            m->sel_start_msg = m->sel_end_msg = m->cursor_over_msg;

            uint16_t i = m->cursor_over_position;
            while (i != 0 && msg->via.txt.msg[i - 1] != '\n'
                   /*  If it's a dclick, also set ' ' as boundary, else do nothing. */
                   && (!triclick ? (msg->via.txt.msg[i - 1] != ' ') : 1)) {
                i -= utf8_unlen(msg->via.txt.msg + i);
            }
            m->sel_start_position = i;
            i = m->cursor_over_position;
            while (i != msg->via.txt.length && msg->via.txt.msg[i] != '\n'
                   /*  If it's a dclick, also set ' ' as boundary, else do nothing. */
                   && (!triclick ? (msg->via.txt.msg[i] != ' ') : 1)) {
                i += utf8_len(msg->via.txt.msg + i);
            }
            m->sel_end_position = i;

            uint32_t diff = m->sel_end_position - m->sel_start_position;
            setselection(msg->via.txt.msg + m->sel_start_position, diff);

            return true;
        }

        case MSG_TYPE_IMAGE: {
            if (m->cursor_over_position) {
                if (msg->via.img.zoom) {
                    msg->via.img.zoom = 0;
                    message_updateheight(m, msg);
                }
            }

            return true;
        }
    }

    return false;
}

static void contextmenu_messages_onselect(uint8_t i) {
    copy(!!i); /* if not 0 force a 1 */
}

bool messages_mright(PANEL *panel) {
    const MESSAGES *m = panel->object;

    if (m->cursor_over_msg == UINT32_MAX) {
        return false;
    }

    const MSG_HEADER *msg = m->data[m->cursor_over_msg];

    switch (msg->msg_type) {
        case MSG_TYPE_NULL: {
            LOG_ERR("Messages", "Invalid message type in messages_mdown.");
            return false;
        }

        case MSG_TYPE_TEXT:
        case MSG_TYPE_ACTION_TEXT: {
            static UTOX_I18N_STR menu_copy[] = { STR_COPY, STR_COPY_WITH_NAMES };
            contextmenu_new(COUNTOF(menu_copy), menu_copy, contextmenu_messages_onselect);
            return true;
        }

        case MSG_TYPE_NOTICE:
        case MSG_TYPE_NOTICE_DAY_CHANGE:
        case MSG_TYPE_IMAGE:
        case MSG_TYPE_FILE: {
            return false;
        }
    }

    LOG_FATAL_ERR(EXIT_FAILURE, "Messages", "Congratulations, you've reached dead code. Please report this.");
}

bool messages_mwheel(PANEL *UNUSED(panel), int UNUSED(height), double UNUSED(d), bool UNUSED(smooth)) {
    return false;
}

bool messages_mup(PANEL *panel) {
    MESSAGES *m = panel->object;

    if (!m->data) {
        return false;
    }

    if (m->cursor_over_msg != UINT32_MAX) {
        MSG_HEADER *msg = m->data[m->cursor_over_msg];
        if (msg->msg_type == MSG_TYPE_TEXT) {
            if (m->cursor_over_uri != UINT32_MAX
                && m->cursor_down_uri == m->cursor_over_uri
                && m->cursor_over_position >= m->cursor_over_uri
                && m->cursor_over_position <= m->cursor_over_uri + m->urllen - 1 /* - 1 Don't open on white space */
                && !m->selecting_text)
            {
                LOG_TRACE("Messages", "mup dURI %u, oURI %u" , m->cursor_down_uri, m->cursor_over_uri);
                char url[m->urllen + 1];
                memcpy(url, msg->via.txt.msg + m->cursor_over_uri, m->urllen * sizeof(char));
                url[m->urllen] = 0;
                openurl(url);
                m->cursor_down_uri = 0;
            }
        }
    }

    if (m->selecting_text) {
        const uint32_t max_selection_size = UINT16_MAX + 1;
        char *sel = calloc(1, max_selection_size);
        if (!sel) {
            LOG_FATAL_ERR(EXIT_MALLOC, "Messages", "Couldn't allocate memory for selection.");
        }
        setselection(sel, messages_selection(panel, sel, max_selection_size, 0));
        free(sel);

        m->selecting_text = 0;
    }

    m->cursor_down_msg = UINT32_MAX;

    return false;
}

bool messages_mleave(PANEL *UNUSED(m)) {
    return false;
}

int messages_selection(PANEL *panel, char *buffer, uint32_t len, bool names) {
    MESSAGES *m = panel->object;

    if (m->number == 0) {
        return 0;
    }

    uint32_t i = m->sel_start_msg, n = m->sel_end_msg + 1;
    MSG_HEADER **dp = &m->data[i];

    char *p = buffer;

    while (i != UINT32_MAX && i != n) {
        const MSG_HEADER *msg = *dp++;

        if (names && (i != m->sel_start_msg || m->sel_start_position == 0)) {
            if (m->is_groupchat) {
                memcpy(p, msg->via.grp.author, msg->via.grp.author_length);
                p += msg->via.grp.author_length;
                len -= msg->via.grp.author_length;
            } else {
                const FRIEND *f = get_friend(m->id);

                if (!msg->our_msg) {
                    if (len <= f->name_length) {
                        break;
                    }

                    memcpy(p, f->name, f->name_length);
                    p += f->name_length;
                    len -= f->name_length;
                } else {
                    if (len <= self.name_length) {
                        break;
                    }

                    memcpy(p, self.name, self.name_length);
                    p += self.name_length;
                    len -= self.name_length;
                }
            }

            if (len <= 2) {
                break;
            }

            strcpy2(p, ": ");
            p += 2;
            len -= 2;
        }

        switch (msg->msg_type) {
            case MSG_TYPE_NULL: {
                LOG_ERR("Messages", "Invalid message type in messages_selection.");
                return 0;
            }

            case MSG_TYPE_TEXT:
            case MSG_TYPE_ACTION_TEXT: {
                char *data;
                uint16_t length;
                if (i == m->sel_start_msg) {
                    if (i == m->sel_end_msg) {
                        data   = msg->via.txt.msg + m->sel_start_position;
                        length = m->sel_end_position - m->sel_start_position;
                    } else {
                        data   = msg->via.txt.msg + m->sel_start_position;
                        length = msg->via.txt.length - m->sel_start_position;
                    }
                } else if (i == m->sel_end_msg) {
                    data   = msg->via.txt.msg;
                    length = m->sel_end_position;
                } else {
                    data   = msg->via.txt.msg;
                    length = msg->via.txt.length;
                }

                if (len <= length) {
                    *p = 0;
                    return p - buffer;
                }

                memcpy(p, data, length);
                p += length;
                len -= length;
                break;
            }

            case MSG_TYPE_IMAGE:
            case MSG_TYPE_FILE:
            case MSG_TYPE_NOTICE:
            case MSG_TYPE_NOTICE_DAY_CHANGE: {
                // Do nothing.
                break;
            }
        }

        i++;

        if (i != n) {
            #ifdef __WIN32__
                if (len <= 2) {
                    break;
                }
                *p++ = '\r';
                *p++ = '\n';
                len -= 2;
            #else
                if (len <= 1) {
                    break;
                }
                *p++ = '\n';
                len--;
            #endif
        }
    }

    return p - buffer;
}

void messages_updateheight(MESSAGES *m, int width) {
    if (!m->data || !width) {
        return;
    }

    setfont(FONT_TEXT);

    uint32_t height = 0;
    for (uint32_t i = 0; i < m->number; ++i) {
        height += message_setheight(m, (void *)m->data[i]);
    }
    m->panel.content_scroll->content_height = m->height = height;
}

bool messages_char(uint32_t ch) {
    MESSAGES *m;

    if (flist_get_sel_friend()) {
        m = messages_friend.object;
    } else if (flist_get_sel_group()) {
        m = messages_group.object;
    } else {
        LOG_TRACE("Messages", "Can't type to nowhere");
        return false;
    }

    switch (ch) {
        // TODO: probably need to fix this section :< m->panel.content scroll is likely to be wrong.
        case KEY_PAGEUP: {
            SCROLLABLE *scroll = m->panel.content_scroll;
            scroll->d -= 0.25; // TODO: Change to a full chat-screen height.
            if (scroll->d < 0.0) {
                scroll->d = 0.0;
            }

            return true;
        }

        case KEY_PAGEDOWN: {
            SCROLLABLE *scroll = m->panel.content_scroll;
            scroll->d += 0.25; // TODO: Change to a full chat-screen height.
            if (scroll->d > 1.0) {
                scroll->d = 1.0;
            }

            return true;
        }

        case KEY_HOME: {
            m->panel.content_scroll->d = 0.0;
            return true;
        }

        case KEY_END: {
            m->panel.content_scroll->d = 1.0;
            return true;
        }
    }

    return false;
}

void messages_init(MESSAGES *m, uint32_t friend_number) {
    if (m->data) {
        messages_clear_all(m);
    }

    pthread_mutex_lock(&messages_lock);

    memset(m, 0, sizeof(*m));

    m->id    = friend_number;
    m->extra = 20;
    m->data  = calloc(20, sizeof(void *));
    if (!m->data) {
        LOG_FATAL_ERR(EXIT_MALLOC, "Messages", "\n\n\nFATAL ERROR TRYING TO CALLOC FOR MESSAGES.\nTHIS IS A BUG, PLEASE REPORT!\n\n\n");
    }

    pthread_mutex_unlock(&messages_lock);
}

void message_free(MSG_HEADER *msg) {
    // The group messages are free()d in groups.c (group_free(GROUPCHAT *g))
    switch (msg->msg_type) {
        case MSG_TYPE_NULL: {
            LOG_ERR("Messages", "Invalid message type in message_free.");
            break;
        }

        case MSG_TYPE_IMAGE: {
            image_free(msg->via.img.image);
            break;
        }

        case MSG_TYPE_FILE: {
            free(msg->via.ft.name);
            free(msg->via.ft.path);
            free(msg->via.ft.data);
            break;
        }

        case MSG_TYPE_NOTICE_DAY_CHANGE: {
            free(msg->via.notice_day.msg);
            break;
        }

        case MSG_TYPE_TEXT: {
            free(msg->via.txt.msg);
            break;
        }

        case MSG_TYPE_ACTION_TEXT: {
            free(msg->via.action.msg);
            break;
        }

        case MSG_TYPE_NOTICE: {
            free(msg->via.notice.msg);
            break;
        }
    }

    free(msg);
}

void messages_clear_all(MESSAGES *m) {
    pthread_mutex_lock(&messages_lock);

    for (uint32_t i = 0; i < m->number; i++) {
        message_free(m->data[i]);
    }

    free(m->data);
    m->data   = NULL;
    m->number = 0;
    m->extra  = 0;
    m->height = 0;

    m->sel_start_msg = m->sel_end_msg = m->sel_start_position = m->sel_end_position = 0;

    pthread_mutex_unlock(&messages_lock);
}
