'use client';
import { ChatLayout } from "@/layout/ChatLayout/ChatLayout";
import { io } from "socket.io-client";
import { useEffect, useMemo, useRef, useState } from "react";
import { getCookie } from 'cookies-next';
import styles from "./styles.module.css";
import Markdown from "react-markdown";
import { Conditional, ConditionalNull } from "@/components/utility/Conditional";

type MessageType = {
  channel: string,
  message: string,
  me: boolean,
  user: string,
  id: string,
  timestamp: number
};

type Res = {type: string, spec: string, data: any};

export default function Home() {
  const message_translation_tables = new Map([
    ["ERR_CHAT_JOIN", new Map([
      ["main", "Error when joining chat"],
      ["JOINED", "You've already joined Chat"],
      ["MALFORMED_TOKEN", "You have a malformed token"],
      ["INVALID_TOKEN", "Your token is invalid or expired"],
      ["INVALID_USER", "Your token has no user or your user is banned (shame on you)"],
    ])],
    ["ERR_CHAT_WHOISTHIS", new Map([
      ["main", "Error when fetching user"],
      ["NO_USER", "Unauthenticated request"],
      ["MALFORMED_USER", "You have a malformed user ID"],
      ["INVALID_USER", "No user with this ID"],
    ])],
    ["ERR_CHAT_CHGCHANNEL", new Map([
      ["main", "Error when changing channel"],
      ["NO_USER", "Unauthenticated request"],
      ["MALFORMED_CHANNEL", "You have a malformed channel selection; try again"],
      ["INVALID_CHANNEL", "No channel with this name"],
      ["CHANNEL_UNREADABLE", "You are not authorized to read this channel"]
    ])],
    ["ERR_CHAT_MESSAGE", new Map([
      ["main", "Error when sending message"],
      ["NO_USER", "Unauthenticated request"],
      ["MALFORMED_MESSAGE", "You have a malformed message; try again"],
      ["DATABASE_ERROR", "The database encountered an error (contact me@abtmtr.link)"],
      ["CHANNEL_UNWRITABLE", "You are not authorized to write to this channel"],
      ["RATE_LIMIT", "You are being rate-limited"],
      ["RATE_LIMIT_BAN", "You have been disconnected due to exceeding the rate-limit"]
    ])],
    ["ERR_CHAT_RMMESSAGE", new Map([
      ["main", "Error when removing message"],
      ["NO_USER", "Unauthenticated request"],
      ["MALFORMED_MESSAGEID", "You have a malformed message selection; try again"],
      ["NULL_MESSAGE", "No message with this ID"],
      ["NUH_UH", "You are not authorized to remove this message"],
      ["DATABASE_ERROR", "The database encountered an error (contact me@abtmtr.link)"],
      ["RATE_LIMIT", "You are being rate-limited"],
      ["RATE_LIMIT_BAN", "You have been disconnected due to exceeding the rate-limit"]
    ])],
    ["ERR_CHAT_USERS", new Map([
      ["main", "Error when fetching users"],
      ["NO_USER", "Unauthenticated request"]
    ])],
    ["ERR_CHAT_CHANNELS", new Map([
      ["main", "Error when fetching channels"],
      ["NO_USER", "Unauthenticated request"],
      ["DATABASE_ERROR", "The database encountered an error (contact me@abtmtr.link)"]
    ])],
    ["ERR_CHAT_MESSAGES", new Map([
      ["main", "Error when fetching messages"],
      ["NO_USER", "Unauthenticated request"],
      ["DATABASE_ERROR", "The database encountered an error (if this continues, contact me@abtmtr.link)"],
      ["CHANNEL_UNREADABLE", "You are not authorized to read this channel"]
    ])]
  ]);
  const data_translation_tables:Map<string, {[key:string]:any}> = new Map([
    ["RATE_LIMIT", {
      reset: (x:number) => `(Refreshes in ${x} seconds)`
    }]
  ])
  const socketLocation = "https://chatserver.abtmtr.link/";
  const socketObject = useMemo(() => io(socketLocation), []);
  const socket = socketObject;

  const token = getCookie('token');

  const [ connected, setConnected ] = useState(false);
  const [ error, _setError ] = useState("");
  const [ whoami, _setWhoAmI ] = useState<{[key:string]:any}>({});
  const whoamiRef = useRef(whoami);
  const setWhoAmI = (x:any) => {whoamiRef.current = x;_setWhoAmI(x);};
  const [ channel, _setChannel ] = useState("");
  const channelRef = useRef(channel);
  const setChannel = (x:any) => {channelRef.current = x;_setChannel(x);};
  const [ channels, setChannels ] = useState<{
    name: string,
    action: () => void,
    readable: boolean,
    writable: boolean
  }[]>([]);
  const [ channelIndicators, _setChannelIndicators ] = useState<{[key:string]:boolean}>({});
  const channelIndicatorsRef = useRef(channelIndicators);
  const setChannelIndicators = (x:any) => {channelIndicatorsRef.current = x;_setChannelIndicators(x);};
  const [ users, _setUsers ] = useState<{
    id: string,
    username: string,
    instance: string,
    admin: boolean
  }[]>([]);
  const usersRef = useRef(users);
  const setUsers = (x:any) => {usersRef.current = x;_setUsers(x);};
  const [ messages, _setMessages ] = useState<MessageType[]>([]);
  const messagesRef = useRef(messages);
  const setMessages = (x:any) => {messagesRef.current = x;_setMessages(x);};

  /* SECT: LHUA; Lord help us all these ones */
  const [ textBox, setTextBox ] = useState<React.ReactNode>(null);
  let Message = useRef<(message:MessageType) => React.ReactNode>(() => <></>);
  const [ affectedByUpdate, forceUpdate ] = useState({});
  const externCachedUsers = useRef(new Map());
  const externCacheRows = useRef(new Map());
  /* END_SECT LHUA; */


  const chatInput = useRef<HTMLTextAreaElement>(null);
  const chatPaneRef = useRef<HTMLDivElement>(null);

  function unsetError() {
    _setError("");
  }
  function setError(type:string, spec:string, data?:any) {
    let errorTable = message_translation_tables.get(type);
    let dataTable = data_translation_tables.get(spec);

    let errorTableType;
    let errorTableSpec;
    if (errorTable == null) {
      errorTableType = type;
      errorTableSpec = spec;
    } else {
      errorTableType = errorTable.get("main");
      if (errorTableType == null)
        errorTableType = type;
      errorTableSpec = errorTable.get(spec);
      if (errorTableSpec == null)
        errorTableSpec = spec;
    }

    let dataTableSpec;
    if (dataTable != null && data != null) {
      dataTableSpec = Object.entries(data).reduce((prev, [k,v]) => prev + " " + dataTable[k](v), "");
    }

    if (dataTableSpec == null)
      _setError(`${errorTableType}: ${errorTableSpec}`);
    else
      _setError(`${errorTableType}: ${errorTableSpec} ${dataTableSpec}`)
  }

  useEffect(() => {

    /* SECT: BSN; Because "soft navigation" */
    // @ts-ignore
    if (window.socket && window.socket !== socket)
      // @ts-ignore
      window.socket.disconnect();
    // @ts-ignore
    window.socket = socket;
    /* END_SECT BSN; */

    function onConnect() {
      setConnected(true);
      if (token != null) {
        socket.emit('SIG_CHAT_JOIN', { token }, joinChatRes);
        setTextBox(<div className={styles.textBox}>
          <textarea rows={2} className={styles.textBoxInput} ref={chatInput}></textarea>
          <button className={styles.textBoxButton} onClick={() => socket.emit('SIG_CHAT_MESSAGE', { message: chatInput.current?.value }, messageChatRes)}>Send</button>
        </div>)
      } else
        socket.emit('SIG_CHAT_CHANNELS', {}, channelsChatRes);

      Message.current = (message:MessageType) => {
        let user;
        user = usersRef.current.find(x => x.id == message.user);
        if (user == null)
          user = externCachedUsers.current.get(message.user);
        if (user == null && !externCacheRows.current.has(message.user)) {
          externCacheRows.current.set(message.user, new Promise((res, rej) => {
            socket.emit('SIG_CHAT_WHOISTHIS', { id: message.user }, ({ type, spec, data }:Res) => {
              if (type.includes("ERR"))
                rej(`${type} - ${spec}`);
              else {
                externCachedUsers.current.set(data.id, data);
                res("");
                forceUpdate({});
              }
              externCacheRows.current.delete(message.user);
            });
          }));
        }
        const canDelete = whoamiRef.current.admin || whoamiRef.current.id == message.user;
        const timeSent = new Date(message.timestamp * 1000).toLocaleString();
        return <div className={styles.Message} key={message.id}>
          <h3 className={styles.MessageHeader}>
            {user?.id != null ?
              <a href={`/jams/user/${user?.id}`} target="_blank">{user?.username}@{user?.instance}</a>
            : <i>Fetching user...</i>}
            {" "}
            <Conditional condition={canDelete} 
              truthy={<button onClick={() => socket.emit('SIG_CHAT_RMMESSAGE', { messageId: message.id }, rmmessageChatRes)} className={styles.deleteButton}>
                <small>{timeSent}</small>
              </button>}
              falsy={<small>{timeSent}</small>}
            />
          </h3>
          <Markdown>{message.message}</Markdown>
        </div>
      };
    }
    function onDisconnect() {
      setConnected(false);
    }

    if (socket.connected && !connected) {
      onConnect();
    }

    socket.on("connect", onConnect);
    socket.on("disconnect", onDisconnect);

    window.addEventListener("navigate", socket.disconnect);

    return () => {
      socket.off("connect", onConnect);
      socket.off("disconnect", onDisconnect);
    };
  }, []);

  // user stuff

  function addUser(userdata:{
    id: string,
    username: string,
    instance: string,
    admin: boolean
  }) {
    setUsers(usersRef.current.concat([ userdata ]));
  }

  function removeUser(userId:string) {
    const index = usersRef.current.findIndex(x => x.id == userId);
    if (index < 0) return;
    setUsers(usersRef.current.toSpliced(index, 1));
  }

  function usersChatRes({ type, spec, data }:Res) {
    unsetError();
    if (type.includes("ERR")) {
      setError(type, spec, data);
    } else {
      setUsers(data);
    }
  }

  socket.off("RES_CHAT_JOIN");
  socket.on("RES_CHAT_JOIN", ({ user }) => {
    addUser(user);
  });

  socket.off("RES_CHAT_LEAVE");
  socket.on("RES_CHAT_LEAVE", ({ userId }) => {
    removeUser(userId);
  });

  // message stuff

  function addMessage(messagesdata:MessageType) {
    setMessages(messagesRef.current.concat([ messagesdata ]));
  }

  function removeMessage(messageId:string) {
    const index = messagesRef.current.findIndex(x => x.id == messageId);
    if (index < 0) return;
    setMessages(messagesRef.current.toSpliced(index, 1));
  }

  function addChannelIndicator(channel:string) {
    const CIs = {
      ...channelIndicatorsRef.current
    };
    CIs[channel] = true;
    setChannelIndicators(CIs);
  }

  function removeChannelIndicator(channel:string) {
    const CIs = {
      ...channelIndicatorsRef.current
    };
    delete CIs[channel];
    setChannelIndicators(CIs);
  }

  function messagesChatRes({ type, spec, data }:Res) {
    unsetError();
    if (type.includes("ERR")) {
      setError(type, spec, data);
    } else {
      setMessages(data);
    }
  }

  function messageChatRes({ type, spec, data }:Res) {
    unsetError();
    if (type.includes("ERR")) {
      setError(type, spec, data);
    } else {
      addMessage(data);
    }
    if (chatInput.current != null)
      chatInput.current.value = "";
  }

  function rmmessageChatRes({ type, spec, data }:Res) {
    unsetError();
    if (type.includes("ERR")) {
      setError(type, spec, data);
    } else {
      removeMessage(data.messageId);
    }
  }

  socket.off("RES_CHAT_MESSAGE");
  socket.on("RES_CHAT_MESSAGE", ({ message }) => {
    if (message.channel == channelRef.current)
      addMessage(message);
    else
      addChannelIndicator(message.channel);
  });

  socket.off("RES_CHAT_RMMESSAGE");
  socket.on("RES_CHAT_RMMESSAGE", ({ messageId }) => {
    removeMessage(messageId);
  });

  function joinChatRes({ type, spec, data }:Res) {
    unsetError();
    if (type.includes("ERR")) {
      setError(type, spec, data);
    } else {
      setWhoAmI(data);
      socket.emit('SIG_CHAT_CHANNELS', {}, channelsChatRes);
    }
  }

  function channelsChatRes({ type, spec, data }:Res) {
    unsetError();
    if (type.includes("ERR")) {
      setError(type, spec, data);
    } else {
      setChannels(data.map((x:{[key:string]:any}) => {
        x.action = () => {
          socket.emit('SIG_CHAT_CHGCHANNEL', { channel: x.name }, chgchannelChatRes);
        }
        return x;
      }));
      if (whoamiRef.current.id != null)
        socket.emit('SIG_CHAT_CHGCHANNEL', { channel: data[0].name }, chgchannelChatRes);
    }
  }

  const prevscrtop = useRef(0);

  function chgchannelChatRes({ type, spec, data }:Res) {
    unsetError();
    if (type.includes("ERR")) {
      setError(type, spec, data);
    } else {
      prevscrtop.current = 0;
      setChannel(data.channel);
      removeChannelIndicator(data.channel);
      socket.emit('SIG_CHAT_USERS', {}, usersChatRes);
      socket.emit('SIG_CHAT_MESSAGES', {}, messagesChatRes);
    }
  }

  let [ messageLayout, setMessageLayout ] = useState<React.ReactNode>();
  useEffect(() => {

    if (messages.length > 0)
      setMessageLayout(messages.map(Message.current));
    else if (whoami.admin || channels.find(x => x.name == channel)?.writable)
      setMessageLayout(
        <div style={{textAlign: "center"}}>
          <h1>This channel is empty.</h1>
          <p>Use the bar below to post something.</p>
        </div>
      );
    else if (token == null)
      setMessageLayout(<>
        <h2>Join abtmtr.link Chat</h2>
        <p>If you'd like to participate in the chat, you should join!</p>
        <p><small>To join with the Fediverse, enter your instance domain.<br />
        To join with GitHub, enter "github.com".<br />
        To join with Discord, enter "discord.com".</small></p>
        <form action="/jams/oauth/login">
          <input name="instance" placeholder="Domain (e.g. &quot;social.besties.house&quot; or &quot;woem.men&quot;)" type="text" />
          <input type="submit" />
        </form>
        <p><small>Tested on Discord, GitHub, Mastodon, GoToSocial, and Akkoma</small></p>
      </>);
  }, [affectedByUpdate, messages]);

  useEffect(() => {
    if (
      chatPaneRef.current != null
      && chatPaneRef.current.scrollTop >= prevscrtop.current - 100
    ) {
      chatPaneRef.current.scrollTop = chatPaneRef.current.scrollHeight;
      prevscrtop.current = chatPaneRef.current.scrollTop;
    }
  }, [messageLayout])

  return (
    <ChatLayout 
      title={channel ? `#${channel}` : `Waiting for channel selection...`}
      subtitle={error}
      props={{
        channels,
        channelIndicators,
        users,
        currentChannel: channel,
        connected,
        // @ts-ignore
        whoami,
        chatPaneRef
      }} 
      footerChildren={textBox}
    >
      {messageLayout}
    </ChatLayout>
  )
}