import React from 'react'
import io from 'socket.io-client'
import linkifyString from 'linkify-string'

import constants from '../constants'
import LoadingIndicator from '../components/LoadingIndicator'
import { queryClient } from '../pages/_app'
import Header from '../components/header'

import styles from './chat.module.css'

const byDate = (a, b) => {
  return a.date - b.date
}

function groupPrevMessages(msgs, currUser) {
  return msgs.reduce((acc, msg) => {
    const group =
      parseInt(msg.sender) !== parseInt(currUser) ? msg.sender : msg.reciever
    return {
      ...acc,
      [group]: {
        msgs: [msg],
        loaded: false,
        lastLoaded: false,
        lastMessage: msg,
      },
    }
  }, {})
}

function addMsgsToGroup(msgGroup, msgs, user) {
  let res = { ...msgGroup }
  msgs.reverse().forEach((msg) => {
    res = addMsgToGroup(res, msg, user)
  })
  return res
}

function addMsgToGroup(msgGroup, msg, user) {
  const group =
    parseInt(msg.sender) === parseInt(user) ? msg.reciever : msg.sender
  if (!msgGroup[group]) {
    msgGroup[group] = { msgs: [], lastLoaded: true, loaded: true }
  }

  const currGroup = msgGroup[group]

  return {
    ...msgGroup,
    [group]: {
      ...currGroup,
      loaded: true,
      msgs: currGroup.loaded
        ? [...currGroup.msgs, msg].sort(byDate)
        : [msg].sort(byDate),
      lastMessage:
        !currGroup.lastMessage || msg.date > currGroup.lastMessage.date
          ? msg
          : currGroup.lastMessage,
    },
  }
}

function SeenMarker() {
  return (
    <span className={styles.unreadMarker} key={`unread_marker`}>
      Látta
    </span>
  )
}
function NewMarker() {
  return (
    <span className={styles.newMarker} key={`unread_marker`}>
      Új üzenet
    </span>
  )
}

function uploadFile(file) {
  return new Promise((resolve) => {
    const formData = new FormData() // eslint-disable-line
    formData.append('file', file)

    fetch(constants.apiUrl + 'postchatfile', {
      method: 'POST',
      credentials: 'include',
      headers: {
        Accept: 'application/json',
      },
      body: formData,
    })
      .then((res) => {
        return res.json()
      })
      .then((res) => {
        resolve(res)
      })
  })
}

export default class Chat extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      msgs: {},
      currMsg: '',
      connected: false,
      selectedUser: 0,
    }
    if (props.globalData && !isNaN(props.globalData.userId)) {
      this.state.user = props.globalData.userId
      this.connect(this.props.globalData.userId)
    }
    this.router = props.router
    this.chatInputRef = React.createRef()
  }

  componentDidUpdate(prevProps, prevState) {
    try {
      const user = this.props.router.query.user
      if (this.state.userFromQuery !== user) {
        if (isNaN(user)) {
          this.setState({
            userFromQuery: user,
          })
        } else {
          this.setState({
            selectedUser: user,
            userFromQuery: user,
          })
        }
      }
    } catch (e) {
      // e
    }

    if (!prevProps.globalData.userId && this.props.globalData.userId) {
      this.setState({
        user: this.props.globalData.userId,
      })
      this.connect(this.props.globalData.userId)
    }

    if (
      prevState.msgs &&
      prevState.msgs[prevState.selectedUser] &&
      this.state.msgs &&
      this.state.msgs[this.state.selectedUser]
    ) {
      const prevLatest = prevState.msgs[prevState.selectedUser].lastMessage
      const newLatest = this.state.msgs[this.state.selectedUser].lastMessage

      if (prevLatest && newLatest && prevLatest.date !== newLatest.date) {
        this.scrollToChatBottom()
      }
      if (prevState.msgs[prevState.selectedUser].msgs.length === 1) {
        this.scrollToChatBottom()
      }
    }
  }

  componentWillUnmount() {
    console.info('Chat disconnect')
    if (this.socket) {
      this.socket.disconnect()
    }
  }

  resizeInputBar() {
    if (this.chatInputRef.current) {
      const input = this.chatInputRef.current
      input.style.height = input.scrollHeight + 'px'
    }
  }

  scrollToChatBottom() {
    const objDiv = document.getElementById('messages')
    if (objDiv) {
      objDiv.scrollTop = objDiv.scrollHeight
    }
  }

  handleErrors(err) {
    this.setState({
      connected: false,
    })
    console.error(err)
    alert(`Chat error: ${err.message}`)
    try {
      this.socket.disconnect()
    } catch (e) {
      console.warn(e)
    }
  }

  connect(user) {
    const { connected } = this.state
    if (connected) {
      console.warn('Already connected ...')
      return
    }
    // https://socket.io/docs/v4/handling-cors/#Configuration
    const socket = io(`${constants.chatUrl}`, {
      withCredentials: true,
      extraHeaders: {
        'qmining-chat': 'qmining-chat',
      },
    })

    socket.on('connect', () => {
      console.info(`Connected as user ${user}`)
      socket.emit('join', { id: user })
    })
    socket.on('connect_error', (err) => this.handleErrors(err))
    socket.on('connect_failed', (err) => this.handleErrors(err))

    socket.on('prev messages', (data) => {
      const { prevMsgs } = data
      const { user } = this.state
      this.setState({
        msgs: groupPrevMessages(prevMsgs, user),
        connected: true,
      })
    })

    socket.on('chat message read', (data) => {
      const { userReadMsg } = data
      this.partnerSeenChatMessage(userReadMsg)
    })

    socket.on('get chat messages', (data) => {
      const { requestsdMsgs, hasMore } = data
      const { msgs, user, selectedUser } = this.state

      this.setState({
        msgs: addMsgsToGroup(
          Object.keys(msgs).reduce((acc, key) => {
            const msgGroup = msgs[key]
            if (parseInt(key) === selectedUser) {
              acc[key] = {
                ...msgGroup,
                lastLoaded: !hasMore,
                msgs: msgGroup.msgs.map((msg) => {
                  return msg
                }),
              }
            } else {
              acc[key] = msgGroup
            }
            return acc
          }, {}),
          requestsdMsgs.map((msg) => {
            return {
              ...msg,
              isFirstMessage: !hasMore,
            }
          }),
          user
        ),
      })
    })

    socket.on('chat message', (data) => {
      const { msgs, user } = this.state
      this.setState({
        msgs: addMsgToGroup(msgs, data, user),
      })
    })

    this.socket = socket
  }

  sendMsg(currMsg, type) {
    this.chatInputRef.current.style.height = '40px'
    const { msgs, selectedUser, user } = this.state

    if (!currMsg || !currMsg.trim()) {
      return
    }
    if (this.socket && selectedUser) {
      const msg = {
        msg: currMsg.trim(),
        reciever: selectedUser,
        sender: user,
        date: new Date().getTime(),
        unread: 1,
        type: type || 'text',
      }
      this.socket.emit('chat message', msg)
      this.setState({
        currMsg: '',
        msgs: addMsgToGroup(msgs, msg, user),
      })
    }
  }

  partnerSeenChatMessage(chatPartner) {
    const { msgs } = this.state
    this.setState({
      msgs: {
        ...msgs,
        [chatPartner]: {
          ...msgs[chatPartner],
          lastMessage: { ...msgs[chatPartner].lastMessage, unread: 0 },
          msgs: msgs[chatPartner].msgs.map((msg) => {
            if (msg.reciever === chatPartner) {
              return {
                ...msg,
                unread: 0,
              }
            } else {
              return msg
            }
          }),
        },
      },
    })
    this.scrollToChatBottom()
  }

  chatMessageSeen(chatPartner) {
    const { msgs, user } = this.state
    this.socket.emit('chat message read', { chatPartner: chatPartner })
    this.setState({
      msgs: {
        ...msgs,
        [chatPartner]: {
          ...msgs[chatPartner],
          lastMessage: { ...msgs[chatPartner].lastMessage, unread: 0 },
          msgs: msgs[chatPartner].msgs.map((msg) => {
            if (msg.reciever === user) {
              return {
                ...msg,
                unread: 0,
              }
            } else {
              return msg
            }
          }),
        },
      },
    })
  }

  selectedUserChange(val) {
    const { msgs, selectedUser, user } = this.state
    const prevLastMessage = msgs[selectedUser]
      ? msgs[selectedUser].lastMessage
      : null
    if (
      prevLastMessage &&
      prevLastMessage.unread === 1 &&
      prevLastMessage.sender !== user
    ) {
      this.chatMessageSeen(selectedUser)
    }
    this.setState({
      selectedUser: val,
    })
    if (!val || isNaN(val)) {
      return
    }
    const currSelectedMsgs = msgs[val]
    if (!currSelectedMsgs) {
      return
    }
    if (!msgs[val].loaded) {
      this.socket.emit('get chat messages', {
        chatPartner: val,
      })
    }
    const lastMessage = currSelectedMsgs.lastMessage
    if (lastMessage.unread === 1 && lastMessage.sender !== user) {
      this.chatMessageSeen(val)
    }
    try {
      this.router.push(`${this.router.pathname}`, undefined, {
        shallow: true,
      })
    } catch (e) {
      // e
    }
    queryClient.setQueryData('infos', (oldData) => ({
      ...oldData,
      unreads: oldData.unreads.filter((x) => x !== val),
    }))
  }

  renderChatInput() {
    const { currMsg, msgs, selectedUser, user } = this.state
    return (
      <div className={styles.chatInput}>
        <div>
          <input
            onChange={(e) => {
              const file = e.target.files[0]
              const isImage = ['png', 'jpg', 'jpeg', 'gif'].some((ext) => {
                return file.name.toLowerCase().includes(ext)
              })
              uploadFile(file).then((res) => {
                const { path, success } = res
                if (success) {
                  this.sendMsg(`${path}`, isImage ? 'img' : 'file')
                } else {
                  alert('Error uploading image :/')
                  console.error(res)
                }
              })
            }}
            type="file"
            id="actual-btn"
            style={{ display: 'none' }}
          />
          <label
            className={styles.file}
            htmlFor="actual-btn"
            onClick={() => {}}
          >
            📂
          </label>
        </div>
        <textarea
          ref={this.chatInputRef}
          className={styles.msgInput}
          autoFocus
          placeholder={'Üzenet ...'}
          type={'text'}
          value={currMsg}
          tabIndex={0}
          onKeyDown={(e) => {
            const lastMessage = msgs[selectedUser]
              ? msgs[selectedUser].lastMessage
              : null
            if (
              lastMessage &&
              lastMessage.unread === 1 &&
              lastMessage.sender !== user
            ) {
              this.chatMessageSeen(selectedUser)
            }
            if (e.key === 'Enter') {
              if (!e.shiftKey) {
                this.sendMsg(currMsg)
                e.preventDefault()
              }
            }
            this.resizeInputBar()
          }}
          onChange={(e) => {
            this.setState({
              currMsg: e.target.value,
            })
          }}
        />
        <div className={`buttonContainer ${styles.sendButton}`}>
          <div
            onClick={() => {
              this.sendMsg(currMsg)
            }}
          >
            Küldés
          </div>
        </div>
      </div>
    )
  }

  renderHome() {
    return (
      <div className={styles.home}>
        <div>
          <input
            autoFocus
            type={'text'}
            onChange={(e) => {
              const val = parseInt(e.target.value)
              if (!isNaN(val)) {
                this.setState({
                  userInputVal: val,
                })
              }
            }}
            placeholder={'Címzett User ID-ja ...'}
          />
        </div>
        <div className={'buttonContainer'}>
          <div
            onClick={() => {
              const { userInputVal } = this.state
              if (isNaN(userInputVal) || userInputVal <= 0) {
                alert('Érvényes User ID-t adj meg! (számot)')
                return
              }
              this.selectedUserChange(userInputVal)
              this.setState({
                userInputVal: null,
              })
            }}
          >
            Chat!
          </div>
        </div>
        <i style={{ fontSize: '12px' }}>Admin User ID-ja: {'"1"'}</i>
      </div>
    )
  }

  renderMsgs() {
    const { msgs, selectedUser, user } = this.state

    const selectedMsgs = msgs[selectedUser].msgs
    let prevMsg
    return (
      <>
        {this.renderLoadMore()}
        {selectedMsgs.reduce((acc, currMessage, i) => {
          const { date, sender, unread } = currMessage
          const msgStyle = sender === user ? styles.ownMsg : styles.partnerMsg
          const timeString = new Date(date).toLocaleString()
          if (prevMsg && prevMsg.unread === 0 && unread === 1) {
            if (sender === selectedUser) {
              acc.push(<NewMarker key={`marker_${i}`} />)
            } else if (prevMsg.sender !== selectedUser) {
              acc.push(<SeenMarker key={`marker_${i}`} />)
            }
          }
          acc.push(
            <div
              key={i}
              title={timeString}
              className={`${styles.messageContainer} ${
                currMessage.type !== 'img' ? msgStyle : ''
              }`}
            >
              {this.renderMsg(currMessage, i)}
            </div>
          )
          if (i === selectedMsgs.length - 1 && unread === 0) {
            if (sender !== selectedUser) {
              acc.push(<SeenMarker key={`marker_${i}`} />)
            }
          }
          prevMsg = currMessage
          return acc
        }, [])}
      </>
    )
  }

  renderMsg(message, key) {
    const { msg, type } = message

    if (type === 'text') {
      return (
        <div
          className={`${styles.messageEntry}`}
          dangerouslySetInnerHTML={{
            __html: linkifyString(msg, {
              defaultProtocol: 'https',
              target: '_blank',
            }),
          }}
        />
      )
    } else if (type === 'img') {
      return (
        <a key={key} href={msg} rel="noreferrer" target="_blank">
          <img
            onLoad={() => {
              if (!this.state.skipBottomScroll) {
                this.scrollToChatBottom()
              }
            }}
            src={`${constants.apiUrl}${msg}`}
          />
        </a>
      )
    } else if (type === 'file') {
      return (
        <a
          className={`${styles.messageEntry}`}
          key={key}
          href={`${constants.apiUrl}${msg}`}
          rel="noreferrer"
          target="_blank"
        >
          {msg.split('/').slice(-1)}
        </a>
      )
    } else {
      console.error(message)
      return <div key={key}>Invalid msg type {type}</div>
    }
  }

  renderLoadMore() {
    const { selectedUser, msgs } = this.state
    const group = msgs[selectedUser]
    const firstMessage = group.msgs[0]

    if (group.lastLoaded) {
      return (
        <div className={styles.loadMore}>
          <div>Beszélgetés eleje</div>
        </div>
      )
    }

    return (
      <div
        className={`${styles.loadMore} ${styles.loadMoreActive}`}
        onClick={() => {
          this.setState({ skipBottomScroll: true })
          this.socket.emit('get chat messages', {
            chatPartner: selectedUser,
            from: firstMessage.date,
          })
        }}
      >
        <div>Több betöltése ...</div>
      </div>
    )
  }

  renderSidebarEntry(i, key) {
    const { selectedUser, msgs, user } = this.state
    const group = msgs[key]
    const lastMessage = group.lastMessage

    const lastMsgDate = new Date(group.lastMessage.date)
    const date =
      lastMsgDate.getDate() === new Date().getDate()
        ? lastMsgDate.toLocaleTimeString()
        : lastMsgDate.toLocaleDateString()

    return (
      <div
        className={`${styles.group} ${
          lastMessage.unread === 1 && lastMessage.sender !== user
            ? styles.unread
            : ''
        } ${selectedUser === parseInt(key) ? styles.activeSidebarItem : ''}`}
        onClick={() => {
          this.selectedUserChange(parseInt(key))
        }}
        key={i}
      >
        <div>
          <b>#{key}</b>
          <div style={{ fontSize: '12px' }}>{date}</div>
        </div>
        <div>
          {lastMessage.type === 'text' ? lastMessage.msg : 'Csatolt fájl'}
        </div>
      </div>
    )
  }

  renderSidebarEntries() {
    const { msgs } = this.state
    const sorted = Object.keys(msgs).sort((a, b) => {
      return msgs[b].lastMessage.date - msgs[a].lastMessage.date
    })
    return sorted.map((key, i) => {
      return this.renderSidebarEntry(i, key)
    })
  }

  render() {
    const { msgs, connected, selectedUser } = this.state

    return (
      <div className={styles.chat}>
        <Header title={'Chat'} />
        {connected ? (
          <>
            <div className={styles.main}>
              <div className={styles.header}>
                {selectedUser ? `User #${selectedUser}` : ''}
              </div>
              <div className={styles.messages} id={'messages'}>
                {selectedUser === 0 ? this.renderHome() : null}
                {selectedUser && msgs[selectedUser] ? this.renderMsgs() : null}
              </div>
              {selectedUser !== 0 ? this.renderChatInput() : null}
            </div>
            <div className={styles.sidebar}>
              <div className={styles.usersContainer}>
                <div
                  className={`${styles.group} ${
                    selectedUser === 0 ? styles.activeSidebarItem : ''
                  }`}
                  onClick={() => {
                    this.selectedUserChange(0)
                  }}
                >
                  <div>Új beszélgetés</div>
                </div>
                <hr />
                {msgs && this.renderSidebarEntries()}
              </div>
              <span className={styles.spacer} />
              <div></div>
            </div>
          </>
        ) : (
          <div className={styles.loading}>
            <LoadingIndicator />
          </div>
        )}
      </div>
    )
  }
}
