Code Sketch


yoiiiiiiiiiiiiiii
By: Mhalsakant School
Category: Programming
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Dimension
import java.awt.Font
import java.awt.GridLayout
import java.awt.Image
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.image.BufferedImage

import java.io.BufferedReader
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.io.PrintWriter

import java.net.Inet4Address
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.NetworkInterface
import java.net.ServerSocket
import java.net.Socket

import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

import java.security.MessageDigest
import java.security.SecureRandom

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

import java.util.Base64
import java.util.Properties
import java.util.UUID

import javax.imageio.ImageIO

import javax.swing.BorderFactory
import javax.swing.DefaultListModel
import javax.swing.JDialog
import javax.swing.JFileChooser
import javax.swing.JFrame
import javax.swing.JLabel
import javax.swing.JList
import javax.swing.JOptionPane
import javax.swing.JPanel
import javax.swing.JPasswordField
import javax.swing.JScrollPane
import javax.swing.JTextArea
import javax.swing.JTextField
import javax.swing.JButton
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.SwingUtilities
import javax.swing.UIManager
import javax.swing.WindowConstants

import scala.collection.mutable


object LiveChatApp {

  // ==========================================================
  // SETTINGS
  // ==========================================================

  private val APP_NAME =
    "YADNESH LIVE CHAT"

  private val DEFAULT_PORT =
    5050


  // ==========================================================
  // UTILITY
  // ==========================================================

  object Util {

    private val random =
      new SecureRandom()

    private val encoder =
      Base64.getUrlEncoder.withoutPadding()

    private val decoder =
      Base64.getUrlDecoder


    def encode(
        value: String
    ): String = {

      val text =
        if (value == null) "" else value

      encoder.encodeToString(
        text.getBytes(
          StandardCharsets.UTF_8
        )
      )
    }


    def decode(
        value: String
    ): String = {

      if (
        value == null ||
        value.isEmpty
      ) {
        ""
      } else {

        try {

          new String(
            decoder.decode(value),
            StandardCharsets.UTF_8
          )

        } catch {

          case _: Throwable =>
            ""
        }
      }
    }


    def sha256(
        value: String
    ): String = {

      val md =
        MessageDigest.getInstance(
          "SHA-256"
        )

      val result =
        md.digest(
          value.getBytes(
            StandardCharsets.UTF_8
          )
        )

      result
        .map(
          "%02x".format(_)
        )
        .mkString
    }


    def randomHex(
        bytes: Int
    ): String = {

      val data =
        new Array[Byte](bytes)

      random.nextBytes(
        data
      )

      data
        .map(
          "%02x".format(_)
        )
        .mkString
    }


    def secureEquals(
        a: String,
        b: String
    ): Boolean = {

      if (
        a == null ||
        b == null
      ) {
        false
      } else {

        MessageDigest.isEqual(
          a.getBytes(
            StandardCharsets.UTF_8
          ),
          b.getBytes(
            StandardCharsets.UTF_8
          )
        )
      }
    }


    def clean(
        value: String,
        max: Int
    ): String = {

      val text =
        if (value == null) {
          ""
        } else {
          value.trim
        }

      if (
        text.length <= max
      ) {
        text
      } else {
        text.substring(
          0,
          max
        )
      }
    }


    def validUsername(
        username: String
    ): Boolean = {

      username != null &&
      username.matches(
        "[A-Za-z0-9_]{3,20}"
      )
    }


    def timeNow(): String = {

      LocalDateTime
        .now()
        .format(
          DateTimeFormatter.ofPattern(
            "HH:mm:ss"
          )
        )
    }


    def loadImageBase64(
        file: String
    ): String = {

      try {

        val image =
          ImageIO.read(
            new java.io.File(file)
          )

        if (image == null) {

          ""

        } else {

          val out =
            new ByteArrayOutputStream()

          ImageIO.write(
            image,
            "png",
            out
          )

          out.close()

          val bytes =
            out.toByteArray

          if (
            bytes.length >
              350 * 1024
          ) {

            ""

          } else {

            encoder.encodeToString(
              bytes
            )
          }
        }

      } catch {

        case _: Throwable =>
          ""
      }
    }


    def loadImage(
        base64: String
    ): BufferedImage = {

      if (
        base64 == null ||
        base64.isEmpty
      ) {

        null

      } else {

        try {

          val data =
            decoder.decode(
              base64
            )

          ImageIO.read(
            new ByteArrayInputStream(
              data
            )
          )

        } catch {

          case _: Throwable =>
            null
        }
      }
    }


    def setupUIFont(): Unit = {

      val font =
        new Font(
          "Segoe UI",
          Font.PLAIN,
          14
        )

      UIManager.put(
        "Label.font",
        font
      )

      UIManager.put(
        "Button.font",
        font
      )

      UIManager.put(
        "TextField.font",
        font
      )

      UIManager.put(
        "PasswordField.font",
        font
      )

      UIManager.put(
        "TextArea.font",
        font
      )

      UIManager.put(
        "List.font",
        font
      )
    }


    // --------------------------------------------------------
    // FIND LOCAL LAN IPv4 ADDRESSES
    // --------------------------------------------------------

    def localIPv4Addresses():
        Seq[String] = {

      val result =
        mutable.ListBuffer[String]()

      try {

        val interfaces =
          NetworkInterface.getNetworkInterfaces()

        while (
          interfaces != null &&
          interfaces.hasMoreElements
        ) {

          val networkInterface =
            interfaces.nextElement()

          if (
            networkInterface.isUp &&
            !networkInterface.isLoopback &&
            !networkInterface.isVirtual
          ) {

            val addresses =
              networkInterface.getInetAddresses()

            while (
              addresses.hasMoreElements
            ) {

              val address =
                addresses.nextElement()

              if (
                address.isInstanceOf[
                  Inet4Address
                ] &&
                !address.isLoopbackAddress
              ) {

                val ip =
                  address.getHostAddress()

                if (
                  !result.contains(ip)
                ) {

                  result +=
                    ip
                }
              }
            }
          }
        }

      } catch {

        case _: Throwable =>
      }

      if (
        result.isEmpty
      ) {

        Seq(
          "127.0.0.1"
        )

      } else {

        result.toSeq
      }
    }
  }


  // ==========================================================
  // DATA
  // ==========================================================

  case class UserRecord(
      username: String,
      passwordSalt: String,
      passwordVerifier: String,
      bio: String,
      avatar: String
  )


  case class MessageRecord(
      sender: String,
      message: String,
      time: String
  )


  case class PersonEntry(
      username: String,
      bio: String,
      online: Boolean
  ) {

    override def toString: String = {

      if (online) {

        username +
          "   ? ONLINE"

      } else {

        username +
          "   ? OFFLINE"
      }
    }
  }


  case class ChatEntry(
      id: String,
      title: String,
      group: Boolean
  ) {

    override def toString: String =
      title
  }


  // ==========================================================
  // SERVER
  // ==========================================================

  object Server {

    private val lock =
      new Object


    private val users =
      mutable.Map[
        String,
        UserRecord
      ]()


    private val onlineUsers =
      mutable.Map[
        String,
        ClientConnection
      ]()


    private val chatMembers =
      mutable.Map[
        String,
        mutable.Set[String]
      ]()


    private val chatNames =
      mutable.Map[
        String,
        String
      ]()


    private val chatGroups =
      mutable.Map[
        String,
        Boolean
      ]()


    private val histories =
      mutable.Map[
        String,
        mutable.ListBuffer[
          MessageRecord
        ]
      ]()


    private val loginChallenges =
      mutable.Map[
        Socket,
        (String, String)
      ]()


    private val registerChallenges =
      mutable.Map[
        Socket,
        (String, String)
      ]()


    private val recoveryChallenges =
      mutable.Map[
        Socket,
        (String, String)
      ]()


    // SHA-256 hash of the private recovery secret.
    // The plain-text recovery secret is NOT stored here.
    private val recoveryVerifier =
      "7f3f667469f4945d7b63b6831e26f1bd84bd02d57b455542b6144fe2b2a6a92d"


    private val dataFolder =
      Paths.get(
        "YadneshLiveChatData"
      )


    private val profileFolder =
      dataFolder.resolve(
        "profiles"
      )


    private var serverSocket:
        ServerSocket = null


    private var running =
      false


    // ========================================================
    // CLIENT CONNECTION
    // ========================================================

    class ClientConnection(
        val socket: Socket,
        val reader: BufferedReader,
        val writer: PrintWriter
    ) {

      var username =
        ""


      def send(
          line: String
      ): Unit = {

        lock.synchronized {

          try {

            if (
              !socket.isClosed
            ) {

              writer.println(
                line
              )

              writer.flush()
            }

          } catch {

            case _: Throwable =>
          }
        }
      }


      def close(): Unit = {

        try {

          socket.close()

        } catch {

          case _: Throwable =>
        }
      }
    }


    // ========================================================
    // START SERVER
    // ========================================================

    def start(
        port: Int
    ): Boolean = {

      lock.synchronized {

        if (running) {

          true

        } else {

          try {

            Files.createDirectories(
              profileFolder
            )

            loadProfiles()

            serverSocket =
              new ServerSocket()

            serverSocket.setReuseAddress(
              true
            )

            // IMPORTANT FOR WIFI/LAN:
            // listen on every network interface
            serverSocket.bind(
              new InetSocketAddress(
                "0.0.0.0",
                port
              )
            )

            running =
              true

            val thread =
              new Thread(
                new Runnable {

                  override def run(): Unit = {

                    acceptLoop()
                  }
                }
              )

            thread.setDaemon(
              true
            )

            thread.start()

            true

          } catch {

            case _: Throwable =>

              running =
                false

              false
          }
        }
      }
    }


    // ========================================================
    // ACCEPT CONNECTIONS
    // ========================================================

    private def acceptLoop(): Unit = {

      while (
        running
      ) {

        try {

          val clientSocket =
            serverSocket.accept()

          clientSocket.setKeepAlive(
            true
          )

          clientSocket.setTcpNoDelay(
            true
          )

          val reader =
            new BufferedReader(
              new InputStreamReader(
                clientSocket.getInputStream,
                StandardCharsets.UTF_8
              )
            )

          val writer =
            new PrintWriter(
              new OutputStreamWriter(
                clientSocket.getOutputStream,
                StandardCharsets.UTF_8
              ),
              true
            )

          val client =
            new ClientConnection(
              clientSocket,
              reader,
              writer
            )

          val thread =
            new Thread(
              new Runnable {

                override def run(): Unit = {

                  clientLoop(
                    client
                  )
                }
              }
            )

          thread.setDaemon(
            true
          )

          thread.start()

        } catch {

          case _: Throwable =>
        }
      }
    }


    // ========================================================
    // PROFILE FILE
    // ========================================================

    private def profileFile(
        username: String
    ): Path = {

      profileFolder.resolve(
        username +
          ".profile"
      )
    }


    private def saveProfile(
        user: UserRecord
    ): Unit = {

      val properties =
        new Properties()

      properties.setProperty(
        "username",
        user.username
      )

      properties.setProperty(
        "passwordSalt",
        user.passwordSalt
      )

      properties.setProperty(
        "passwordVerifier",
        user.passwordVerifier
      )

      properties.setProperty(
        "bio",
        user.bio
      )

      properties.setProperty(
        "avatar",
        user.avatar
      )

      val out =
        Files.newOutputStream(
          profileFile(
            user.username
          )
        )

      try {

        properties.store(
          out,
          "Yadnesh Live Chat Account"
        )

      } finally {

        out.close()
      }
    }


    private def loadProfiles(): Unit = {

      users.clear()

      if (
        !Files.exists(
          profileFolder
        )
      ) {

        return
      }

      val stream =
        Files.list(
          profileFolder
        )

      try {

        val iterator =
          stream.iterator()

        while (
          iterator.hasNext
        ) {

          val file =
            iterator.next()

          if (
            file.toString
              .endsWith(
                ".profile"
              )
          ) {

            try {

              val properties =
                new Properties()

              val input =
                Files.newInputStream(
                  file
                )

              try {

                properties.load(
                  input
                )

              } finally {

                input.close()
              }

              val username =
                properties.getProperty(
                  "username",
                  ""
                )

              if (
                Util.validUsername(
                  username
                )
              ) {

                val user =
                  UserRecord(

                    username,

                    properties.getProperty(
                      "passwordSalt",
                      ""
                    ),

                    properties.getProperty(
                      "passwordVerifier",
                      ""
                    ),

                    properties.getProperty(
                      "bio",
                      ""
                    ),

                    properties.getProperty(
                      "avatar",
                      ""
                    )
                  )

                users.put(
                  username.toLowerCase,
                  user
                )
              }

            } catch {

              case _: Throwable =>
            }
          }
        }

      } finally {

        stream.close()
      }
    }


    // ========================================================
    // CHAT HELPERS
    // ========================================================

    private def directChatId(
        first: String,
        second: String
    ): String = {

      val sorted =
        Seq(
          first.toLowerCase,
          second.toLowerCase
        ).sorted

      "DM:" +
        sorted.head +
        ":" +
        sorted(1)
    }


    private def addMember(
        chatId: String,
        username: String
    ): Unit = {

      val members =
        chatMembers.getOrElseUpdate(
          chatId,
          mutable.Set[String]()
        )

      members +=
        username
    }


    private def sendTo(
        username: String,
        line: String
    ): Unit = {

      onlineUsers
        .get(
          username.toLowerCase
        )
        .foreach {
          client =>

            client.send(
              line
            )
        }
    }


    private def broadcast(
        chatId: String,
        line: String
    ): Unit = {

      chatMembers
        .get(chatId)
        .foreach {
          members =>

            members.foreach {
              username =>

                sendTo(
                  username,
                  line
                )
            }
        }
    }


    private def addChatForUser(
        chatId: String,
        username: String
    ): Unit = {

      val isGroup =
        chatGroups.getOrElse(
          chatId,
          false
        )

      val title = {

        if (isGroup) {

          "#" +
            chatNames.getOrElse(
              chatId,
              "Group"
            )

        } else {

          val members =
            chatMembers.getOrElse(
              chatId,
              mutable.Set[String]()
            )

          val other =
            members.find {
              member =>
                !member.equalsIgnoreCase(
                  username
                )
            }

          other match {

            case Some(name) =>
              "@" + name

            case None =>
              "@Chat"
          }
        }
      }

      sendTo(
        username,
        "CHAT|" +
          Util.encode(
            chatId
          ) +
          "|" +
          Util.encode(
            title
          ) +
          "|" +
          (
            if (isGroup) {
              "1"
            } else {
              "0"
            }
          )
      )
    }


    private def sendUserChats(
        username: String
    ): Unit = {

      chatMembers.keys.foreach {
        chatId =>

          chatMembers
            .get(chatId)
            .foreach {
              members =>

                if (
                  members.contains(
                    username
                  )
                ) {

                  addChatForUser(
                    chatId,
                    username
                  )
                }
            }
      }
    }


    // ========================================================
    // SEND HISTORY
    // ========================================================

    private def sendHistory(
        client: ClientConnection,
        chatId: String
    ): Unit = {

      val list =
        histories.getOrElse(
          chatId,
          mutable.ListBuffer[
            MessageRecord
          ]()
        )

      val start =
        math.max(
          0,
          list.length - 100
        )

      val recent =
        list.slice(
          start,
          list.length
        )

      val result =
        new StringBuilder()

      result.append(
        "HISTORY|"
      )

      result.append(
        Util.encode(
          chatId
        )
      )

      result.append(
        "|" +
          recent.length
      )

      recent.foreach {
        message =>

          result.append(
            "|" +
              Util.encode(
                message.sender
              )
          )

          result.append(
            "|" +
              Util.encode(
                message.time
              )
          )

          result.append(
            "|" +
              Util.encode(
                message.message
              )
          )
      }

      client.send(
        result.toString
      )
    }


    // ========================================================
    // CLIENT LOOP
    // ========================================================

    private def clientLoop(
        client: ClientConnection
    ): Unit = {

      try {

        while (
          !client.socket.isClosed
        ) {

          val line =
            client.reader.readLine()

          if (
            line == null
          ) {

            throw new IOException(
              "Disconnected"
            )
          }

          processCommand(
            client,
            line
          )
        }

      } catch {

        case _: Throwable =>
      }


      lock.synchronized {

        val username =
          client.username

        if (
          username != null &&
          username.nonEmpty
        ) {

          onlineUsers.get(
            username.toLowerCase
          ) match {

            case Some(existing)
                if existing eq client =>

              onlineUsers.remove(
                username.toLowerCase
              )

              broadcastPresence(
                username
              )

            case _ =>
          }
        }

        loginChallenges.remove(
          client.socket
        )

        registerChallenges.remove(
          client.socket
        )

        recoveryChallenges.remove(
          client.socket
        )
      }

      client.close()
    }


    // ========================================================
    // COMMAND PROCESSOR
    // ========================================================

    private def processCommand(
        client: ClientConnection,
        line: String
    ): Unit = {

      val parts =
        line.split(
          "\\|",
          -1
        )

      if (
        parts.length == 0
      ) {

        return
      }

      val command =
        parts(0)


      // ======================================================
      // REGISTER START
      // ======================================================

      if (
        command ==
          "REGISTER_START"
      ) {

        if (
          parts.length < 2
        ) {
          return
        }

        val username =
          Util.decode(
            parts(1)
          ).trim

        if (
          !Util.validUsername(
            username
          )
        ) {

          client.send(
            "ERROR|" +
              Util.encode(
                "Username must be 3-20 characters."
              )
          )

          return
        }

        lock.synchronized {

          if (
            users.contains(
              username.toLowerCase
            )
          ) {

            client.send(
              "ERROR|" +
                Util.encode(
                  "Username already exists."
                )
            )

          } else {

            val salt =
              Util.randomHex(
                16
              )

            registerChallenges.put(
              client.socket,
              (
                username,
                salt
              )
            )

            client.send(
              "REGISTER_CHALLENGE|" +
                Util.encode(
                  salt
                )
            )
          }
        }

        return
      }


      // ======================================================
      // REGISTER FINISH
      // ======================================================

      if (
        command ==
          "REGISTER_FINISH"
      ) {

        if (
          parts.length < 6
        ) {
          return
        }

        lock.synchronized {

          registerChallenges
            .get(
              client.socket
            ) match {

            case None =>

              client.send(
                "ERROR|" +
                  Util.encode(
                    "Registration expired."
                  )
              )

            case Some(
              (
                username,
                salt
              )
              ) =>

              if (
                users.contains(
                  username.toLowerCase
                )
              ) {

                client.send(
                  "ERROR|" +
                    Util.encode(
                      "Username already exists."
                    )
                )

                registerChallenges.remove(
                  client.socket
                )

              } else {

                val passwordVerifier =
                  Util.decode(
                    parts(2)
                  )

                val bio =
                  Util.clean(
                    Util.decode(
                      parts(3)
                    ),
                    500
                  )

                val avatar =
                  Util.decode(
                    parts(4)
                  )

                val validAvatar = {

                  if (
                    avatar.length >
                      480000
                  ) {
                    ""
                  } else {
                    avatar
                  }
                }

                val user =
                  UserRecord(
                    username,
                    salt,
                    passwordVerifier,
                    bio,
                    validAvatar
                  )

                users.put(
                  username.toLowerCase,
                  user
                )

                saveProfile(
                  user
                )

                registerChallenges.remove(
                  client.socket
                )

                client.username =
                  username

                onlineUsers.put(
                  username.toLowerCase,
                  client
                )

                client.send(
                  "LOGIN_OK|" +
                    Util.encode(
                      username
                    )
                )

                sendUserChats(
                  username
                )

                broadcastPresence(
                  username
                )
              }
          }
        }

        return
      }


      // ======================================================
      // LOGIN START
      // ======================================================

      if (
        command ==
          "LOGIN_START"
      ) {

        if (
          parts.length < 2
        ) {
          return
        }

        val username =
          Util.decode(
            parts(1)
          ).trim

        lock.synchronized {

          users.get(
            username.toLowerCase
          ) match {

            case None =>

              client.send(
                "ERROR|" +
                  Util.encode(
                    "Account not found."
                  )
              )

            case Some(user) =>

              val nonce =
                Util.randomHex(
                  32
                )

              loginChallenges.put(
                client.socket,
                (
                  user.username,
                  nonce
                )
              )

              client.send(
                "LOGIN_CHALLENGE|" +
                  Util.encode(
                    user.passwordSalt
                  ) +
                  "|" +
                  Util.encode(
                    nonce
                  )
              )
          }
        }

        return
      }


      // ======================================================
      // LOGIN FINISH
      // ======================================================

      if (
        command ==
          "LOGIN_FINISH"
      ) {

        if (
          parts.length < 3
        ) {
          return
        }

        lock.synchronized {

          loginChallenges
            .get(
              client.socket
            ) match {

            case None =>

              client.send(
                "ERROR|" +
                  Util.encode(
                    "Login expired."
                  )
              )

            case Some(
              (
                username,
                nonce
              )
              ) =>

              users.get(
                username.toLowerCase
              ) match {

                case None =>

                  client.send(
                    "ERROR|" +
                      Util.encode(
                        "Account not found."
                      )
                  )

                case Some(user) =>

                  val proof =
                    Util.decode(
                      parts(2)
                    )

                  val expected =
                    Util.sha256(
                      user.passwordVerifier +
                        nonce
                    )

                  if (
                    Util.secureEquals(
                      proof,
                      expected
                    )
                  ) {

                    onlineUsers
                      .get(
                        username.toLowerCase
                      )
                      .foreach {
                        old =>
                          old.close()
                      }

                    client.username =
                      username

                    onlineUsers.put(
                      username.toLowerCase,
                      client
                    )

                    loginChallenges.remove(
                      client.socket
                    )

                    client.send(
                      "LOGIN_OK|" +
                        Util.encode(
                          username
                        )
                    )

                    sendUserChats(
                      username
                    )

                    broadcastPresence(
                      username
                    )

                  } else {

                    loginChallenges.remove(
                      client.socket
                    )

                    client.send(
                      "ERROR|" +
                        Util.encode(
                          "Wrong password."
                        )
                    )
                  }
              }
          }
        }

        return
      }


      // ======================================================
      // PASSWORD RECOVERY START
      // ======================================================

      if (
        command ==
          "RECOVERY_START"
      ) {

        if (
          parts.length < 2
        ) {
          return
        }

        val username =
          Util.decode(
            parts(1)
          ).trim

        lock.synchronized {

          users.get(
            username.toLowerCase
          ) match {

            case None =>

              client.send(
                "ERROR|" +
                  Util.encode(
                    "Password recovery failed."
                  )
              )

            case Some(user) =>

              val nonce =
                Util.randomHex(
                  32
                )

              recoveryChallenges.put(
                client.socket,
                (
                  user.username,
                  nonce
                )
              )

              client.send(
                "RECOVERY_CHALLENGE|" +
                  Util.encode(
                    nonce
                  )
              )
          }
        }

        return
      }


      // ======================================================
      // PASSWORD RECOVERY FINISH
      // ======================================================

      if (
        command ==
          "RECOVERY_FINISH"
      ) {

        if (
          parts.length < 5
        ) {
          return
        }

        lock.synchronized {

          recoveryChallenges
            .get(
              client.socket
            ) match {

            case None =>

              client.send(
                "ERROR|" +
                  Util.encode(
                    "Recovery expired."
                  )
              )

            case Some(
              (
                username,
                nonce
              )
              ) =>

              val proof =
                Util.decode(
                  parts(2)
                )

              val newSalt =
                Util.decode(
                  parts(3)
                )

              val newVerifier =
                Util.decode(
                  parts(4)
                )

              val expected =
                Util.sha256(
                  recoveryVerifier +
                    nonce
                )

              if (
                Util.secureEquals(
                  proof,
                  expected
                )
              ) {

                users.get(
                  username.toLowerCase
                ) match {

                  case Some(oldUser) =>

                    val updated =
                      oldUser.copy(
                        passwordSalt =
                          newSalt,
                        passwordVerifier =
                          newVerifier
                      )

                    users.put(
                      username.toLowerCase,
                      updated
                    )

                    saveProfile(
                      updated
                    )

                    client.send(
                      "RECOVERY_OK"
                    )

                  case None =>

                    client.send(
                      "ERROR|" +
                        Util.encode(
                          "Recovery failed."
                        )
                    )
                }

              } else {

                client.send(
                  "ERROR|" +
                    Util.encode(
                      "Recovery failed."
                    )
                )
              }

              recoveryChallenges.remove(
                client.socket
              )
          }
        }

        return
      }


      // ======================================================
      // LOGIN REQUIRED
      // ======================================================

      val currentUser =
        client.username

      if (
        currentUser == null ||
        currentUser.isEmpty
      ) {

        client.send(
          "ERROR|" +
            Util.encode(
              "Please login first."
            )
        )

        return
      }


      // ======================================================
      // SEARCH
      // ======================================================

      if (
        command ==
          "SEARCH"
      ) {

        val query =
          if (
            parts.length >= 2
          ) {
            Util.decode(
              parts(1)
            ).trim.toLowerCase
          } else {
            ""
          }

        val result =
          users.values
            .filter {
              user =>

                user.username
                  .toLowerCase
                  .contains(
                    query
                  ) &&
                !user.username
                  .equalsIgnoreCase(
                    currentUser
                  )
            }
            .toSeq
            .sortBy(
              _.username.toLowerCase
            )
            .take(100)

        val out =
          new StringBuilder()

        out.append(
          "SEARCH_RESULT|"
        )

        out.append(
          result.length
        )

        result.foreach {
          user =>

            out.append(
              "|" +
                Util.encode(
                  user.username
                )
            )

            out.append(
              "|" +
                Util.encode(
                  user.bio
                )
            )

            out.append(
              "|" +
                (
                  if (
                    onlineUsers.contains(
                      user.username.toLowerCase
                    )
                  ) {
                    "1"
                  } else {
                    "0"
                  }
                )
            )
        }

        client.send(
          out.toString
        )

        return
      }


      // ======================================================
      // PROFILE
      // ======================================================

      if (
        command ==
          "PROFILE"
      ) {

        if (
          parts.length < 2
        ) {
          return
        }

        val target =
          Util.decode(
            parts(1)
          ).trim

        users.get(
          target.toLowerCase
        ) match {

          case None =>

            client.send(
              "ERROR|" +
                Util.encode(
                  "Profile not found."
                )
            )

          case Some(user) =>

            client.send(
              "PROFILE_DATA|" +
                Util.encode(
                  user.username
                ) +
                "|" +
                Util.encode(
                  user.bio
                ) +
                "|" +
                Util.encode(
                  user.avatar
                ) +
                "|" +
                (
                  if (
                    onlineUsers.contains(
                      user.username.toLowerCase
                    )
                  ) {
                    "1"
                  } else {
                    "0"
                  }
                )
            )
        }

        return
      }


      // ======================================================
      // UPDATE PROFILE
      // ======================================================

      if (
        command ==
          "UPDATE_PROFILE"
      ) {

        if (
          parts.length < 3
        ) {
          return
        }

        lock.synchronized {

          users.get(
            currentUser.toLowerCase
          ) match {

            case None =>

              client.send(
                "ERROR|" +
                  Util.encode(
                    "Profile not found."
                  )
              )

            case Some(oldUser) =>

              val bio =
                Util.clean(
                  Util.decode(
                    parts(1)
                  ),
                  500
                )

              var avatar =
                Util.decode(
                  parts(2)
                )

              if (
                avatar.length >
                  480000
              ) {

                avatar =
                  oldUser.avatar
              }

              val updated =
                oldUser.copy(
                  bio =
                    bio,
                  avatar =
                    avatar
                )

              users.put(
                currentUser.toLowerCase,
                updated
              )

              saveProfile(
                updated
              )

              client.send(
                "PROFILE_SAVED"
              )

              broadcastProfileChange(
                currentUser
              )
          }
        }

        return
      }


      // ======================================================
      // OPEN DIRECT CHAT
      // ======================================================

      if (
        command ==
          "OPEN_DM"
      ) {

        if (
          parts.length < 2
        ) {
          return
        }

        val target =
          Util.decode(
            parts(1)
          ).trim

        if (
          !users.contains(
            target.toLowerCase
          )
        ) {

          client.send(
            "ERROR|" +
              Util.encode(
                "User does not exist."
              )
          )

        } else if (
          target.equalsIgnoreCase(
            currentUser
          )
        ) {

          client.send(
            "ERROR|" +
              Util.encode(
                "You cannot chat with yourself."
              )
          )

        } else {

          lock.synchronized {

            val chatId =
              directChatId(
                currentUser,
                target
              )

            addMember(
              chatId,
              currentUser
            )

            addMember(
              chatId,
              target
            )

            chatGroups.put(
              chatId,
              false
            )

            chatNames.put(
              chatId,
              ""
            )

            histories.getOrElseUpdate(
              chatId,
              mutable.ListBuffer[
                MessageRecord
              ]()
            )

            addChatForUser(
              chatId,
              currentUser
            )

            addChatForUser(
              chatId,
              target
            )

            sendHistory(
              client,
              chatId
            )
          }
        }

        return
      }


      // ======================================================
      // CREATE GROUP
      // ======================================================

      if (
        command ==
          "CREATE_GROUP"
      ) {

        if (
          parts.length < 3
        ) {
          return
        }

        val groupName =
          Util.clean(
            Util.decode(
              parts(1)
            ),
            40
          )

        val count =
          try {
            parts(2).toInt
          } catch {
            case _: Throwable =>
              0
          }

        if (
          groupName.isEmpty ||
          count < 1 ||
          count > 20 ||
          parts.length <
            3 + count
        ) {

          client.send(
            "ERROR|" +
              Util.encode(
                "Invalid group."
              )
          )

          return
        }

        lock.synchronized {

          val members =
            mutable.Set[String]()

          members +=
            currentUser

          var i =
            0

          while (
            i < count
          ) {

            val name =
              Util.decode(
                parts(
                  3 + i
                )
              ).trim

            if (
              users.contains(
                name.toLowerCase
              ) &&
              !name.equalsIgnoreCase(
                currentUser
              )
            ) {

              members +=
                users(
                  name.toLowerCase
                ).username
            }

            i +=
              1
          }

          if (
            members.size < 2
          ) {

            client.send(
              "ERROR|" +
                Util.encode(
                  "Select at least one other person."
                )
            )

          } else {

            val chatId =
              "GROUP:" +
                UUID.randomUUID()
                  .toString

            chatMembers.put(
              chatId,
              members
            )

            chatGroups.put(
              chatId,
              true
            )

            chatNames.put(
              chatId,
              groupName
            )

            histories.put(
              chatId,
              mutable.ListBuffer[
                MessageRecord
              ]()
            )

            members.foreach {
              username =>

                addChatForUser(
                  chatId,
                  username
                )
            }

            client.send(
              "GROUP_CREATED|" +
                Util.encode(
                  groupName
                )
            )
          }
        }

        return
      }


      // ======================================================
      // OPEN CHAT
      // ======================================================

      if (
        command ==
          "OPEN_CHAT"
      ) {

        if (
          parts.length < 2
        ) {
          return
        }

        val chatId =
          Util.decode(
            parts(1)
          )

        val member =
          chatMembers
            .get(chatId)
            .exists {
              members =>
                members.contains(
                  currentUser
                )
            }

        if (
          member
        ) {

          sendHistory(
            client,
            chatId
          )

        } else {

          client.send(
            "ERROR|" +
              Util.encode(
                "You are not a member of this chat."
              )
          )
        }

        return
      }


      // ======================================================
      // SEND MESSAGE
      // ======================================================

      if (
        command ==
          "SEND_MESSAGE"
      ) {

        if (
          parts.length < 3
        ) {
          return
        }

        val chatId =
          Util.decode(
            parts(1)
          )

        val message =
          Util.clean(
            Util.decode(
              parts(2)
            ),
            2000
          )

        val allowed =
          chatMembers
            .get(chatId)
            .exists {
              members =>
                members.contains(
                  currentUser
                )
            }

        if (
          allowed &&
          message.nonEmpty
        ) {

          val record =
            MessageRecord(
              currentUser,
              message,
              Util.timeNow()
            )

          val list =
            histories.getOrElseUpdate(
              chatId,
              mutable.ListBuffer[
                MessageRecord
              ]()
            )

          list +=
            record

          while (
            list.length > 300
          ) {

            list.remove(0)
          }

          broadcast(
            chatId,
            "MESSAGE|" +
              Util.encode(
                chatId
              ) +
              "|" +
              Util.encode(
                record.sender
              ) +
              "|" +
              Util.encode(
                record.time
              ) +
              "|" +
              Util.encode(
                record.message
              )
          )
        }

        return
      }


      // ======================================================
      // PING
      // ======================================================

      if (
        command ==
          "PING"
      ) {

        client.send(
          "PONG"
        )

        return
      }
    }


    // ========================================================
    // PRESENCE
    // ========================================================

    private def broadcastPresence(
        username: String
    ): Unit = {

      val status =
        if (
          onlineUsers.contains(
            username.toLowerCase
          )
        ) {
          "1"
        } else {
          "0"
        }

      val line =
        "PRESENCE|" +
          Util.encode(
            username
          ) +
          "|" +
          status

      onlineUsers.values.foreach {
        client =>

          client.send(
            line
          )
      }
    }


    // ========================================================
    // PROFILE CHANGE
    // ========================================================

    private def broadcastProfileChange(
        username: String
    ): Unit = {

      onlineUsers.values.foreach {
        client =>

          client.send(
            "PROFILE_CHANGED|" +
              Util.encode(
                username
              )
          )
      }
    }
  }


  // ==========================================================
  // CLIENT NETWORK
  // ==========================================================

  class ChatClient {

    var socket:
        Socket = null

    var reader:
        BufferedReader = null

    var writer:
        PrintWriter = null

    var connected =
      false

    var username =
      ""

    var password =
      ""

    var registerUsername =
      ""

    var registerPassword =
      ""

    var registerBio =
      ""

    var registerAvatar =
      ""

    var recoverySecret =
      ""

    var recoveryUsername =
      ""

    var recoveryNewPassword =
      ""


    var ownBio =
      ""

    var ownAvatar =
      ""


    var callback:
        String => Unit =
        (_: String) => {}


    // ========================================================
    // CONNECT
    // ========================================================

    def connect(
        host: String,
        port: Int
    ): Boolean = {

      if (
        connected
      ) {

        return true
      }

      try {

        socket =
          new Socket()

        socket.setKeepAlive(
          true
        )

        socket.setTcpNoDelay(
          true
        )

        socket.connect(
          new InetSocketAddress(
            host,
            port
          ),
          6000
        )

        reader =
          new BufferedReader(
            new InputStreamReader(
              socket.getInputStream,
              StandardCharsets.UTF_8
            )
          )

        writer =
          new PrintWriter(
            new OutputStreamWriter(
              socket.getOutputStream,
              StandardCharsets.UTF_8
            ),
            true
          )

        connected =
          true

        val thread =
          new Thread(
            new Runnable {

              override def run(): Unit = {

                readLoop()
              }
            }
          )

        thread.setDaemon(
          true
        )

        thread.start()

        true

      } catch {

        case _: Throwable =>

          connected =
            false

          false
      }
    }


    def send(
        line: String
    ): Unit = {

      if (
        connected &&
        writer != null
      ) {

        try {

          writer.println(
            line
          )

          writer.flush()

        } catch {

          case _: Throwable =>
        }
      }
    }


    def close(): Unit = {

      connected =
        false

      try {

        if (
          socket != null
        ) {

          socket.close()
        }

      } catch {

        case _: Throwable =>
      }
    }


    // ========================================================
    // READ LOOP
    // ========================================================

    private def readLoop(): Unit = {

      try {

        while (
          connected
        ) {

          val line =
            reader.readLine()

          if (
            line == null
          ) {

            throw new IOException(
              "Server closed"
            )
          }

          val event =
            line

          SwingUtilities.invokeLater(
            new Runnable {

              override def run(): Unit = {

                processEvent(
                  event
                )
              }
            }
          )
        }

      } catch {

        case _: Throwable =>

          connected =
            false
      }
    }


    // ========================================================
    // EVENTS
    // ========================================================

    private def processEvent(
        line: String
    ): Unit = {

      val parts =
        line.split(
          "\\|",
          -1
        )

      if (
        parts.length == 0
      ) {
        return
      }

      parts(0) match {

        // ----------------------------------------------------
        // LOGIN
        // ----------------------------------------------------

        case "LOGIN_CHALLENGE" =>

          if (
            parts.length >= 3
          ) {

            val salt =
              Util.decode(
                parts(1)
              )

            val nonce =
              Util.decode(
                parts(2)
              )

            val verifier =
              Util.sha256(
                salt +
                  password
              )

            val proof =
              Util.sha256(
                verifier +
                  nonce
              )

            send(
              "LOGIN_FINISH|" +
                Util.encode(
                  username
                ) +
                "|" +
                Util.encode(
                  proof
                )
            )
          }


        // ----------------------------------------------------
        // REGISTER
        // ----------------------------------------------------

        case "REGISTER_CHALLENGE" =>

          if (
            parts.length >= 2
          ) {

            val salt =
              Util.decode(
                parts(1)
              )

            val verifier =
              Util.sha256(
                salt +
                  registerPassword
              )

            send(
              "REGISTER_FINISH|" +
                Util.encode(
                  registerUsername
                ) +
                "|" +
                Util.encode(
                  verifier
                ) +
                "|" +
                Util.encode(
                  registerBio
                ) +
                "|" +
                Util.encode(
                  registerAvatar
                ) +
                "|" +
                Util.encode(
                  "new"
                )
            )
          }


        // ----------------------------------------------------
        // RECOVERY
        // ----------------------------------------------------

        case "RECOVERY_CHALLENGE" =>

          if (
            parts.length >= 2
          ) {

            val nonce =
              Util.decode(
                parts(1)
              )

            val recoveryHash =
              Util.sha256(
                recoverySecret
              )

            val proof =
              Util.sha256(
                recoveryHash +
                  nonce
              )

            val newSalt =
              Util.randomHex(
                16
              )

            val newVerifier =
              Util.sha256(
                newSalt +
                  recoveryNewPassword
              )

            send(
              "RECOVERY_FINISH|" +
                Util.encode(
                  recoveryUsername
                ) +
                "|" +
                Util.encode(
                  proof
                ) +
                "|" +
                Util.encode(
                  newSalt
                ) +
                "|" +
                Util.encode(
                  newVerifier
                )
            )

            recoverySecret =
              ""

            recoveryNewPassword =
              ""
          }


        // ----------------------------------------------------
        // LOGIN OK
        // ----------------------------------------------------

        case "LOGIN_OK" =>

          if (
            parts.length >= 2
          ) {

            username =
              Util.decode(
                parts(1)
              )

            password =
              ""

            callback(
              line
            )
          }


        case "RECOVERY_OK" =>
          callback(line)


        case "SEARCH_RESULT" =>
          callback(line)


        case "PROFILE_DATA" =>
          callback(line)


        case "PROFILE_SAVED" =>
          callback(line)


        case "PROFILE_CHANGED" =>
          callback(line)


        case "CHAT" =>
          callback(line)


        case "HISTORY" =>
          callback(line)


        case "MESSAGE" =>
          callback(line)


        case "PRESENCE" =>
          callback(line)


        case "GROUP_CREATED" =>
          callback(line)


        case "ERROR" =>

          val message =
            if (
              parts.length >= 2
            ) {
              Util.decode(
                parts(1)
              )
            } else {
              "Unknown error."
            }

          JOptionPane.showMessageDialog(
            null,
            message,
            APP_NAME,
            JOptionPane.ERROR_MESSAGE
          )


        case _ =>
      }
    }
  }


  // ==========================================================
  // LOGIN FRAME
  // ==========================================================

  class LoginFrame {

    private val frame =
      new JFrame(
        APP_NAME
      )

    private val client =
      new ChatClient


    private val hostField =
      new JTextField(
        "127.0.0.1"
      )

    private val portField =
      new JTextField(
        DEFAULT_PORT.toString
      )

    private val usernameField =
      new JTextField()

    private val passwordField =
      new JPasswordField()


    // ========================================================
    // SHOW
    // ========================================================

    def show(): Unit = {

      Util.setupUIFont()

      frame.setTitle(
        APP_NAME +
          " - Login"
      )

      frame.setDefaultCloseOperation(
        WindowConstants.EXIT_ON_CLOSE
      )

      frame.setSize(
        760,
        570
      )

      frame.setLocationRelativeTo(
        null
      )


      val root =
        new JPanel(
          new BorderLayout(
            12,
            12
          )
        )

      root.setBorder(
        BorderFactory.createEmptyBorder(
          20,
          20,
          20,
          20
        )
      )


      // ------------------------------------------------------
      // TITLE
      // ------------------------------------------------------

      val title =
        new JLabel(
          "<html>" +
            "<font size='6'><b>YADNESH LIVE CHAT</b></font><br>" +
            "<font size='3'>Wi-Fi PC ? PC LIVE CHAT</font>" +
            "</html>"
        )

      root.add(
        title,
        BorderLayout.NORTH
      )


      // ------------------------------------------------------
      // FORM
      // ------------------------------------------------------

      val form =
        new JPanel(
          new GridLayout(
            4,
            2,
            8,
            8
          )
        )

      form.add(
        new JLabel(
          "Server IP / Host:"
        )
      )

      form.add(
        hostField
      )


      form.add(
        new JLabel(
          "Port:"
        )
      )

      form.add(
        portField
      )


      form.add(
        new JLabel(
          "Username:"
        )
      )

      form.add(
        usernameField
      )


      form.add(
        new JLabel(
          "Password:"
        )
      )

      form.add(
        passwordField
      )


      root.add(
        form,
        BorderLayout.CENTER
      )


      // ------------------------------------------------------
      // BUTTON PANEL
      // ------------------------------------------------------

      val loginButton =
        new JButton(
          "LOGIN"
        )

      val registerButton =
        new JButton(
          "CREATE ACCOUNT"
        )

      val recoveryButton =
        new JButton(
          "FORGOT PASSWORD"
        )

      val hostButton =
        new JButton(
          "HOST SERVER"
        )


      val buttons =
        new JPanel(
          new GridLayout(
            2,
            2,
            8,
            8
          )
        )

      buttons.add(
        loginButton
      )

      buttons.add(
        registerButton
      )

      buttons.add(
        recoveryButton
      )

      buttons.add(
        hostButton
      )


      root.add(
        buttons,
        BorderLayout.SOUTH
      )


      // ======================================================
      // LOGIN
      // ======================================================

      loginButton.addActionListener(
        new ActionListener {

          override def actionPerformed(
              e: ActionEvent
          ): Unit = {

            login()
          }
        }
      )


      // ======================================================
      // REGISTER
      // ======================================================

      registerButton.addActionListener(
        new ActionListener {

          override def actionPerformed(
              e: ActionEvent
          ): Unit = {

            registerDialog()
          }
        }
      )


      // ======================================================
      // RECOVERY
      // ======================================================

      recoveryButton.addActionListener(
        new ActionListener {

          override def actionPerformed(
              e: ActionEvent
          ): Unit = {

            recoveryDialog()
          }
        }
      )


      // ======================================================
      // HOST
      // ======================================================

      hostButton.addActionListener(
        new ActionListener {

          override def actionPerformed(
              e: ActionEvent
          ): Unit = {

            startHost()
          }
        }
      )


      frame.setContentPane(
        root
      )

      frame.setVisible(
        true
      )
    }


    // ========================================================
    // PORT
    // ========================================================

    private def getPort(): Int = {

      try {

        val value =
          portField
            .getText
            .trim
            .toInt

        if (
          value >= 1024 &&
          value <= 65535
        ) {

          value

        } else {

          -1
        }

      } catch {

        case _: Throwable =>
          -1
      }
    }


    // ========================================================
    // CONNECT
    // ========================================================

    private def connectToServer():
        Boolean = {

      val host =
        hostField
          .getText
          .trim

      val port =
        getPort()

      if (
        host.isEmpty ||
        port <= 0
      ) {

        JOptionPane.showMessageDialog(
          frame,
          "Enter correct IP and port.",
          APP_NAME,
          JOptionPane.WARNING_MESSAGE
        )

        false

      } else {

        val ok =
          client.connect(
            host,
            port
          )

        if (!ok) {

          JOptionPane.showMessageDialog(
            frame,
            "Connection failed.\n\n" +
              "Host: " +
              host +
              "\n" +
              "Port: " +
              port +
              "\n\n" +
              "Check Wi-Fi, IP and Windows Firewall.",
            APP_NAME,
            JOptionPane.ERROR_MESSAGE
          )
        }

        ok
      }
    }


    // ========================================================
    // LOGIN
    // ========================================================

    private def login(): Unit = {

      val username =
        usernameField
          .getText
          .trim

      val password =
        new String(
          passwordField.getPassword
        )


      if (
        !Util.validUsername(
          username
        )
      ) {

        JOptionPane.showMessageDialog(
          frame,
          "Username must be 3-20 characters.",
          APP_NAME,
          JOptionPane.WARNING_MESSAGE
        )

        return
      }


      if (
        password.isEmpty
      ) {

        JOptionPane.showMessageDialog(
          frame,
          "Enter password.",
          APP_NAME,
          JOptionPane.WARNING_MESSAGE
        )

        return
      }


      if (
        connectToServer()
      ) {

        client.username =
          username

        client.password =
          password

        client.callback =
          loginEvent

        client.send(
          "LOGIN_START|" +
            Util.encode(
              username
            )
        )
      }
    }


    // ========================================================
    // LOGIN EVENT
    // ========================================================

    private def loginEvent(
        line: String
    ): Unit = {

      if (
        line.startsWith(
          "LOGIN_OK|"
        )
      ) {

        openMain()
      }
    }


    // ========================================================
    // REGISTER DIALOG
    // ========================================================

    private def registerDialog(): Unit = {

      val dialog =
        new JDialog(
          frame,
          "Create New Account",
          true
        )

      dialog.setSize(
        650,
        560
      )

      dialog.setLocationRelativeTo(
        frame
      )


      val username =
        new JTextField()

      val password =
        new JPasswordField()

      val confirm =
        new JPasswordField()

      val bio =
        new JTextField(
          "Hello! I use Yadnesh Live Chat."
        )


      val chooseImage =
        new JButton(
          "CHOOSE PROFILE PHOTO"
        )

      val imageLabel =
        new JLabel(
          "No image selected"
        )

      var avatar =
        ""


      val panel =
        new JPanel(
          new GridLayout(
            6,
            2,
            8,
            8
          )
        )

      panel.setBorder(
        BorderFactory.createEmptyBorder(
          15,
          15,
          15,
          15
        )
      )


      panel.add(
        new JLabel(
          "Username:"
        )
      )

      panel.add(
        username
      )


      panel.add(
        new JLabel(
          "Password:"
        )
      )

      panel.add(
        password
      )


      panel.add(
        new JLabel(
          "Confirm Password:"
        )
      )

      panel.add(
        confirm
      )


      panel.add(
        new JLabel(
          "Public Bio:"
        )
      )

      panel.add(
        bio
      )


      panel.add(
        new JLabel(
          "Profile Photo:"
        )
      )

      val photoPanel =
        new JPanel(
          new BorderLayout(
            5,
            0
          )
        )

      photoPanel.add(
        chooseImage,
        BorderLayout.WEST
      )

      photoPanel.add(
        imageLabel,
        BorderLayout.CENTER
      )

      panel.add(
        photoPanel
      )


      val createButton =
        new JButton(
          "CREATE ACCOUNT"
        )

      panel.add(
        new JLabel(
          ""
        )
      )

      panel.add(
        createButton
      )


      chooseImage.addActionListener(
        new ActionListener {

          override def actionPerformed(
              e: ActionEvent
          ): Unit = {

            val chooser =
              new JFileChooser()

            val result =
              chooser.showOpenDialog(
                dialog
              )

            if (
              result ==
                JFileChooser.APPROVE_OPTION
            ) {

              val encoded =
                Util.loadImageBase64(
                  chooser
                    .getSelectedFile
                    .getAbsolutePath
                )

              if (
                encoded.isEmpty
              ) {

                JOptionPane.showMessageDialog(
                  dialog,
                  "Image is too large or invalid.",
                  APP_NAME,
                  JOptionPane.WARNING_MESSAGE
                )

              } else {

                avatar =
                  encoded

                imageLabel.setText(
                  "Image selected ?"
                )
              }
            }
          }
        }
      )


      createButton.addActionListener(
        new ActionListener {

          override def actionPerformed(
              e: ActionEvent
          ): Unit = {

            val name =
              username
                .getText
                .trim

            val pass =
              new String(
                password.getPassword
              )

            val confirmPass =
              new String(
                confirm.getPassword
              )


            if (
              !Util.validUsername(
                name
              )
            ) {

              JOptionPane.showMessageDialog(
                dialog,
                "Username must be 3-20 characters.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )

              return
            }


            if (
              pass.length < 4
            ) {

              JOptionPane.showMessageDialog(
                dialog,
                "Password must contain at least 4 characters.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )

              return
            }


            if (
              pass != confirmPass
            ) {

              JOptionPane.showMessageDialog(
                dialog,
                "Passwords do not match.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )

              return
            }


            if (
              !connectToServer()
            ) {
              return
            }


            client.registerUsername =
              name

            client.registerPassword =
              pass

            client.registerBio =
              bio.getText.trim

            client.registerAvatar =
              avatar

            client.callback =
              registerEvent

            client.send(
              "REGISTER_START|" +
                Util.encode(
                  name
                )
            )


            dialog.dispose()
          }
        }
      )


      dialog.setContentPane(
        panel
      )

      dialog.setVisible(
        true
      )
    }


    // ========================================================
    // REGISTER EVENT
    // ========================================================

    private def registerEvent(
        line: String
    ): Unit = {

      if (
        line.startsWith(
          "LOGIN_OK|"
        )
      ) {

        frame.dispose()

        val main =
          new MainFrame(
            client
          )

        client.callback =
          main.handleServerEvent

        main.show()

        client.send(
          "PROFILE|" +
            Util.encode(
              client.username
            )
        )

        client.send(
          "SEARCH|" +
            Util.encode(
              ""
            )
        )
      }
    }


    // ========================================================
    // PASSWORD RECOVERY
    // ========================================================

    private def recoveryDialog(): Unit = {

      val dialog =
        new JDialog(
          frame,
          "Forgot Password",
          true
        )

      dialog.setSize(
        550,
        390
      )

      dialog.setLocationRelativeTo(
        frame
      )


      val username =
        new JTextField()

      val secret =
        new JPasswordField()

      val newPassword =
        new JPasswordField()

      val confirm =
        new JPasswordField()


      val note =
        new JLabel(
          "<html>" +
            "Enter your account ID and private recovery code.<br>" +
            "Then create a new password." +
            "</html>"
        )


      val recoverButton =
        new JButton(
          "RESET PASSWORD"
        )


      val panel =
        new JPanel(
          new GridLayout(
            6,
            2,
            8,
            8
          )
        )

      panel.setBorder(
        BorderFactory.createEmptyBorder(
          15,
          15,
          15,
          15
        )
      )


      panel.add(
        new JLabel(
          "Information:"
        )
      )

      panel.add(
        note
      )


      panel.add(
        new JLabel(
          "Username / ID:"
        )
      )

      panel.add(
        username
      )


      panel.add(
        new JLabel(
          "Recovery Secret:"
        )
      )

      panel.add(
        secret
      )


      panel.add(
        new JLabel(
          "New Password:"
        )
      )

      panel.add(
        newPassword
      )


      panel.add(
        new JLabel(
          "Confirm New Password:"
        )
      )

      panel.add(
        confirm
      )


      panel.add(
        new JLabel(
          ""
        )
      )

      panel.add(
        recoverButton
      )


      recoverButton.addActionListener(
        new ActionListener {

          override def actionPerformed(
              e: ActionEvent
          ): Unit = {

            val name =
              username
                .getText
                .trim

            val recovery =
              new String(
                secret.getPassword
              )

            val newPass =
              new String(
                newPassword.getPassword
              )

            val confirmPass =
              new String(
                confirm.getPassword
              )


            if (
              !Util.validUsername(
                name
              )
            ) {

              JOptionPane.showMessageDialog(
                dialog,
                "Enter correct username.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )

              return
            }


            if (
              recovery.isEmpty
            ) {

              JOptionPane.showMessageDialog(
                dialog,
                "Enter recovery secret.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )

              return
            }


            if (
              newPass.length < 4
            ) {

              JOptionPane.showMessageDialog(
                dialog,
                "New password must contain at least 4 characters.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )

              return
            }


            if (
              newPass != confirmPass
            ) {

              JOptionPane.showMessageDialog(
                dialog,
                "New passwords do not match.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )

              return
            }


            if (
              !connectToServer()
            ) {
              return
            }


            client.recoveryUsername =
              name

            client.recoverySecret =
              recovery

            client.recoveryNewPassword =
              newPass

            client.callback =
              recoveryEvent

            client.send(
              "RECOVERY_START|" +
                Util.encode(
                  name
                )
            )


            dialog.dispose()
          }
        }
      )


      dialog.setContentPane(
        panel
      )

      dialog.setVisible(
        true
      )
    }


    // ========================================================
    // RECOVERY EVENT
    // ========================================================

    private def recoveryEvent(
        line: String
    ): Unit = {

      if (
        line ==
          "RECOVERY_OK"
      ) {

        JOptionPane.showMessageDialog(
          frame,
          "Password reset successfully.\nYou can now login with the new password.",
          APP_NAME,
          JOptionPane.INFORMATION_MESSAGE
        )

        client.callback =
          loginEvent
      }
    }


    // ========================================================
    // START HOST SERVER
    // ========================================================

    private def startHost(): Unit = {

      val port =
        getPort()

      if (
        port <= 0
      ) {

        JOptionPane.showMessageDialog(
          frame,
          "Port should be between 1024 and 65535.",
          APP_NAME,
          JOptionPane.WARNING_MESSAGE
        )

        return
      }


      val started =
        Server.start(
          port
        )

      if (!started) {

        JOptionPane.showMessageDialog(
          frame,
          "Server could not start.\nPort may already be in use.",
          APP_NAME,
          JOptionPane.ERROR_MESSAGE
        )

        return
      }


      val ips =
        Util.localIPv4Addresses()


      val text =
        new StringBuilder()

      text.append(
        "SERVER STARTED SUCCESSFULLY\n\n"
      )

      text.append(
        "Port: " +
          port +
          "\n\n"
      )

      text.append(
        "Use one of these IP addresses on the OTHER PC:\n\n"
      )


      var i =
        0

      while (
        i < ips.length
      ) {

        text.append(
          ips(i)
        )

        text.append(
          ":" +
            port +
            "\n"
        )

        i +=
          1
      }


      text.append(
        "\nBoth PCs should be connected to the same Wi-Fi/router."
      )


      JOptionPane.showMessageDialog(
        frame,
        text.toString,
        APP_NAME,
        JOptionPane.INFORMATION_MESSAGE
      )


      hostField.setText(
        "127.0.0.1"
      )

      portField.setText(
        port.toString
      )
    }


    // ========================================================
    // OPEN MAIN
    // ========================================================

    private def openMain(): Unit = {

      frame.dispose()

      val main =
        new MainFrame(
          client
        )

      client.callback =
        main.handleServerEvent

      main.show()

      client.send(
        "PROFILE|" +
          Util.encode(
            client.username
          )
      )

      client.send(
        "SEARCH|" +
          Util.encode(
            ""
          )
      )
    }
  }


  // ==========================================================
  // MAIN CHAT WINDOW
  // ==========================================================

  class MainFrame(
      client: ChatClient
  ) {

    private val frame =
      new JFrame(
        APP_NAME +
          " - " +
          client.username
      )


    private val peopleModel =
      new DefaultListModel[
        PersonEntry
      ]()


    private val chatModel =
      new DefaultListModel[
        ChatEntry
      ]()


    private val peopleList =
      new JList[
        PersonEntry
      ](
        peopleModel
      )


    private val chatList =
      new JList[
        ChatEntry
      ](
        chatModel
      )


    private val searchField =
      new JTextField()


    private val chatArea =
      new JTextArea()


    private val messageField =
      new JTextField()


    private val sendButton =
      new JButton(
        "SEND"
      )


    private val chatTitle =
      new JLabel(
        "Select a chat"
      )


    private val profileName =
      new JLabel(
        "No person selected"
      )


    private val profileStatus =
      new JLabel(
        ""
      )


    private val profileBio =
      new JTextArea()


    private val profileImage =
      new JLabel(
        "No Photo",
        SwingConstants.CENTER
      )


    private val chatButton =
      new JButton(
        "OPEN CHAT"
      )


    private val editProfileButton =
      new JButton(
        "EDIT MY PROFILE"
      )


    private val groupButton =
      new JButton(
        "CREATE GROUP"
      )


    private var selectedPerson =
      ""


    private var currentChat =
      ""


    private val chatMap =
      mutable.Map[
        String,
        ChatEntry
      ]()


    // ========================================================
    // SHOW
    // ========================================================

    def show(): Unit = {

      Util.setupUIFont()


      frame.setDefaultCloseOperation(
        WindowConstants.EXIT_ON_CLOSE
      )

      frame.setSize(
        1250,
        760
      )

      frame.setMinimumSize(
        new Dimension(
          1000,
          650
        )
      )

      frame.setLocationRelativeTo(
        null
      )


      val root =
        new JPanel(
          new BorderLayout(
            7,
            7
          )
        )


      // ======================================================
      // TOP BAR
      // ======================================================

      val top =
        new JPanel(
          new BorderLayout()
        )

      top.setBackground(
        new Color(
          33,
          38,
          46
        )
      )


      val title =
        new JLabel(
          "   YADNESH LIVE CHAT"
        )

      title.setForeground(
        Color.WHITE
      )

      title.setFont(
        new Font(
          "Segoe UI",
          Font.BOLD,
          22
        )
      )


      val logged =
        new JLabel(
          "Logged in: " +
            client.username +
            "   "
        )

      logged.setForeground(
        Color.WHITE
      )


      top.add(
        title,
        BorderLayout.WEST
      )

      top.add(
        logged,
        BorderLayout.EAST
      )


      root.add(
        top,
        BorderLayout.NORTH
      )


      // ======================================================
      // LEFT PANEL
      // ======================================================

      val left =
        new JPanel(
          new BorderLayout(
            5,
            5
          )
        )

      left.setPreferredSize(
        new Dimension(
          300,
          100
        )
      )


      // ------------------------------------------------------
      // SEARCH
      // ------------------------------------------------------

      val searchPanel =
        new JPanel(
          new BorderLayout(
            5,
            5
          )
        )


      val searchButton =
        new JButton(
          "SEARCH"
        )


      searchPanel.add(
        searchField,
        BorderLayout.CENTER
      )

      searchPanel.add(
        searchButton,
        BorderLayout.EAST
      )


      left.add(
        searchPanel,
        BorderLayout.NORTH
      )


      // ------------------------------------------------------
      // PEOPLE LIST
      // ------------------------------------------------------

      peopleList.setSelectionMode(
        ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
      )


      val peopleTitle =
        new JLabel(
          " PEOPLE"
        )


      val peoplePanel =
        new JPanel(
          new BorderLayout(
            3,
            3
          )
        )


      peoplePanel.add(
        peopleTitle,
        BorderLayout.NORTH
      )


      peoplePanel.add(
        new JScrollPane(
          peopleList
        ),
        BorderLayout.CENTER
      )


      // ------------------------------------------------------
      // CHAT LIST
      // ------------------------------------------------------

      val chatsTitle =
        new JLabel(
          " MY CHATS"
        )


      val chatsPanel =
        new JPanel(
          new BorderLayout(
            3,
            3
          )
        )


      chatsPanel.add(
        chatsTitle,
        BorderLayout.NORTH
      )


      chatsPanel.add(
        new JScrollPane(
          chatList
        ),
        BorderLayout.CENTER
      )


      val leftCenter =
        new JPanel(
          new GridLayout(
            2,
            1,
            5,
            5
          )
        )


      leftCenter.add(
        peoplePanel
      )

      leftCenter.add(
        chatsPanel
      )


      left.add(
        leftCenter,
        BorderLayout.CENTER
      )


      left.add(
        groupButton,
        BorderLayout.SOUTH
      )


      root.add(
        left,
        BorderLayout.WEST
      )


      // ======================================================
      // CENTER CHAT
      // ======================================================

      val center =
        new JPanel(
          new BorderLayout(
            5,
            5
          )
        )


      chatTitle.setFont(
        new Font(
          "Segoe UI",
          Font.BOLD,
          18
        )
      )


      center.add(
        chatTitle,
        BorderLayout.NORTH
      )


      chatArea.setEditable(
        false
      )

      chatArea.setLineWrap(
        true
      )

      chatArea.setWrapStyleWord(
        true
      )

      chatArea.setFont(
        new Font(
          "Segoe UI",
          Font.PLAIN,
          14
        )
      )


      // UNIQUE VARIABLE NAME
      val messageScrollPane =
        new JScrollPane(
          chatArea
        )


      center.add(
        messageScrollPane,
        BorderLayout.CENTER
      )


      val inputPanel =
        new JPanel(
          new BorderLayout(
            5,
            5
          )
        )


      inputPanel.add(
        messageField,
        BorderLayout.CENTER
      )


      inputPanel.add(
        sendButton,
        BorderLayout.EAST
      )


      center.add(
        inputPanel,
        BorderLayout.SOUTH
      )


      root.add(
        center,
        BorderLayout.CENTER
      )


      // ======================================================
      // RIGHT PROFILE
      // ======================================================

      val right =
        new JPanel(
          new BorderLayout(
            5,
            5
          )
        )

      right.setPreferredSize(
        new Dimension(
          285,
          100
        )
      )


      val profileTitle =
        new JLabel(
          " PROFILE"
        )

      profileTitle.setFont(
        new Font(
          "Segoe UI",
          Font.BOLD,
          16
        )
      )


      right.add(
        profileTitle,
        BorderLayout.NORTH
      )


      profileImage.setPreferredSize(
        new Dimension(
          240,
          180
        )
      )


      profileImage.setBorder(
        BorderFactory.createLineBorder(
          new Color(
            180,
            180,
            180
          )
        )
      )


      profileName.setFont(
        new Font(
          "Segoe UI",
          Font.BOLD,
          16
        )
      )


      profileBio.setEditable(
        false
      )

      profileBio.setLineWrap(
        true
      )

      profileBio.setWrapStyleWord(
        true
      )


      val profileInfo =
        new JPanel(
          new BorderLayout(
            5,
            5
          )
        )


      val profileTop =
        new JPanel(
          new GridLayout(
            2,
            1,
            3,
            3
          )
        )


      profileTop.add(
        profileName
      )

      profileTop.add(
        profileStatus
      )


      profileInfo.add(
        profileTop,
        BorderLayout.NORTH
      )


      profileInfo.add(
        new JScrollPane(
          profileBio
        ),
        BorderLayout.CENTER
      )


      val profileCenter =
        new JPanel(
          new BorderLayout(
            5,
            5
          )
        )


      profileCenter.add(
        profileImage,
        BorderLayout.NORTH
      )


      profileCenter.add(
        profileInfo,
        BorderLayout.CENTER
      )


      right.add(
        profileCenter,
        BorderLayout.CENTER
      )


      val profileButtons =
        new JPanel(
          new GridLayout(
            2,
            1,
            5,
            5
          )
        )


      profileButtons.add(
        chatButton
      )

      profileButtons.add(
        editProfileButton
      )


      right.add(
        profileButtons,
        BorderLayout.SOUTH
      )


      root.add(
        right,
        BorderLayout.EAST
      )


      // ======================================================
      // SEARCH EVENT
      // ======================================================

      searchButton.addActionListener(
        new ActionListener {

          override def actionPerformed(
              e: ActionEvent
          ): Unit = {

            searchPeople()
          }
        }
      )


      searchField.addActionListener(
        new ActionListener {

          override def actionPerformed(
              e: ActionEvent
          ): Unit = {

            searchPeople()
          }
        }
      )


      // ======================================================
      // PERSON SELECT
      // ======================================================

      peopleList.addListSelectionListener(
        _ => {

          val person =
            peopleList
              .getSelectedValue

          if (
            person != null
          ) {

            selectedPerson =
              person.username

            profileName.setText(
              person.username
            )

            profileStatus.setText(
              if (person.online) {
                "? ONLINE"
              } else {
                "? OFFLINE"
              }
            )

            profileBio.setText(
              if (person.bio.isEmpty) {
                "No public bio."
              } else {
                person.bio
              }
            )

            client.send(
              "PROFILE|" +
                Util.encode(
                  person.username
                )
            )
          }
        }
      )


      // ======================================================
      // DOUBLE CLICK PERSON
      // ======================================================

      peopleList.addMouseListener(
        new MouseAdapter {

          override def mouseClicked(
              e: MouseEvent
          ): Unit = {

            if (
              e.getClickCount == 2
            ) {

              val person =
                peopleList
                  .getSelectedValue

              if (
                person != null
              ) {

                openDirectChat(
                  person.username
                )
              }
            }
          }
        }
      )


      // ======================================================
      // OPEN CHAT SELECT
      // ======================================================

      chatList.addListSelectionListener(
        _ => {

          val item =
            chatList
              .getSelectedValue

          if (
            item != null
          ) {

            currentChat =
              item.id

            chatTitle.setText(
              item.title
            )

            chatArea.setText(
              "Loading..."
            )

            client.send(
              "OPEN_CHAT|" +
                Util.encode(
                  item.id
                )
            )
          }
        }
      )


      // ======================================================
      // CHAT BUTTON
      // ======================================================

      chatButton.addActionListener(
        new ActionListener {

          override def actionPerformed(
              e: ActionEvent
          ): Unit = {

            if (
              selectedPerson != null &&
              selectedPerson.nonEmpty
            ) {

              openDirectChat(
                selectedPerson
              )
            }
          }
        }
      )


      // ======================================================
      // SEND MESSAGE
      // ======================================================

      sendButton.addActionListener(
        new ActionListener {

          override def actionPerformed(
              e: ActionEvent
          ): Unit = {

            sendMessage()
          }
        }
      )


      messageField.addActionListener(
        new ActionListener {

          override def actionPerformed(
              e: ActionEvent
          ): Unit = {

            sendMessage()
          }
        }
      )


      // ======================================================
      // CREATE GROUP
      // ======================================================

      groupButton.addActionListener(
        new ActionListener {

          override def actionPerformed(
              e: ActionEvent
          ): Unit = {

            createGroup()
          }
        }
      )


      // ======================================================
      // EDIT PROFILE
      // ======================================================

      editProfileButton.addActionListener(
        new ActionListener {

          override def actionPerformed(
              e: ActionEvent
          ): Unit = {

            editProfile()
          }
        }
      )


      frame.setContentPane(
        root
      )

      frame.setVisible(
        true
      )
    }


    // ========================================================
    // SEARCH
    // ========================================================

    private def searchPeople(): Unit = {

      client.send(
        "SEARCH|" +
          Util.encode(
            searchField
              .getText
              .trim
          )
      )
    }


    // ========================================================
    // OPEN DM
    // ========================================================

    private def openDirectChat(
        username: String
    ): Unit = {

      client.send(
        "OPEN_DM|" +
          Util.encode(
            username
          )
      )
    }


    // ========================================================
    // SEND
    // ========================================================

    private def sendMessage(): Unit = {

      if (
        currentChat == null ||
        currentChat.isEmpty
      ) {

        JOptionPane.showMessageDialog(
          frame,
          "First select or open a chat.",
          APP_NAME,
          JOptionPane.WARNING_MESSAGE
        )

        return
      }


      val message =
        messageField
          .getText
          .trim


      if (
        message.isEmpty
      ) {

        return
      }


      client.send(
        "SEND_MESSAGE|" +
          Util.encode(
            currentChat
          ) +
          "|" +
          Util.encode(
            message
          )
      )


      messageField.setText(
        ""
      )
    }


    // ========================================================
    // CREATE GROUP
    // ========================================================

    private def createGroup(): Unit = {

      // Java java.util.List returned here.
      // Use while loop instead of Scala foreach.
      val selected =
        peopleList
          .getSelectedValuesList()


      if (
        selected == null ||
        selected.size() < 1
      ) {

        JOptionPane.showMessageDialog(
          frame,
          "Select at least one person from PEOPLE.",
          APP_NAME,
          JOptionPane.WARNING_MESSAGE
        )

        return
      }


      val groupName =
        JOptionPane.showInputDialog(
          frame,
          "Enter group name:",
          "Create Group",
          JOptionPane.QUESTION_MESSAGE
        )


      if (
        groupName == null ||
        groupName.trim.isEmpty
      ) {

        return
      }


      val count =
        selected.size()


      if (
        count > 20
      ) {

        JOptionPane.showMessageDialog(
          frame,
          "Maximum 20 people.",
          APP_NAME,
          JOptionPane.WARNING_MESSAGE
        )

        return
      }


      val request =
        new StringBuilder()


      request.append(
        "CREATE_GROUP|"
      )

      request.append(
        Util.encode(
          groupName.trim
        )
      )

      request.append(
        "|" +
          count
      )


      var i =
        0

      while (
        i < count
      ) {

        val person =
          selected.get(
            i
          )

        request.append(
          "|" +
            Util.encode(
              person.username
            )
        )

        i +=
          1
      }


      client.send(
        request.toString
      )
    }


    // ========================================================
    // EDIT PROFILE
    // ========================================================

    private def editProfile(): Unit = {

      val dialog =
        new JDialog(
          frame,
          "Edit My Profile",
          true
        )

      dialog.setSize(
        650,
        500
      )

      dialog.setLocationRelativeTo(
        frame
      )


      val bioField =
        new JTextField(
          client.ownBio
        )


      val chooseImage =
        new JButton(
          "CHANGE PHOTO"
        )


      val imageLabel =
        new JLabel(
          if (
            client.ownAvatar.nonEmpty
          ) {
            "Current image saved ?"
          } else {
            "No image"
          }
        )


      var avatar =
        client.ownAvatar


      val saveButton =
        new JButton(
          "SAVE PROFILE"
        )


      val panel =
        new JPanel(
          new BorderLayout(
            10,
            10
          )
        )


      panel.setBorder(
        BorderFactory.createEmptyBorder(
          15,
          15,
          15,
          15
        )
      )


      val form =
        new GridLayout(
          3,
          2,
          8,
          8
        )


      val formPanel =
        new JPanel(
          form
        )


      formPanel.add(
        new JLabel(
          "Public Bio:"
        )
      )

      formPanel.add(
        bioField
      )


      formPanel.add(
        new JLabel(
          "Profile Photo:"
        )
      )


      val imagePanel =
        new JPanel(
          new BorderLayout(
            5,
            0
          )
        )


      imagePanel.add(
        chooseImage,
        BorderLayout.WEST
      )


      imagePanel.add(
        imageLabel,
        BorderLayout.CENTER
      )


      formPanel.add(
        imagePanel
      )


      formPanel.add(
        new JLabel(
          "Account:"
        )
      )

      formPanel.add(
        new JLabel(
          client.username
        )
      )


      panel.add(
        formPanel,
        BorderLayout.CENTER
      )


      panel.add(
        saveButton,
        BorderLayout.SOUTH
      )


      chooseImage.addActionListener(
        new ActionListener {

          override def actionPerformed(
              e: ActionEvent
          ): Unit = {

            val chooser =
              new JFileChooser()


            if (
              chooser.showOpenDialog(
                dialog
              ) ==
                JFileChooser.APPROVE_OPTION
            ) {

              val encoded =
                Util.loadImageBase64(
                  chooser
                    .getSelectedFile
                    .getAbsolutePath
                )


              if (
                encoded.nonEmpty
              ) {

                avatar =
                  encoded

                imageLabel.setText(
                  "New image selected ?"
                )

              } else {

                JOptionPane.showMessageDialog(
                  dialog,
                  "Invalid or large image.",
                  APP_NAME,
                  JOptionPane.WARNING_MESSAGE
                )
              }
            }
          }
        }
      )


      saveButton.addActionListener(
        new ActionListener {

          override def actionPerformed(
              e: ActionEvent
          ): Unit = {

            client.ownBio =
              bioField
                .getText
                .trim

            client.ownAvatar =
              avatar

            client.send(
              "UPDATE_PROFILE|" +
                Util.encode(
                  client.ownBio
                ) +
                "|" +
                Util.encode(
                  client.ownAvatar
                )
            )

            dialog.dispose()
          }
        }
      )


      dialog.setContentPane(
        panel
      )

      dialog.setVisible(
        true
      )
    }


    // ========================================================
    // SERVER EVENTS
    // ========================================================

    def handleServerEvent(
        line: String
    ): Unit = {

      val parts =
        line.split(
          "\\|",
          -1
        )


      if (
        parts.length == 0
      ) {

        return
      }


      parts(0) match {

        // ----------------------------------------------------
        // SEARCH
        // ----------------------------------------------------

        case "SEARCH_RESULT" =>

          peopleModel.clear()

          if (
            parts.length >= 2
          ) {

            val count =
              try {
                parts(1).toInt
              } catch {
                case _: Throwable =>
                  0
              }

            var index =
              2

            var i =
              0

            while (
              i < count &&
              index + 2 <
                parts.length
            ) {

              val username =
                Util.decode(
                  parts(index)
                )

              val bio =
                Util.decode(
                  parts(index + 1)
                )

              val online =
                parts(index + 2) == "1"


              peopleModel.addElement(
                PersonEntry(
                  username,
                  bio,
                  online
                )
              )


              index +=
                3

              i +=
                1
            }
          }


        // ----------------------------------------------------
        // PROFILE
        // ----------------------------------------------------

        case "PROFILE_DATA" =>

          if (
            parts.length >= 5
          ) {

            val name =
              Util.decode(
                parts(1)
              )

            val bio =
              Util.decode(
                parts(2)
              )

            val avatar =
              Util.decode(
                parts(3)
              )

            val online =
              parts(4) == "1"


            if (
              name.equalsIgnoreCase(
                client.username
              )
            ) {

              client.ownBio =
                bio

              client.ownAvatar =
                avatar
            }


            if (
              name.equalsIgnoreCase(
                selectedPerson
              )
            ) {

              profileName.setText(
                name
              )

              profileStatus.setText(
                if (online) {
                  "? ONLINE"
                } else {
                  "? OFFLINE"
                }
              )

              profileBio.setText(
                if (
                  bio.isEmpty
                ) {
                  "No public bio."
                } else {
                  bio
                }
              )

              showAvatar(
                avatar
              )
            }
          }


        // ----------------------------------------------------
        // PROFILE SAVED
        // ----------------------------------------------------

        case "PROFILE_SAVED" =>

          JOptionPane.showMessageDialog(
            frame,
            "Profile saved successfully.",
            APP_NAME,
            JOptionPane.INFORMATION_MESSAGE
          )

          client.send(
            "PROFILE|" +
              Util.encode(
                client.username
              )
          )


        // ----------------------------------------------------
        // PROFILE CHANGED
        // ----------------------------------------------------

        case "PROFILE_CHANGED" =>

          if (
            parts.length >= 2
          ) {

            val name =
              Util.decode(
                parts(1)
              )

            if (
              name.equalsIgnoreCase(
                selectedPerson
              )
            ) {

              client.send(
                "PROFILE|" +
                  Util.encode(
                    selectedPerson
                  )
              )
            }

            searchPeople()
          }


        // ----------------------------------------------------
        // CHAT
        // ----------------------------------------------------

        case "CHAT" =>

          if (
            parts.length >= 4
          ) {

            val id =
              Util.decode(
                parts(1)
              )

            val title =
              Util.decode(
                parts(2)
              )

            val group =
              parts(3) == "1"


            if (
              !chatMap.contains(
                id
              )
            ) {

              val entry =
                ChatEntry(
                  id,
                  title,
                  group
                )

              chatMap.put(
                id,
                entry
              )

              chatModel.addElement(
                entry
              )
            }
          }


        // ----------------------------------------------------
        // HISTORY
        // ----------------------------------------------------

        case "HISTORY" =>

          if (
            parts.length >= 3
          ) {

            val id =
              Util.decode(
                parts(1)
              )

            val count =
              try {
                parts(2).toInt
              } catch {
                case _: Throwable =>
                  0
              }


            if (
              id ==
                currentChat
            ) {

              val text =
                new StringBuilder()

              var index =
                3

              var i =
                0


              while (
                i < count &&
                index + 2 <
                  parts.length
              ) {

                val sender =
                  Util.decode(
                    parts(index)
                  )

                val time =
                  Util.decode(
                    parts(index + 1)
                  )

                val message =
                  Util.decode(
                    parts(index + 2)
                  )


                text.append(
                  "[" +
                    time +
                    "] " +
                    sender +
                    ": " +
                    message +
                    "\n"
                )


                index +=
                  3

                i +=
                  1
              }


              chatArea.setText(
                text.toString
              )


              chatArea.setCaretPosition(
                chatArea.getDocument
                  .getLength
              )
            }
          }


        // ----------------------------------------------------
        // LIVE MESSAGE
        // ----------------------------------------------------

        case "MESSAGE" =>

          if (
            parts.length >= 5
          ) {

            val id =
              Util.decode(
                parts(1)
              )

            val sender =
              Util.decode(
                parts(2)
              )

            val time =
              Util.decode(
                parts(3)
              )

            val message =
              Util.decode(
                parts(4)
              )


            if (
              id ==
                currentChat
            ) {

              chatArea.append(
                "[" +
                  time +
                  "] " +
                  sender +
                  ": " +
                  message +
                  "\n"
              )


              chatArea.setCaretPosition(
                chatArea.getDocument
                  .getLength
              )
            }
          }


        // ----------------------------------------------------
        // PRESENCE
        // ----------------------------------------------------

        case "PRESENCE" =>

          if (
            parts.length >= 3
          ) {

            val username =
              Util.decode(
                parts(1)
              )

            val online =
              parts(2) == "1"


            var i =
              0


            while (
              i < peopleModel.size()
            ) {

              val old =
                peopleModel
                  .getElementAt(
                    i
                  )


              if (
                old.username
                  .equalsIgnoreCase(
                    username
                  )
              ) {

                peopleModel.setElementAt(
                  old.copy(
                    online =
                      online
                  ),
                  i
                )
              }


              i +=
                1
            }


            if (
              selectedPerson
                .equalsIgnoreCase(
                  username
                )
            ) {

              profileStatus.setText(
                if (online) {
                  "? ONLINE"
                } else {
                  "? OFFLINE"
                }
              )
            }
          }


        // ----------------------------------------------------
        // GROUP
        // ----------------------------------------------------

        case "GROUP_CREATED" =>

          if (
            parts.length >= 2
          ) {

            JOptionPane.showMessageDialog(
              frame,
              "Group created:\n" +
                Util.decode(
                  parts(1)
                ),
              APP_NAME,
              JOptionPane.INFORMATION_MESSAGE
            )
          }


        case _ =>
      }
    }


    // ========================================================
    // SHOW PROFILE IMAGE
    // ========================================================

    private def showAvatar(
        base64: String
    ): Unit = {

      if (
        base64 == null ||
        base64.isEmpty
      ) {

        profileImage.setIcon(
          null
        )

        profileImage.setText(
          "No Photo"
        )

      } else {

        val image =
          Util.loadImage(
            base64
          )


        if (
          image == null
        ) {

          profileImage.setIcon(
            null
          )

          profileImage.setText(
            "No Photo"
          )

        } else {

          val scaled =
            image.getScaledInstance(
              225,
              165,
              Image.SCALE_SMOOTH
            )


          profileImage.setText(
            ""
          )

          profileImage.setIcon(
            new javax.swing.ImageIcon(
              scaled
            )
          )
        }
      }
    }
  }


  // ==========================================================
  // START SCREEN
  // ==========================================================

  object StartScreen {

    def show(): Unit = {

      Util.setupUIFont()


      val frame =
        new JFrame(
          APP_NAME
        )


      frame.setDefaultCloseOperation(
        WindowConstants.EXIT_ON_CLOSE
      )


      frame.setSize(
        720,
        540
      )


      frame.setLocationRelativeTo(
        null
      )


      val root =
        new JPanel(
          new BorderLayout(
            15,
            15
          )
        )


      root.setBorder(
        BorderFactory.createEmptyBorder(
          30,
          30,
          30,
          30
        )
      )


      val title =
        new JLabel(
          "<html>" +
            "<center>" +
            "<font size='7'><b>YADNESH LIVE CHAT</b></font><br>" +
            "<font size='4'>LIVE PC ? PC WIFI CHAT</font>" +
            "</center>" +
            "</html>",
          SwingConstants.CENTER
        )


      root.add(
        title,
        BorderLayout.NORTH
      )


      val information =
        new JTextArea(
          "FEATURES\n\n" +
            "? PC to PC live Wi-Fi chat\n" +
            "? 2-person direct chat\n" +
            "? Group chat\n" +
            "? Create new accounts\n" +
            "? Search people\n" +
            "? Online / offline status\n" +
            "? Profile photo\n" +
            "? Public profile bio\n" +
            "? Forgot Password recovery\n" +
            "? Profiles saved on host PC\n\n" +
            "NO SECOND PASSWORD FIELD\n\n" +
            "Forgot password uses the private recovery secret."
        )


      information.setEditable(
        false
      )

      information.setLineWrap(
        true
      )

      information.setWrapStyleWord(
        true
      )


      root.add(
        new JScrollPane(
          information
        ),
        BorderLayout.CENTER
      )


      val startButton =
        new JButton(
          "START YADNESH LIVE CHAT"
        )


      startButton.setPreferredSize(
        new Dimension(
          320,
          55
        )
      )


      val buttonPanel =
        new JPanel(
          new GridLayout(
            1,
            1
          )
        )


      buttonPanel.add(
        startButton
      )


      root.add(
        buttonPanel,
        BorderLayout.SOUTH
      )


      startButton.addActionListener(
        new ActionListener {

          override def actionPerformed(
              e: ActionEvent
          ): Unit = {

            frame.dispose()

            val login =
              new LoginFrame()

            login.show()
          }
        }
      )


      frame.setContentPane(
        root
      )

      frame.setVisible(
        true
      )
    }
  }


  // ==========================================================
  // DIRECT LAUNCHER
  // ==========================================================

  def startApp(): Unit = {

    SwingUtilities.invokeLater(
      new Runnable {

        override def run(): Unit = {

          try {

            UIManager.setLookAndFeel(
              UIManager
                .getSystemLookAndFeelClassName
            )

          } catch {

            case _: Throwable =>
          }

          StartScreen.show()
        }
      }
    )
  }
}


// ============================================================
// KOJO DIRECT RUN
// ============================================================

LiveChatApp.startApp()