/**
 * @file Namespace for chat-related functionalities.
 *
 * @module chat
 */
var chat = function () {};
window.chat = chat;

// List of functions to track for synchronization between chat and comm
const legacyFunctions = [
  'genPostData',
  'updateOldNewHash',
  'parseMsgData',
  'writeDataToHash',
  'renderText',
  'getChatPortalName',
  'renderPortal',
  'renderFactionEnt',
  'renderPlayer',
  'renderMarkupEntity',
  'renderMarkup',
  'renderTimeCell',
  'renderNickCell',
  'renderMsgCell',
  'renderMsgRow',
  'renderDivider',
  'renderData',
];
const newCommApi = [
  '_genPostData',
  '_updateOldNewHash',
  'parseMsgData',
  '_writeDataToHash',
  'renderText',
  'getChatPortalName',
  'renderPortal',
  'renderFactionEnt',
  'renderPlayer',
  'renderMarkupEntity',
  'renderMarkup',
  'renderTimeCell',
  'renderNickCell',
  'renderMsgCell',
  'renderMsgRow',
  'renderDivider',
  'renderData',
];

// Function to map legacy function names to their new names in comm
function mapLegacyFunctionNameToCommApi(functionName) {
  const index = legacyFunctions.indexOf(functionName);
  return index !== -1 ? newCommApi[index] : functionName;
}

// Create a proxy for chat to ensure backward compatibility of migrated functions from chat to comm
window.chat = new Proxy(window.chat, {
  get(target, prop, receiver) {
    if (prop in target) {
      // Return the property from chat if it's defined
      return target[prop];
    } else if (legacyFunctions.includes(prop)) {
      // Map the legacy function name to its new name in comm and return the corresponding function
      const commProp = mapLegacyFunctionNameToCommApi(prop);
      return window.IITC.comm[commProp];
    }
    // Return default value if the property is not found
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value) {
    if (legacyFunctions.includes(prop)) {
      // Map the legacy function name to its new name in comm and synchronize the function between chat and comm
      const commProp = mapLegacyFunctionNameToCommApi(prop);
      window.IITC.comm[commProp] = value;
    }
    // Update or add the property in chat
    target[prop] = value;
    return true; // Indicates that the assignment was successful
  },
});

//
// common
//

/**
 * Adds a nickname to the chat input.
 *
 * @function addNickname
 * @param {string} nick - The nickname to add.
 */
chat.addNickname = function (nick) {
  var c = document.getElementById('chattext');
  c.value = [c.value.trim(), nick].join(' ').trim() + ' ';
  c.focus();
};

/**
 * Handles click events on nicknames in the chat.
 *
 * @function nicknameClicked
 * @param {Event} event - The click event.
 * @param {string} nickname - The clicked nickname.
 * @returns {boolean} Always returns false.
 */
chat.nicknameClicked = function (event, nickname) {
  // suppress @ if coming from chat
  if (nickname.startsWith('@')) {
    nickname = nickname.slice(1);
  }
  var hookData = { event: event, nickname: nickname };

  if (window.runHooks('nicknameClicked', hookData)) {
    chat.addNickname('@' + nickname);
  }

  event.preventDefault();
  event.stopPropagation();
  return false;
};

//
// Channels
//

// WORK IN PROGRESS
// 'all' 'faction' and 'alerts' channels are hard coded in several places (including mobile app)
// dont change those channels since they refer to stock channels
// you can add channels from another source provider (message relay, logging from plugins...)

/**
 * Hold channel description
 *
 * See comm.js for examples
 * @typedef {Object} ChannelDescription
 * @property {string} id - uniq id, matches 'tab' parameter for server requests
 * @property {string} name - visible name
 * @property {string} [inputPrompt] - (optional) string for the input prompt
 * @property {string} [inputClass] - (optional) class to apply to #chatinput
 * @property {ChannelSendMessageFn} [sendMessage] - (optional) function to send the message
 * @property {ChannelRequestFn} [request] - (optional) function to call to request new message
 * @property {ChannelRenderFn} [render] - (optional) function to render channel content,, called on tab change
 * @property {boolean} [localBounds] - (optional) if true, reset on view change
 */
/**
 * @callback ChannelSendMessageFn
 * @param {string} id - channel id
 * @param {string} message - input message
 * @returns {void}
 */
/**
 * @callback ChannelRequestFn
 * @param {string} id - channel id
 * @param {boolean} getOlderMsgs - true if request data from a scroll to top
 * @param {boolean} isRetry
 * @returns {void}
 */
/**
 * @callback ChannelRenderFn
 * @param {string} id - channel id
 * @param {boolean} oldMsgsWereAdded - true if data has been added at the top (to preserve scroll position)
 * @returns {void}
 */

/**
 * Holds channels infos.
 *
 * @type {ChannelDescription[]}
 * @memberof module:chat
 */
chat.channels = [];

/**
 * Gets the name of the active chat tab.
 *
 * @function getActive
 * @returns {string} The name of the active chat tab.
 */
chat.getActive = function () {
  return $('#chatcontrols .active').data('channel');
};

/**
 * Converts a chat tab name to its corresponding channel object.
 *
 * @function getChannelDesc
 * @param {string} tab - The name of the chat tab.
 * @returns {ChannelDescription} The corresponding channel name ('faction', 'alerts', or 'all').
 */
chat.getChannelDesc = function (tab) {
  var channelObject = null;
  chat.channels.forEach(function (entry) {
    if (entry.id === tab) channelObject = entry;
  });
  return channelObject;
};

/**
 * Allows plugins to request and monitor COMM data streams in the background. This is useful for plugins
 * that need to process COMM data even when the user is not actively viewing the COMM channels.
 * It tracks the requested channels for each plugin instance and updates the global state accordingly.
 *
 * @function backgroundChannelData
 * @param {string} instance - A unique identifier for the plugin or instance requesting background COMM data.
 * @param {string} channel - The name of the COMM channel ('all', 'faction', or 'alerts').
 * @param {boolean} flag - Set to true to request data for the specified channel, false to stop requesting.
 */
chat.backgroundChannelData = function (instance, channel, flag) {
  // first, store the state for this instance
  if (!chat.backgroundInstanceChannel) chat.backgroundInstanceChannel = {};
  if (!chat.backgroundInstanceChannel[instance]) chat.backgroundInstanceChannel[instance] = {};
  chat.backgroundInstanceChannel[instance][channel] = flag;

  // now, to simplify the request code, merge the flags for all instances into one
  // 1. clear existing overall flags
  chat.backgroundChannels = {};
  // 2. for each instance monitoring COMM...
  $.each(chat.backgroundInstanceChannel, function (instance) {
    // 3. and for each channel monitored by this instance...
    $.each(chat.backgroundInstanceChannel[instance], function (channel, flag) {
      // 4. if it's monitored, set the channel flag
      if (flag) chat.backgroundChannels[channel] = true;
    });
  });
};

/**
 * Requests chat messages for the currently active chat tab and background channels.
 * It calls the appropriate request function based on the active tab or background channels.
 *
 * @function request
 */
chat.request = function () {
  var channel = chat.getActive();
  chat.channels.forEach(function (entry) {
    if (channel === entry.id || (chat.backgroundChannels && chat.backgroundChannels[entry.id])) {
      if (entry.request) entry.request(entry.id, false);
    }
  });
};

/**
 * Checks if the currently selected chat tab needs more messages.
 * This function is triggered by scroll events and loads older messages when the user scrolls to the top.
 *
 * @function needMoreMessages
 */
chat.needMoreMessages = function () {
  var activeTab = chat.getActive();
  var channel = chat.getChannelDesc(activeTab);
  if (!channel || !channel.request) return;

  var activeChat = $('#chat > :visible');
  if (activeChat.length === 0) return;

  var hasScrollbar = window.scrollBottom(activeChat) !== 0 || activeChat.scrollTop() !== 0;
  var nearTop = activeChat.scrollTop() <= window.CHAT_REQUEST_SCROLL_TOP;
  if (hasScrollbar && !nearTop) return;

  channel.request(channel.id, false);
};

/**
 * Chooses and activates a specified chat tab.
 * Also triggers an early refresh of the chat data when switching tabs.
 *
 * @function chooseTab
 * @param {string} tab - The name of the chat tab to activate ('all', 'faction', or 'alerts').
 */
chat.chooseTab = function (tab) {
  if (
    chat.channels.every(function (entry) {
      return entry.id !== tab;
    })
  ) {
    var tabsAvalaible = chat.channels
      .map(function (entry) {
        return '"' + entry.id + '"';
      })
      .join(', ');
    log.warn('chat tab "' + tab + '" requested - but only ' + tabsAvalaible + ' are valid - assuming "all" wanted');
    tab = 'all';
  }

  var oldTab = chat.getActive();

  localStorage['iitc-chat-tab'] = tab;

  var oldChannel = chat.getChannelDesc(oldTab);
  var channel = chat.getChannelDesc(tab);

  var chatInput = $('#chatinput');
  if (oldChannel && oldChannel.inputClass) chatInput.removeClass(oldChannel.inputClass);
  if (channel.inputClass) chatInput.addClass(channel.inputClass);

  var mark = $('#chatinput mark');
  mark.text(channel.inputPrompt || '');

  $('#chatcontrols .active').removeClass('active');
  $("#chatcontrols a[data-channel='" + tab + "']").addClass('active');

  if (tab !== oldTab) window.startRefreshTimeout(0.1 * 1000); // only chat uses the refresh timer stuff, so a perfect way of forcing an early refresh after a tab change

  $('#chat > div').hide();

  var elm = $('#chat' + tab);
  elm.show();

  if (channel.render) channel.render(tab);

  if (elm.data('needsScrollTop')) {
    elm.data('ignoreNextScroll', true);
    elm.scrollTop(elm.data('needsScrollTop'));
    elm.data('needsScrollTop', null);
  }
};

/**
 * Toggles the chat window between expanded and collapsed states.
 * When expanded, the chat window covers a larger area of the screen.
 * This function also ensures that the chat is scrolled to the bottom when collapsed.
 *
 * @function toggle
 */
chat.toggle = function () {
  var c = $('#chat, #chatcontrols');
  if (c.hasClass('expand')) {
    c.removeClass('expand');
    var div = $('#chat > div:visible');
    div.data('ignoreNextScroll', true);
    div.scrollTop(99999999); // scroll to bottom
    $('.leaflet-control').removeClass('chat-expand');
  } else {
    c.addClass('expand');
    $('.leaflet-control').addClass('chat-expand');
    chat.needMoreMessages();
  }
};

/**
 * Displays the chat interface and activates a specified chat tab.
 *
 * @function show
 * @param {string} name - The name of the chat tab to show and activate.
 */
chat.show = function (name) {
  if (window.isSmartphone()) {
    $('#updatestatus').hide();
  } else {
    $('#updatestatus').show();
  }
  $('#chat, #chatinput').show();

  chat.chooseTab(name);
};

/**
 * Chat tab chooser handler.
 * This function is triggered by a click event on the chat tab. It reads the tab name from the event target
 * and activates the corresponding chat tab.
 *
 * @function chooser
 * @param {Event} event - The event triggered by clicking a chat tab.
 */
chat.chooser = function (event) {
  var t = $(event.target);
  var tab = t.data('channel');

  if (window.isSmartphone() && !window.useAppPanes()) {
    window.show(tab);
  } else {
    chat.chooseTab(tab);
  }
};

/**
 * Maintains the scroll position of a chat box when new messages are added.
 * This function is designed to keep the scroll position fixed when old messages are loaded, and to automatically scroll
 * to the bottom when new messages are added if the user is already at the bottom of the chat.
 *
 * @function keepScrollPosition
 * @param {jQuery} box - The jQuery object of the chat box.
 * @param {number} scrollBefore - The scroll position before new messages were added.
 * @param {boolean} isOldMsgs - Indicates if the added messages are older messages.
 */
chat.keepScrollPosition = function (box, scrollBefore, isOldMsgs) {
  // If scrolled down completely, keep it that way so new messages can
  // be seen easily. If scrolled up, only need to fix scroll position
  // when old messages are added. New messages added at the bottom don’t
  // change the view and enabling this would make the chat scroll down
  // for every added message, even if the user wants to read old stuff.

  if (box.is(':hidden') && !isOldMsgs) {
    box.data('needsScrollTop', 99999999);
    return;
  }

  if (scrollBefore === 0 || isOldMsgs) {
    box.data('ignoreNextScroll', true);
    box.scrollTop(box.scrollTop() + (window.scrollBottom(box) - scrollBefore));
  }
};

/**
 * Create and insert into the DOM/Mobile app the channel tab
 *
 * @function createChannelTab
 * @memberof chat
 * @param {ChannelDescription} channelDesc - channel description
 * @static
 */
function createChannelTab(channelDesc) {
  var chatControls = $('#chatcontrols');
  var chatDiv = $('#chat');
  var accessLink = L.Util.template('<a data-channel="{id}" accesskey="{index}" title="[{index}]">{name}</a>', channelDesc);
  $(accessLink).appendTo(chatControls).click(chat.chooser);

  var channelDiv = L.Util.template('<div id="chat{id}"><table></table></div>', channelDesc);
  var elm = $(channelDiv).appendTo(chatDiv);
  if (channelDesc.request) {
    elm.scroll(function () {
      var t = $(this);
      if (t.data('ignoreNextScroll')) return t.data('ignoreNextScroll', false);
      if (t.scrollTop() < window.CHAT_REQUEST_SCROLL_TOP) channelDesc.request(channelDesc.id, true);
      if (window.scrollBottom(t) === 0) channelDesc.request(channelDesc.id, false);
    });
  }

  // pane
  if (window.useAndroidPanes()) {
    // exlude hard coded panes
    if (channelDesc.id !== 'all' && channelDesc.id !== 'faction' && channelDesc.id !== 'alerts') {
      app.addPane(channelDesc.id, channelDesc.name, 'ic_action_view_as_list');
    }
  }
}

var isTabsSetup = false;
/**
 * Add to the channel list a new channel description
 *
 * If tabs are already created, a tab is created for this channel as well
 *
 * @function addChannel
 * @param {ChannelDescription} channelDesc - channel description
 */
chat.addChannel = function (channelDesc) {
  // deny reserved name
  if (channelDesc.id === 'info' || channelDesc.id === 'map') {
    log.warn('could not add channel "' + channelDesc.id + '": reserved');
    return false;
  }
  if (chat.getChannelDesc(channelDesc.id)) {
    log.warn('could not add channel "' + channelDesc.id + '": already exist');
    return false;
  }

  chat.channels.push(channelDesc);
  channelDesc.index = chat.channels.length;

  if (isTabsSetup) createChannelTab(channelDesc);

  return true;
};

//
// setup
//

/**
 * Sets up all channels starting from intel COMM
 *
 * @function setupTabs
 */
chat.setupTabs = function () {
  isTabsSetup = true;

  // insert at the begining the comm channels
  chat.channels.splice(0, 0, ...IITC.comm.channels);

  chat.channels.forEach(function (entry, i) {
    entry.index = i + 1;
    createChannelTab(entry);
  });

  // legacy compatibility
  chat._public = IITC.comm._channelsData.all;
  chat._faction = IITC.comm._channelsData.faction;
  chat._alerts = IITC.comm._channelsData.alerts;

  /**
   * Initiates a request for public chat data.
   *
   * @function requestPublic
   * @param {boolean} getOlderMsgs - Whether to retrieve older messages.
   * @param {boolean} [isRetry=false] - Whether the request is a retry.
   */
  chat.requestPublic = function (getOlderMsgs, isRetry) {
    return IITC.comm.requestChannel('all', getOlderMsgs, isRetry);
  };

  /**
   * Requests faction chat messages.
   *
   * @function requestFaction
   * @param {boolean} getOlderMsgs - Flag to determine if older messages are being requested.
   * @param {boolean} [isRetry=false] - Flag to indicate if this is a retry attempt.
   */
  chat.requestFaction = function (getOlderMsgs, isRetry) {
    return IITC.comm.requestChannel('faction', getOlderMsgs, isRetry);
  };

  /**
   * Initiates a request for alerts chat data.
   *
   * @function requestAlerts
   * @param {boolean} getOlderMsgs - Whether to retrieve older messages.
   * @param {boolean} [isRetry=false] - Whether the request is a retry.
   */
  chat.requestAlerts = function (getOlderMsgs, isRetry) {
    return IITC.comm.requestChannel('alerts', getOlderMsgs, isRetry);
  };

  /**
   * Renders public chat in the UI.
   *
   * @function renderPublic
   * @param {boolean} oldMsgsWereAdded - Indicates if older messages were added to the chat.
   */
  chat.renderPublic = function (oldMsgsWereAdded) {
    return IITC.comm.renderChannel('all', oldMsgsWereAdded);
  };

  /**
   * Renders faction chat.
   *
   * @function renderFaction
   * @param {boolean} oldMsgsWereAdded - Indicates if old messages were added in the current rendering.
   */
  chat.renderFaction = function (oldMsgsWereAdded) {
    return IITC.comm.renderChannel('faction', oldMsgsWereAdded);
  };

  /**
   * Renders alerts chat in the UI.
   *
   * @function renderAlerts
   * @param {boolean} oldMsgsWereAdded - Indicates if older messages were added to the chat.
   */
  chat.renderAlerts = function (oldMsgsWereAdded) {
    return IITC.comm.renderChannel('allerts', oldMsgsWereAdded);
  };
};

/**
 * Sets up the chat interface.
 *
 * @function setup
 */
chat.setup = function () {
  chat.setupTabs();

  if (localStorage['iitc-chat-tab']) {
    chat.chooseTab(localStorage['iitc-chat-tab']);
  }

  $('#chatcontrols, #chat, #chatinput').show();

  $('#chatcontrols a:first').click(chat.toggle);

  $('#chatinput').click(function () {
    $('#chatinput input').focus();
  });

  chat.setupTime();
  chat.setupPosting();

  window.requests.addRefreshFunction(chat.request);

  var cls = PLAYER.team === 'RESISTANCE' ? 'res' : 'enl';
  $('#chatinput mark').addClass(cls);

  $(document).on('click', '.nickname', function (event) {
    return chat.nicknameClicked(event, $(this).text());
  });
};

/**
 * Sets up the time display in the chat input box.
 * This function updates the time displayed next to the chat input field every minute to reflect the current time.
 *
 * @function setupTime
 */
chat.setupTime = function () {
  var inputTime = $('#chatinput time');
  var updateTime = function () {
    if (window.isIdle()) return;
    var d = new Date();
    var h = d.getHours() + '';
    if (h.length === 1) h = '0' + h;
    var m = d.getMinutes() + '';
    if (m.length === 1) m = '0' + m;
    inputTime.text(h + ':' + m);
    // update ON the minute (1ms after)
    setTimeout(updateTime, (60 - d.getSeconds()) * 1000 + 1);
  };
  updateTime();
  window.addResumeFunction(updateTime);
};

//
// posting
//

/**
 * Handles tab completion in chat input.
 *
 * @function handleTabCompletion
 */
chat.handleTabCompletion = function () {
  var el = $('#chatinput input');
  var curPos = el.get(0).selectionStart;
  var text = el.val();
  var word = text
    .slice(0, curPos)
    .replace(/.*\b([a-z0-9-_])/, '$1')
    .toLowerCase();

  var list = $('#chat > div:visible mark');
  list = list.map(function (ind, mark) {
    return $(mark).text();
  });
  list = window.uniqueArray(list);

  var nick = null;
  for (var i = 0; i < list.length; i++) {
    if (!list[i].toLowerCase().startsWith(word)) continue;
    if (nick && nick !== list[i]) {
      log.warn('More than one nick matches, aborting. (' + list[i] + ' vs ' + nick + ')');
      return;
    }
    nick = list[i];
  }
  if (!nick) {
    return;
  }

  var posStart = curPos - word.length;
  var newText = text.substring(0, posStart);
  var atPresent = text.substring(posStart - 1, posStart) === '@';
  newText += (atPresent ? '' : '@') + nick + ' ';
  newText += text.substring(curPos);
  el.val(newText);
};

/**
 * Posts a chat message to the currently active chat tab.
 *
 * @function postMsg
 */
chat.postMsg = function () {
  var c = chat.getActive();
  var channel = chat.getChannelDesc(c);

  var msg = $.trim($('#chatinput input').val());
  if (!msg || msg === '') return;

  if (channel.sendMessage) {
    $('#chatinput input').val('');
    return channel.sendMessage(c, msg);
  }
};

/**
 * Sets up the chat message posting functionality.
 *
 * @function setupPosting
 */
chat.setupPosting = function () {
  if (!window.isSmartphone()) {
    $('#chatinput input').keydown(function (event) {
      try {
        var kc = event.keyCode ? event.keyCode : event.which;
        if (kc === 13) {
          // enter
          chat.postMsg();
          event.preventDefault();
        } else if (kc === 9) {
          // tab
          event.preventDefault();
          chat.handleTabCompletion();
        }
      } catch (e) {
        log.error(e);
        // if (e.stack) { console.error(e.stack); }
      }
    });
  }

  $('#chatinput').submit(function (event) {
    event.preventDefault();
    chat.postMsg();
  });
};

/**
 * Legacy function for rendering chat messages. Used for backward compatibility with plugins.
 *
 * @deprecated
 * @function renderMsg
 * @param {string} msg - The chat message.
 * @param {string} nick - The nickname of the player who sent the message.
 * @param {number} time - The timestamp of the message.
 * @param {string} team - The team of the player who sent the message.
 * @param {boolean} msgToPlayer - Flag indicating if the message is directed to the player.
 * @param {boolean} systemNarrowcast - Flag indicating if the message is a system narrowcast.
 * @returns {string} The HTML string representing a chat message row.
 */
chat.renderMsg = function (msg, nick, time, team, msgToPlayer, systemNarrowcast) {
  // Imitating data usually derived from processing raw chat data
  var fakeData = {
    guid: 'legacyguid-' + Math.random(),
    time: time,
    public: !systemNarrowcast,
    secure: systemNarrowcast,
    alert: msgToPlayer,
    msgToPlayer: msgToPlayer,
    type: systemNarrowcast ? 'SYSTEM_NARROWCAST' : 'PLAYER_GENERATED',
    narrowcast: systemNarrowcast,
    auto: false, // Assuming the message is player-generated if it's not a system broadcast
    team: team,
    player: {
      name: nick,
      team: team,
    },
    markup: [
      ['TEXT', { plain: msg }], // A simple message with no special markup
    ],
  };

  // Use existing IITC functions to render a chat message row
  return IITC.comm.renderMsgRow(fakeData);
};

/**
 * Legacy function for converts a chat tab name to its corresponding COMM channel name.
 * Used for backward compatibility with plugins.
 *
 * @deprecated
 * @function tabToChannel
 * @param {string} tab - The name of the chat tab.
 * @returns {string} The corresponding channel name ('faction', 'alerts', or 'all').
 */
chat.tabToChannel = function (tab) {
  if (tab === 'faction') return 'faction';
  if (tab === 'alerts') return 'alerts';
  return 'all';
};

/* global log, PLAYER, L, IITC, app */