// Written by retoor@molodetz.nl

// This class defines a custom HTML element that displays a list of messages with avatars and timestamps. It handles message addition with a delay in event dispatch and ensures the display of messages in the correct format.

// The code seems to rely on some external dependencies like 'models.Message', 'app', and 'Schedule'. These should be imported or defined elsewhere in your application.

// MIT License: This is free software. Permission is granted to use, copy, modify, and/or distribute this software for any purpose with or without fee. The software is provided "as is" without any warranty.
import { app } from "./app.js";

const LONG_TIME = 1000 * 60 * 20;

export class ReplyEvent extends Event {
  constructor(messageTextTarget) {
    super('reply', { bubbles: true, composed: true });
    this.messageTextTarget = messageTextTarget;

    // Clone and sanitize message node to text-only reply
    const newMessage = messageTextTarget.cloneNode(true);
    newMessage.style.maxHeight = "0";
    messageTextTarget.parentElement.insertBefore(newMessage, messageTextTarget);

    // Remove all .embed-url-link
    newMessage.querySelectorAll('.embed-url-link').forEach(link => link.remove());

    // Replace <picture> with their <img>
    newMessage.querySelectorAll('picture').forEach(picture => {
      const img = picture.querySelector('img');
      if (img) picture.replaceWith(img);
    });

    // Replace <img> with just their src
    newMessage.querySelectorAll('img').forEach(img => {
      const src = img.src || img.currentSrc;
      img.replaceWith(document.createTextNode(src));
    });

    // Replace <iframe> with their src
    newMessage.querySelectorAll('iframe').forEach(iframe => {
      const src = iframe.src || iframe.currentSrc;
      iframe.replaceWith(document.createTextNode(src));
    });

    // Replace <a> with href or markdown
    newMessage.querySelectorAll('a').forEach(a => {
      const href = a.getAttribute('href');
      const text = a.innerText || a.textContent;
      if (text === href || text === '') {
        a.replaceWith(document.createTextNode(href));
      } else {
        a.replaceWith(document.createTextNode(`[${text}](${href})`));
      }
    });

    this.replyText = newMessage.innerText.replaceAll("\n\n", "\n").trim();
    newMessage.remove();
  }
}

class MessageElement extends HTMLElement {
  updateUI() {
    if (this._originalChildren === undefined) {
      const { color, user_nick, created_at, user_uid } = this.dataset;
      this.classList.add('message');
      this.style.maxWidth = '100%';
      this._originalChildren = Array.from(this.children);

      this.innerHTML = `
        <a class="avatar" style="background-color: ${color || ''}; color: black;" href="/user/${user_uid || ''}.html">
          <img class="avatar-img" width="40" height="40" src="/avatar/${user_uid || ''}.svg" alt="${user_nick || ''}" loading="lazy">
        </a>
        <div class="message-content">
          <div class="author" style="color: ${color || ''};">${user_nick || ''}</div>
          <div class="text"></div>
          <div class="time no-select" data-created_at="${created_at || ''}">
            <span></span>
            <a href="#reply">reply</a>
          </div>
        </div>
      `;

      this.messageDiv = this.querySelector('.text');
      if (this._originalChildren && this._originalChildren.length > 0) {
        this._originalChildren.forEach(child => {
          this.messageDiv.appendChild(child);
        });
      }

      this.timeDiv = this.querySelector('.time span');
      this.replyDiv = this.querySelector('.time a');

      this.replyDiv.addEventListener('click', (e) => {
        e.preventDefault();
        this.dispatchEvent(new ReplyEvent(this.messageDiv));
      });
    }

    // Sibling logic for user switches and long time gaps
    if ((!this.siblingGenerated || this.siblingGenerated !== this.nextElementSibling) && this.nextElementSibling) {
      this.siblingGenerated = this.nextElementSibling;
      if (this.nextElementSibling?.dataset?.user_uid !== this.dataset.user_uid) {
        this.classList.add('switch-user');
      } else {
        this.classList.remove('switch-user');
        const siblingTime = new Date(this.nextElementSibling.dataset.created_at);
        const currentTime = new Date(this.dataset.created_at);

        if (currentTime.getTime() - siblingTime.getTime() > LONG_TIME) {
          this.classList.add('long-time');
        } else {
          this.classList.remove('long-time');
        }
      }
    }

    this.timeDiv.innerText = app.timeDescription(this.dataset.created_at);
  }

  updateMessage(...messages) {
    if (this._originalChildren) {
      this.messageDiv.replaceChildren(...messages);
      this._originalChildren = messages;
    }
  }

  connectedCallback() {
    this.updateUI();
  }

  disconnectedCallback() {}
  connectedMoveCallback() {}

  attributeChangedCallback(name, oldValue, newValue) {
    this.updateUI();
  }
}

class MessageList extends HTMLElement {
  constructor() {
    super();
    this.messageMap = new Map();
    this.visibleSet = new Set();

    this._observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          this.visibleSet.add(entry.target);
          if (entry.target instanceof MessageElement) {
            entry.target.updateUI();
          }
        } else {
          this.visibleSet.delete(entry.target);
        }
      });
    }, {
      root: this,
      threshold: 0,
    });

    // End-of-messages marker
    this.endOfMessages = document.createElement('div');
    this.endOfMessages.classList.add('message-list-bottom');
    this.prepend(this.endOfMessages);

    // Observe existing children and index by uid
    for (const c of this.children) {
      this._observer.observe(c);
      if (c instanceof MessageElement) {
        this.messageMap.set(c.dataset.uid, c);
      }
    }

    // Wire up socket events
    app.ws.addEventListener("update_message_text", (data) => {
      if (this.messageMap.has(data.uid)) {
        this.upsertMessage(data);
      }
    });
    app.ws.addEventListener("set_typing", (data) => {
      this.triggerGlow(data.user_uid, data.color);
    });

    this.scrollToBottom(true);
  }

  connectedCallback() {
    this.addEventListener('click', (e) => {
      if (
        e.target.tagName !== 'IMG' ||
        e.target.classList.contains('avatar-img')
      ) return;

      const img = e.target;
      const overlay = document.createElement('div');
      overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);display:flex;justify-content:center;align-items:center;z-index:9999;cursor:pointer;';

      const urlObj = new URL(img.currentSrc || img.src, window.location.origin);
      urlObj.searchParams.delete('width');
      urlObj.searchParams.delete('height');

      const fullImg = document.createElement('img');
      fullImg.src = urlObj.toString();
      fullImg.alt = img.alt || '';
      fullImg.style.maxWidth = '90%';
      fullImg.style.maxHeight = '90%';
      fullImg.style.boxShadow = '0 0 32px #000';
      fullImg.style.borderRadius = '8px';
      fullImg.style.background = '#222';
      fullImg.style.objectFit = 'contain';
      fullImg.loading = 'lazy';

      overlay.appendChild(fullImg);
      document.body.appendChild(overlay);

      overlay.addEventListener('click', () => {
        if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
      });
      // ESC to close
      const escListener = (evt) => {
        if (evt.key === 'Escape') {
          if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
          document.removeEventListener('keydown', escListener);
        }
      };
      document.addEventListener('keydown', escListener);
    });
  }

  isElementVisible(element) {
    if (!element) return false;
    const rect = element.getBoundingClientRect();
    return (
      rect.top >= 0 &&
      rect.left >= 0 &&
      rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
      rect.right <= (window.innerWidth || document.documentElement.clientWidth)
    );
  }

  isScrolledToBottom() {
    return this.visibleSet.has(this.endOfMessages);
  }

  scrollToBottom(force = false, behavior = 'instant') {
    if (force || !this.isScrolledToBottom()) {
      this.endOfMessages.scrollIntoView({ behavior, block: 'end' });
      setTimeout(() => {
        this.endOfMessages.scrollIntoView({ behavior, block: 'end' });
      }, 200);
    }
  }

  triggerGlow(uid, color) {
    if (!uid || !color) return;
    app.starField.glowColor(color);
    let lastElement = null;
    this.querySelectorAll('.avatar').forEach((el) => {
      const anchor = el.closest('a');
      if (anchor && typeof anchor.href === 'string' && anchor.href.includes(uid)) {
        lastElement = el;
      }
    });
    if (lastElement) {
      lastElement.classList.add('glow');
      setTimeout(() => {
        lastElement.classList.remove('glow');
      }, 1000);
    }
  }

  updateTimes() {
    this.visibleSet.forEach((messageElement) => {
      if (messageElement instanceof MessageElement) {
        messageElement.updateUI();
      }
    });
  }

  upsertMessage(data) {
    let message = this.messageMap.get(data.uid);
    if ((message && (data.is_final || !data.message)) {
      message.parentElement?.removeChild(message);
      // TO force insert 
      message = null;
      
    }
    if (!data.message) return;

    const wrapper = document.createElement("div");
    wrapper.innerHTML = data.html;

    if (message) {
      // If the old element is already custom, only update its message children
      message.updateMessage(...(wrapper.firstElementChild._originalChildren || wrapper.firstElementChild.children));
    } else {
      // If not, insert the new one and observe
      message = wrapper.firstElementChild;
      this.messageMap.set(data.uid, message);
      this._observer.observe(message);
    }

    const scrolledToBottom = this.isScrolledToBottom();
    this.endOfMessages.after(message);
    if (scrolledToBottom) this.scrollToBottom(true);
  }
}

customElements.define("chat-message", MessageElement);
customElements.define("message-list", MessageList);

