import EmberObject from "@ember/object";
import { cancel, later } from "@ember/runloop";
import { ajax } from "discourse/lib/ajax";
import discourseComputed from "discourse-common/utils/decorators";

// The durations chosen here determines the accuracy of the presence feature and
// is tied closely with the server side implementation. Decreasing the duration
// to increase the accuracy will come at the expense of having to more network
// calls to publish the client's state.
//
// Logic walk through of our heuristic implementation:
// - When client A is typing, a message is published every KEEP_ALIVE_DURATION_SECONDS.
// - Client B receives the message and stores each user in an array and marks
//   the user with a client-side timestamp of when the user was seen.
// - If client A continues to type, client B will continue to receive messages to
//   update the client-side timestamp of when client A was last seen.
// - If client A disconnects or becomes inactive, the state of client A will be
//   cleaned up on client B by a scheduler that runs every TIMER_INTERVAL_MILLISECONDS
export const KEEP_ALIVE_DURATION_SECONDS = 10;
const BUFFER_DURATION_SECONDS = KEEP_ALIVE_DURATION_SECONDS + 2;

const MESSAGE_BUS_LAST_ID = 0;
const TIMER_INTERVAL_MILLISECONDS = 2000;

export const REPLYING = "replying";
export const EDITING = "editing";
export const CLOSED = "closed";

export const TOPIC_TYPE = "topic";
export const COMPOSER_TYPE = "composer";

const Presence = EmberObject.extend({
  users: null,
  editingUsers: null,
  subscribers: null,
  topicId: null,
  currentUser: null,
  messageBus: null,
  siteSettings: null,

  init() {
    this._super(...arguments);

    this.setProperties({
      users: [],
      editingUsers: [],
      subscribers: new Set(),
    });
  },

  subscribe(type) {
    if (this.subscribers.size === 0) {
      this.messageBus.subscribe(
        this.channel,
        (message) => {
          const { user, state } = message;
          if (this.get("currentUser.id") === user.id) return;

          switch (state) {
            case REPLYING:
              this._appendUser(this.users, user);
              break;
            case EDITING:
              this._appendUser(this.editingUsers, user, {
                post_id: parseInt(message.post_id, 10),
              });
              break;
            case CLOSED:
              this._removeUser(user);
              break;
          }
        },
        MESSAGE_BUS_LAST_ID
      );
    }

    this.subscribers.add(type);
  },

  unsubscribe(type) {
    this.subscribers.delete(type);
    const noSubscribers = this.subscribers.size === 0;

    if (noSubscribers) {
      this.messageBus.unsubscribe(this.channel);
      this._stopTimer();

      this.setProperties({
        users: [],
        editingUsers: [],
      });
    }

    return noSubscribers;
  },

  @discourseComputed("topicId")
  channel(topicId) {
    return `/presence/${topicId}`;
  },

  publish(state, whisper, postId, staffOnly) {
    if (this.get("currentUser.hide_profile_and_presence")) return;

    const data = {
      state,
      topic_id: this.topicId,
    };

    if (whisper) {
      data.is_whisper = true;
    }

    if (postId && state === EDITING) {
      data.post_id = postId;
    }

    if (staffOnly) {
      data.staff_only = true;
    }

    return ajax("/presence/publish", {
      type: "POST",
      data,
    });
  },

  _removeUser(user) {
    [this.users, this.editingUsers].forEach((users) => {
      const existingUser = users.findBy("id", user.id);
      if (existingUser) users.removeObject(existingUser);
    });
  },

  _cleanUpUsers() {
    [this.users, this.editingUsers].forEach((users) => {
      const staleUsers = [];

      users.forEach((user) => {
        if (user.last_seen <= Date.now() - BUFFER_DURATION_SECONDS * 1000) {
          staleUsers.push(user);
        }
      });

      users.removeObjects(staleUsers);
    });

    return this.users.length === 0 && this.editingUsers.length === 0;
  },

  _appendUser(users, user, attrs) {
    let existingUser;
    let usersLength = 0;

    users.forEach((u) => {
      if (u.id === user.id) {
        existingUser = u;
      }

      if (attrs && attrs.post_id) {
        if (u.post_id === attrs.post_id) usersLength++;
      } else {
        usersLength++;
      }
    });

    const props = attrs || {};
    props.last_seen = Date.now();

    if (existingUser) {
      existingUser.setProperties(props);
    } else {
      const limit = this.get("siteSettings.presence_max_users_shown");

      if (usersLength < limit) {
        users.pushObject(EmberObject.create(Object.assign(user, props)));
      }
    }

    this._startTimer(() => {
      this._cleanUpUsers();
    });
  },

  _scheduleTimer(callback) {
    return later(
      this,
      () => {
        const stop = callback();

        if (!stop) {
          this.set("_timer", this._scheduleTimer(callback));
        }
      },
      TIMER_INTERVAL_MILLISECONDS
    );
  },

  _stopTimer() {
    cancel(this._timer);
  },

  _startTimer(callback) {
    if (!this._timer) {
      this.set("_timer", this._scheduleTimer(callback));
    }
  },
});

export default Presence;
