import * as message_scroll from "./message_scroll";

function max_id_for_messages(messages) {
    let max_id = 0;
    for (const msg of messages) {
        max_id = Math.max(max_id, msg.id);
    }
    return max_id;
}

export class FetchStatus {
    // The FetchStatus object tracks the state of a
    // message_list_data object, whether rendered in the DOM or not,
    // and is the source of truth for whether the message_list_data
    // object has the complete history of the view or whether more
    // messages should be loaded when scrolling to the top or bottom
    // of the message feed.
    _loading_older = false;
    _loading_newer = false;
    _found_oldest = false;
    _found_newest = false;
    _history_limited = false;

    // Tracks the highest message ID that we know exist in this view,
    // but are not within the contiguous range of messages we have
    // received from the server.  Used to correctly handle a rare race
    // condition where a newly sent message races with fetching a
    // group of messages that would lead to found_newest being set
    // (described in detail below).
    _expected_max_message_id = 0;

    start_older_batch(opts) {
        this._loading_older = true;
        if (opts.update_loading_indicator) {
            message_scroll.show_loading_older();
        }
    }

    finish_older_batch(opts) {
        this._loading_older = false;
        this._found_oldest = opts.found_oldest;
        this._history_limited = opts.history_limited;
        if (opts.update_loading_indicator) {
            message_scroll.hide_loading_older();
        }
    }

    can_load_older_messages() {
        return !this._loading_older && !this._found_oldest;
    }

    has_found_oldest() {
        return this._found_oldest;
    }

    history_limited() {
        return this._history_limited;
    }

    start_newer_batch(opts) {
        this._loading_newer = true;
        if (opts.update_loading_indicator) {
            message_scroll.show_loading_newer();
        }
    }

    finish_newer_batch(messages, opts) {
        // Returns true if and only if the caller needs to trigger an
        // additional fetch due to the race described below.
        const found_max_message_id = max_id_for_messages(messages);
        this._loading_newer = false;
        this._found_newest = opts.found_newest;
        if (opts.update_loading_indicator) {
            message_scroll.hide_loading_newer();
        }
        if (this._found_newest && this._expected_max_message_id > found_max_message_id) {
            // This expected_max_message_id logic is designed to
            // resolve a subtle race condition involving newly sent
            // messages in a view that does not display the currently
            // latest messages.
            //
            // When a new message arrives matching the current view
            // and found_newest is false, we cannot add the message to
            // the view in-order without creating invalid output
            // (where two messages are display adjacent but might be
            // weeks and hundreds of messages apart in actuality).
            //
            // So we have to discard those messages.  Usually, this is
            // fine; the client will receive those when the user
            // scrolls to the bottom of the page, triggering another
            // fetch.  With that solution, a rare race is still possible,
            // with this sequence:
            //
            // 1. Client initiates GET /messages to fetch the last
            //    batch of messages in this view.  The server
            //    completes the database access and and starts sending
            //    the response with found_newest=true.
            // 1. A new message is sent matching the view, the event reaches
            //    the client.  We discard the message because found_newest=false.
            // 1. The client receives the GET /messages response, and
            //    marks found_newest=true.  As a result, it believes is has
            //    the latest messages and won't fetch more, but is missing the
            //    recently sent message.
            //
            // To address this problem, we track the highest message
            // ID among messages that were discarded due to
            // fetch_status in expected_max_message_id.  If that is
            // higher than the highest ID returned in a GET /messages
            // response with found_newest=true, we know the above race
            // has happened and trigger an additional fetch.
            this._found_newest = false;

            // Resetting our tracked last message id is an important
            // circuit-breaker for cases where the message(s) that we
            // "know" exist were deleted or moved to another topic.
            this._expected_max_message_id = 0;
            return true;
        }
        return false;
    }

    can_load_newer_messages() {
        return !this._loading_newer && !this._found_newest;
    }

    has_found_newest() {
        return this._found_newest;
    }

    update_expected_max_message_id(messages) {
        this._expected_max_message_id = Math.max(
            this._expected_max_message_id,
            max_id_for_messages(messages),
        );
    }
}
