Code Sketch


yoii live
By: Mhalsakant School
Category: Programming
import java.awt._
import java.awt.event._
import java.awt.geom._
import java.net.{HttpURLConnection, URI, URL}
import java.io.{BufferedReader, InputStreamReader}
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.Date
import javax.swing._
import javax.swing.border._
import javax.swing.table.DefaultTableModel
import scala.collection.mutable.ArrayBuffer

// ============================================================
// CRICKET LIVE CENTER
// Scala + Java Swing/AWT
// Kojo friendly - no Scala Swing
// ============================================================

object CricketConfig {
  val APP_TITLE = "CRICKET LIVE CENTER"

  val HOME_URL =
    "https://www.cricbuzz.com/cricket-match/live-scores"

  val FALLBACK_URL =
    "https://www.cricbuzz.com/cricket-match/live-scores"

  val USER_AGENT =
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
      "(KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36"

  // SHA-256 of: bixuu@cricketlive
  val PASSWORD_HASH =
    "07a3ccbb2c0983f152db1bc75b540d945c82372fe1771d71f3c025e89e4bd79b"

  val AUTO_REFRESH_MS = 20000
}

// ============================================================
// DATA CLASSES
// ============================================================

case class MatchInfo(
  name: String,
  url: String,
  status: String
)

case class CricketBatter(
  name: String,
  runs: String,
  balls: String,
  fours: String,
  sixes: String,
  sr: String
)

case class CricketBowler(
  name: String,
  overs: String,
  maidens: String,
  runs: String,
  wickets: String,
  economy: String
)

case class LiveScoreData(
  matchName: String,
  state: String,
  score: String,
  team: String,
  overs: String,
  crr: String,
  req: String,
  target: String,
  venue: String,
  toss: String,
  partnership: String,
  striker: String,
  nonStriker: String,
  currentBowler: String,
  lastBall: String,
  recentOver: String,
  updated: String,
  crowd: String,
  batters: ArrayBuffer[CricketBatter],
  bowlers: ArrayBuffer[CricketBowler],
  commentary: ArrayBuffer[String]
)

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

object TextUtil {

  def cleanHtml(s: String): String = {
    if (s == null) {
      return ""
    }

    var x = s

    x = x.replaceAll("(?is)<script[^>]*>.*?</script>", " ")
    x = x.replaceAll("(?is)<style[^>]*>.*?</style>", " ")
    x = x.replaceAll("(?is)<br\\s*/?>", " ")
    x = x.replaceAll("(?is)</p>", " ")
    x = x.replaceAll("(?is)</div>", " ")
    x = x.replaceAll("(?is)</li>", " ")
    x = x.replaceAll("(?is)</tr>", " ")

    x = x.replaceAll("(?is)<[^>]+>", " ")

    x = x
      .replace("&nbsp;", " ")
      .replace("&amp;", "&")
      .replace("&quot;", "\"")
      .replace("&#39;", "'")
      .replace("&apos;", "'")
      .replace("&lt;", "<")
      .replace("&gt;", ">")

    x = x.replaceAll("\\s+", " ").trim

    x
  }

  def safe(s: String): String = {
    if (s == null || s.trim.isEmpty) "?" else s.trim
  }

  def shorten(s: String, max: Int): String = {
    val x = safe(s)
    if (x.length <= max) x
    else x.substring(0, max - 1) + "?"
  }

  def nowTime(): String = {
    new SimpleDateFormat("HH:mm:ss").format(new Date())
  }
}

// ============================================================
// WEB READER
// ============================================================

object CricketWeb {

  def get(urlText: String): String = {

    var connection: HttpURLConnection = null
    var reader: BufferedReader = null

    try {
      val url = new URL(urlText)

      connection = url.openConnection().asInstanceOf[HttpURLConnection]
      connection.setRequestMethod("GET")
      connection.setRequestProperty("User-Agent", CricketConfig.USER_AGENT)
      connection.setRequestProperty("Accept", "text/html,application/xhtml+xml")
      connection.setRequestProperty("Accept-Language", "en-US,en;q=0.9")
      connection.setConnectTimeout(10000)
      connection.setReadTimeout(15000)
      connection.setInstanceFollowRedirects(true)

      val code = connection.getResponseCode

      if (code < 200 || code >= 400) {
        throw new RuntimeException("HTTP " + code)
      }

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

      val result = new StringBuilder
      var line: String = null

      line = reader.readLine()

      while (line != null) {
        result.append(line)
        result.append("\n")
        line = reader.readLine()
      }

      result.toString()

    } finally {
      if (reader != null) {
        try reader.close() catch {
          case _: Throwable =>
        }
      }

      if (connection != null) {
        connection.disconnect()
      }
    }
  }
}

// ============================================================
// PASSWORD
// ============================================================

object PasswordUtil {

  def sha256(text: String): String = {

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

    val bytes =
      digest.digest(text.getBytes(StandardCharsets.UTF_8))

    val out = new StringBuilder

    bytes.foreach { b =>
      out.append(String.format("%02x", Byte.box(b)))
    }

    out.toString()
  }

  def verify(password: String): Boolean = {
    sha256(password) == CricketConfig.PASSWORD_HASH
  }
}

// ============================================================
// MATCH LIST PARSER
// ============================================================

object MatchListParser {

  def parse(html: String): ArrayBuffer[MatchInfo] = {

    val result = ArrayBuffer[MatchInfo]()

    if (html == null || html.isEmpty) {
      return result
    }

    val anchorPattern =
      """(?is)<a[^>]+href=["']([^"']*live-cricket-(?:scores|scorecard)[^"']*)["'][^>]*>(.*?)</a>""".r

    anchorPattern.findAllMatchIn(html).foreach { m =>

      val href = m.group(1)
      val rawText = m.group(2)
      val name = TextUtil.cleanHtml(rawText)

      if (name.length >= 6) {

        val absoluteUrl =
          if (href.startsWith("http")) href
          else if (href.startsWith("/")) "https://www.cricbuzz.com" + href
          else "https://www.cricbuzz.com/" + href

        val lower = name.toLowerCase

        var status = "MATCH"

        if (lower.contains("live")) {
          status = "LIVE"
        } else if (lower.contains("won") || lower.contains("result")) {
          status = "RECENT"
        } else if (
          lower.contains("today") ||
          lower.contains("tomorrow")
        ) {
          status = "UPCOMING"
        }

        var duplicate = false

        result.foreach { existing =>
          if (existing.url == absoluteUrl) {
            duplicate = true
          }
        }

        if (!duplicate) {
          result += MatchInfo(
            TextUtil.shorten(name, 90),
            absoluteUrl,
            status
          )
        }
      }
    }

    result
  }
}

// ============================================================
// LIVE SCORE PARSER
// ============================================================

object LiveParser {

  private def first(pattern: String, text: String): String = {
    try {
      val r = pattern.r
      val found = r.findFirstMatchIn(text)

      if (found.isDefined) {
        found.get.group(1)
      } else {
        ""
      }

    } catch {
      case _: Throwable => ""
    }
  }

  private def all(pattern: String, text: String): ArrayBuffer[String] = {

    val out = ArrayBuffer[String]()

    try {
      pattern.r.findAllMatchIn(text).foreach { m =>
        if (m.groupCount >= 1) {
          out += m.group(1).trim
        }
      }
    } catch {
      case _: Throwable =>
    }

    out
  }

  private def guessTeam(text: String): String = {

    val scorePattern =
      """([A-Za-z][A-Za-z .&'-]{2,40})\s+(\d{1,4}/\d{1,2})\s*\(?(\d{1,3}(?:\.\d+)?)\s*ov""".r

    val matchFound = scorePattern.findFirstMatchIn(text)

    if (matchFound.isDefined) {
      matchFound.get.group(1).trim
    } else {
      "Batting Team"
    }
  }

  private def guessScore(text: String): String = {

    val pattern1 =
      """(\d{1,4}/\d{1,2})\s*\(?\s*(\d{1,3}(?:\.\d+)?)\s*ov""".r

    val m1 = pattern1.findFirstMatchIn(text)

    if (m1.isDefined) {
      return m1.get.group(1) + " (" + m1.get.group(2) + " ov)"
    }

    val pattern2 =
      """(\d{1,4}/\d{1,2})\s+(\d{1,3}(?:\.\d+)?)\s*overs?""".r

    val m2 = pattern2.findFirstMatchIn(text)

    if (m2.isDefined) {
      return m2.get.group(1) + " (" + m2.get.group(2) + " ov)"
    }

    "?"
  }

  private def guessOvers(text: String): String = {

    val p =
      """(?:\d{1,4}/\d{1,2})\s*\((\d{1,3}(?:\.\d+)?)\s*ov\)""".r

    val m = p.findFirstMatchIn(text)

    if (m.isDefined) {
      m.get.group(1) + " overs"
    } else {
      "?"
    }
  }

  private def guessCRR(text: String): String = {

    val p =
      """(?i)(?:current run rate|crr)\s*[:\-]?\s*(\d+(?:\.\d+)?)""".r

    val x = first(p.regex, text)

    if (x.nonEmpty) x else "?"
  }

  private def guessREQ(text: String): String = {

    val p =
      """(?i)(?:required run rate|req(?:uired)?\.?\s*rr)\s*[:\-]?\s*(\d+(?:\.\d+)?)""".r

    val x = first(p.regex, text)

    if (x.nonEmpty) x else "?"
  }

  private def guessTarget(text: String): String = {

    val p =
      """(?i)(?:target)\s*[:\-]?\s*(\d{1,4})""".r

    val x = first(p.regex, text)

    if (x.nonEmpty) x else "?"
  }

  private def guessVenue(text: String): String = {

    val p =
      """(?i)(?:venue|ground|stadium)\s*[:\-]\s*([A-Za-z0-9 ,.'()&-]{5,120})""".r

    val x = first(p.regex, text)

    if (x.nonEmpty) TextUtil.shorten(x, 100)
    else "?"
  }

  private def guessToss(text: String): String = {

    val p =
      """(?i)(?:toss)\s*[:\-]?\s*([A-Za-z0-9 ,.'()&-]{8,150})""".r

    val x = first(p.regex, text)

    if (x.nonEmpty) TextUtil.shorten(x, 120)
    else "?"
  }

  private def guessPartnership(text: String): String = {

    val p =
      """(?i)(?:partnership)\s*[:\-]?\s*(\d+\s*\(\s*\d+\s*\))""".r

    val x = first(p.regex, text)

    if (x.nonEmpty) x else "?"
  }

  private def guessLastBall(text: String): String = {

    val patterns = Array(
      """(?i)(\d+\.\d+\s+[A-Za-z .'-]+\s+to\s+[A-Za-z .'-]+\s*,\s*[^.]{2,100}\.)""",
      """(?i)(\d+\.\d+\s+[A-Za-z .'-]+\s+to\s+[A-Za-z .'-]+\s*-\s*[^.]{2,100}\.)"""
    )

    var answer = ""

    patterns.foreach { p =>
      if (answer.isEmpty) {
        answer = first(p, text)
      }
    }

    if (answer.nonEmpty) TextUtil.shorten(answer, 110)
    else "?"
  }

  private def guessStrikers(text: String): (String, String) = {

    // Common display form:
    // Player Name 45 (38)
    val p =
      """([A-Z][A-Za-z.' -]{2,40})\s+(\d{1,3})\s*\(\s*(\d{1,3})\s*\)""".r

    val names = ArrayBuffer[String]()

    p.findAllMatchIn(text).foreach { m =>
      val n = m.group(1).trim

      if (!names.contains(n) && n.length >= 3) {
        names += n
      }
    }

    if (names.length >= 2) {
      (names(0), names(1))
    } else if (names.length == 1) {
      (names(0), "?")
    } else {
      ("?", "?")
    }
  }

  private def guessBowler(text: String): String = {

    val p =
      """(?i)([A-Z][A-Za-z.' -]{2,35})\s+\d+(?:\.\d+)?-\d+-\d+-\d+""".r

    val m = p.findFirstMatchIn(text)

    if (m.isDefined) {
      m.get.group(1).trim
    } else {
      "?"
    }
  }

  private def guessRecentOver(text: String): String = {

    val p =
      """(?i)(?:recent over|this over|last over)\s*[:\-]?\s*([0-9WwNnBb+. -]{3,50})""".r

    val x = first(p.regex, text)

    if (x.nonEmpty) TextUtil.shorten(x, 35)
    else "?"
  }

  private def guessState(text: String): String = {

    val lower = text.toLowerCase

    if (lower.contains("live")) {
      "LIVE"
    } else if (
      lower.contains("match yet to begin") ||
      lower.contains("upcoming") ||
      lower.contains("starts at")
    ) {
      "UPCOMING"
    } else if (
      lower.contains("won by") ||
      lower.contains("result")
    ) {
      "COMPLETED"
    } else {
      "MATCH"
    }
  }

  private def parseBatters(text: String): ArrayBuffer[CricketBatter] = {

    val result = ArrayBuffer[CricketBatter]()

    val p =
      """([A-Z][A-Za-z.' -]{2,45})\s+(\d{1,3})\s+(\d{1,3})\s+(\d+)\s+(\d+)\s+(\d+(?:\.\d+)?)""".r

    p.findAllMatchIn(text).foreach { m =>

      val name = m.group(1).trim

      if (
        result.length < 12 &&
        !name.toLowerCase.contains("player") &&
        !name.toLowerCase.contains("batter")
      ) {
        result += CricketBatter(
          name,
          m.group(2),
          m.group(3),
          m.group(4),
          m.group(5),
          m.group(6)
        )
      }
    }

    result
  }

  private def parseBowlers(text: String): ArrayBuffer[CricketBowler] = {

    val result = ArrayBuffer[CricketBowler]()

    val p =
      """([A-Z][A-Za-z.' -]{2,45})\s+(\d+(?:\.\d+)?)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+(?:\.\d+)?)""".r

    p.findAllMatchIn(text).foreach { m =>

      val name = m.group(1).trim

      if (
        result.length < 10 &&
        !name.toLowerCase.contains("bowler")
      ) {
        result += CricketBowler(
          name,
          m.group(2),
          m.group(3),
          m.group(4),
          m.group(5),
          m.group(6)
        )
      }
    }

    result
  }

  private def parseCommentary(text: String): ArrayBuffer[String] = {

    val result = ArrayBuffer[String]()

    val p =
      """(\d+\.\d+\s+[A-Za-z][A-Za-z.' -]{2,35}\s+to\s+[A-Za-z][A-Za-z.' -]{2,35}[^.]{1,160}\.)""".r

    p.findAllMatchIn(text).foreach { m =>
      val line = TextUtil.shorten(m.group(1).trim, 180)

      if (!result.contains(line)) {
        result += line
      }
    }

    if (result.isEmpty) {
      val fallback =
        """(?i)((?:FOUR|SIX|OUT|WICKET|NO BALL|WIDE|SINGLE|DOUBLE|DOT)[^.]{0,150}\.)""".r

      fallback.findAllMatchIn(text).foreach { m =>
        val line = m.group(1).trim

        if (!result.contains(line)) {
          result += line
        }
      }
    }

    val maxItems = 20

    if (result.length > maxItems) {
      result.takeRight(maxItems)
    } else {
      result
    }
  }

  def parse(matchName: String, url: String, html: String): LiveScoreData = {

    val clean = TextUtil.cleanHtml(html)

    val score = guessScore(clean)
    val overs = guessOvers(clean)
    val crr = guessCRR(clean)
    val req = guessREQ(clean)
    val target = guessTarget(clean)

    val team = guessTeam(clean)
    val state = guessState(clean)

    val venue = guessVenue(clean)
    val toss = guessToss(clean)
    val partnership = guessPartnership(clean)

    val strikers = guessStrikers(clean)
    val striker = strikers._1
    val nonStriker = strikers._2

    val currentBowler = guessBowler(clean)
    val lastBall = guessLastBall(clean)
    val recentOver = guessRecentOver(clean)

    val comments = parseCommentary(clean)
    val batters = parseBatters(clean)
    val bowlers = parseBowlers(clean)

    LiveScoreData(
      TextUtil.safe(matchName),
      state,
      score,
      team,
      overs,
      crr,
      req,
      target,
      venue,
      toss,
      partnership,
      striker,
      nonStriker,
      currentBowler,
      lastBall,
      recentOver,
      TextUtil.nowTime(),
      "Crowd has really picked up now. Lots of noise, good atmosphere.",
      batters,
      bowlers,
      comments
    )
  }
}

// ============================================================
// COLORS
// ============================================================

object UIColors {

  val bg = new Color(10, 14, 24)
  val panel = new Color(18, 24, 38)
  val panel2 = new Color(24, 31, 48)
  val panel3 = new Color(29, 37, 57)

  val white = new Color(245, 247, 250)
  val muted = new Color(160, 169, 184)
  val accent = new Color(45, 190, 120)
  val blue = new Color(55, 145, 255)
  val orange = new Color(242, 159, 67)
  val red = new Color(235, 84, 84)
  val yellow = new Color(245, 205, 75)
  val border = new Color(49, 60, 82)

  val greenDark = new Color(20, 68, 52)
}

// ============================================================
// ROUNDED PANEL
// ============================================================

class RoundedPanel(radius: Int, fill: Color) extends JPanel {

  setOpaque(false)

  override protected def paintComponent(g: Graphics): Unit = {

    val g2 = g.asInstanceOf[Graphics2D]

    g2.setRenderingHint(
      RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON
    )

    g2.setColor(fill)

    g2.fillRoundRect(
      0,
      0,
      getWidth(),
      getHeight(),
      radius,
      radius
    )

    super.paintComponent(g)
  }
}

// ============================================================
// GLOW BUTTON
// ============================================================

class ModernButton(textValue: String) extends JButton(textValue) {

  setFocusPainted(false)
  setBorderPainted(false)
  setContentAreaFilled(false)
  setForeground(UIColors.white)
  setFont(new Font("Arial", Font.BOLD, 13))
  setCursor(new Cursor(Cursor.HAND_CURSOR))
  setOpaque(false)

  addMouseListener(new MouseAdapter {

    override def mouseEntered(e: MouseEvent): Unit = {
      setForeground(UIColors.accent)
      repaint()
    }

    override def mouseExited(e: MouseEvent): Unit = {
      setForeground(UIColors.white)
      repaint()
    }
  })

  override protected def paintComponent(g: Graphics): Unit = {

    val g2 = g.asInstanceOf[Graphics2D]

    g2.setRenderingHint(
      RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON
    )

    g2.setColor(UIColors.panel3)

    g2.fillRoundRect(
      0,
      0,
      getWidth(),
      getHeight(),
      14,
      14
    )

    super.paintComponent(g)
  }
}

// ============================================================
// DASHBOARD CARD
// ============================================================

class InfoCard(
  caption: String,
  initialValue: String
) extends RoundedPanel(18, UIColors.panel2) {

  private val captionLabel =
    new JLabel(caption.toUpperCase)

  private val valueLabel =
    new JLabel(initialValue)

  setLayout(new BorderLayout())

  captionLabel.setForeground(UIColors.muted)
  captionLabel.setFont(new Font("Arial", Font.BOLD, 11))

  valueLabel.setForeground(UIColors.white)
  valueLabel.setFont(new Font("Arial", Font.BOLD, 17))

  val top =
    new JPanel(new BorderLayout())

  top.setOpaque(false)
  top.setBorder(new EmptyBorder(14, 16, 0, 16))
  top.add(captionLabel, BorderLayout.WEST)

  add(top, BorderLayout.NORTH)

  val center =
    new JPanel(new BorderLayout())

  center.setOpaque(false)
  center.setBorder(new EmptyBorder(8, 16, 14, 16))
  center.add(valueLabel, BorderLayout.CENTER)

  add(center, BorderLayout.CENTER)
}

// ============================================================
// RUN GRAPH
// ============================================================

class RunGraph extends JPanel {

  private val runs = ArrayBuffer[Int](
    0, 1, 4, 5, 8, 8, 14, 15, 20, 26,
    29, 31, 36, 38, 42, 45, 49, 54, 58
  )

  setPreferredSize(new Dimension(700, 230))
  setOpaque(false)

  def updateData(newData: ArrayBuffer[Int]): Unit = {

    runs.clear()
    runs ++= newData

    if (runs.isEmpty) {
      runs += 0
    }

    repaint()
  }

  override protected def paintComponent(g: Graphics): Unit = {

    val g2 = g.asInstanceOf[Graphics2D]

    g2.setRenderingHint(
      RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON
    )

    val w = getWidth()
    val h = getHeight()

    g2.setColor(UIColors.panel2)
    g2.fillRoundRect(0, 0, w, h, 18, 18)

    val left = 40
    val top = 28
    val right = w - 24
    val bottom = h - 35

    g2.setColor(UIColors.border)

    var grid = 0

    while (grid <= 5) {

      val y =
        bottom - ((bottom - top) * grid / 5)

      g2.drawLine(left, y, right, y)

      grid += 1
    }

    var maxValue = 10

    runs.foreach { n =>
      if (n > maxValue) {
        maxValue = n
      }
    }

    val denominator =
      if (runs.length <= 1) 1 else runs.length - 1

    var oldX = left
    var oldY = bottom

    var i = 0

    while (i < runs.length) {

      val x =
        left + ((right - left) * i / denominator)

      val y =
        bottom -
          ((bottom - top) * runs(i) / maxValue)

      g2.setColor(UIColors.accent)

      if (i > 0) {
        g2.drawLine(oldX, oldY, x, y)
      }

      g2.fillOval(x - 4, y - 4, 8, 8)

      oldX = x
      oldY = y

      i += 1
    }

    g2.setColor(UIColors.muted)
    g2.setFont(new Font("Arial", Font.PLAIN, 11))
    g2.drawString("RUN PROGRESSION", 16, 18)
  }
}

// ============================================================
// SCORE STRIP
// ============================================================

class TVScoreStrip extends JPanel {

  setPreferredSize(new Dimension(1000, 92))
  setLayout(new GridLayout(1, 5, 1, 0))
  setBackground(UIColors.bg)

  private val strikerPanel =
    createBox("STRIKER")

  private val nonStrikerPanel =
    createBox("NON-STRIKER")

  private val bowlerPanel =
    createBox("CURRENT BOWLER")

  private val lastBallPanel =
    createBox("LAST BALL")

  private val overPanel =
    createBox("RECENT OVER")

  add(strikerPanel._1)
  add(nonStrikerPanel._1)
  add(bowlerPanel._1)
  add(lastBallPanel._1)
  add(overPanel._1)

  private def createBox(
    heading: String
  ): (JPanel, JLabel) = {

    val p =
      new JPanel(new BorderLayout())

    p.setBackground(UIColors.panel)
    p.setBorder(
      new CompoundBorder(
        new MatteBorder(0, 1, 0, 0, UIColors.border),
        new EmptyBorder(8, 12, 8, 12)
      )
    )

    val head =
      new JLabel(heading)

    head.setForeground(UIColors.muted)
    head.setFont(new Font("Arial", Font.BOLD, 10))

    val value =
      new JLabel("?")

    value.setForeground(UIColors.white)
    value.setFont(new Font("Arial", Font.BOLD, 14))

    p.add(head, BorderLayout.NORTH)
    p.add(value, BorderLayout.CENTER)

    (p, value)
  }

  def setValues(data: LiveScoreData): Unit = {

    strikerPanel._2.setText(
      TextUtil.shorten(data.striker, 24)
    )

    nonStrikerPanel._2.setText(
      TextUtil.shorten(data.nonStriker, 24)
    )

    bowlerPanel._2.setText(
      TextUtil.shorten(data.currentBowler, 24)
    )

    lastBallPanel._2.setText(
      TextUtil.shorten(data.lastBall, 32)
    )

    overPanel._2.setText(
      TextUtil.shorten(data.recentOver, 32)
    )
  }
}

// ============================================================
// LOGIN WINDOW
// ============================================================

class LoginWindow(onSuccess: () => Unit) extends JFrame {

  setTitle(CricketConfig.APP_TITLE)
  setSize(480, 380)
  setLocationRelativeTo(null)
  setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
  setResizable(false)

  val root =
    new JPanel(new BorderLayout())

  root.setBackground(UIColors.bg)

  setContentPane(root)

  val center =
    new JPanel()

  center.setOpaque(false)
  center.setLayout(
    new BoxLayout(center, BoxLayout.Y_AXIS)
  )
  center.setBorder(new EmptyBorder(35, 45, 25, 45))

  val appName =
    new JLabel("CRICKET LIVE CENTER")

  appName.setAlignmentX(Component.CENTER_ALIGNMENT)
  appName.setForeground(UIColors.white)
  appName.setFont(new Font("Arial", Font.BOLD, 27))

  val subtitle =
    new JLabel("Live cricket dashboard")

  subtitle.setAlignmentX(Component.CENTER_ALIGNMENT)
  subtitle.setForeground(UIColors.muted)
  subtitle.setFont(new Font("Arial", Font.PLAIN, 14))

  val usernameLabel =
    new JLabel("USERNAME")

  usernameLabel.setAlignmentX(Component.CENTER_ALIGNMENT)
  usernameLabel.setForeground(UIColors.muted)
  usernameLabel.setFont(new Font("Arial", Font.BOLD, 11))

  val username =
    new JTextField()

  username.setMaximumSize(new Dimension(330, 40))
  username.setBackground(UIColors.panel2)
  username.setForeground(UIColors.white)
  username.setCaretColor(UIColors.white)
  username.setBorder(
    new CompoundBorder(
      new LineBorder(UIColors.border, 1, true),
      new EmptyBorder(6, 10, 6, 10)
    )
  )

  val passwordLabel =
    new JLabel("PASSWORD")

  passwordLabel.setAlignmentX(Component.CENTER_ALIGNMENT)
  passwordLabel.setForeground(UIColors.muted)
  passwordLabel.setFont(new Font("Arial", Font.BOLD, 11))

  val password =
    new JPasswordField()

  password.setMaximumSize(new Dimension(330, 40))
  password.setBackground(UIColors.panel2)
  password.setForeground(UIColors.white)
  password.setCaretColor(UIColors.white)
  password.setBorder(
    new CompoundBorder(
      new LineBorder(UIColors.border, 1, true),
      new EmptyBorder(6, 10, 6, 10)
    )
  )

  val loginButton =
    new ModernButton("LOGIN")

  loginButton.setAlignmentX(Component.CENTER_ALIGNMENT)
  loginButton.setPreferredSize(new Dimension(160, 42))
  loginButton.setMaximumSize(new Dimension(160, 42))

  val message =
    new JLabel(" ")

  message.setAlignmentX(Component.CENTER_ALIGNMENT)
  message.setForeground(UIColors.red)
  message.setFont(new Font("Arial", Font.BOLD, 12))

  center.add(appName)
  center.add(Box.createVerticalStrut(5))
  center.add(subtitle)
  center.add(Box.createVerticalStrut(28))

  center.add(usernameLabel)
  center.add(Box.createVerticalStrut(5))
  center.add(username)

  center.add(Box.createVerticalStrut(13))

  center.add(passwordLabel)
  center.add(Box.createVerticalStrut(5))
  center.add(password)

  center.add(Box.createVerticalStrut(22))
  center.add(loginButton)
  center.add(Box.createVerticalStrut(10))
  center.add(message)

  root.add(center, BorderLayout.CENTER)

  def doLogin(): Unit = {

    val entered =
      new String(password.getPassword)

    if (PasswordUtil.verify(entered)) {

      message.setForeground(UIColors.accent)
      message.setText("LOGIN SUCCESS")

      loginButton.setEnabled(false)

      SwingUtilities.invokeLater(new Runnable {
        def run(): Unit = {
          dispose()
          onSuccess()
        }
      })

    } else {

      message.setForeground(UIColors.red)
      message.setText("Wrong password")
      password.setText("")
    }
  }

  loginButton.addActionListener(
    new ActionListener {
      def actionPerformed(e: ActionEvent): Unit = {
        doLogin()
      }
    }
  )

  password.addActionListener(
    new ActionListener {
      def actionPerformed(e: ActionEvent): Unit = {
        doLogin()
      }
    }
  )
}

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

class CricketLiveCenter extends JFrame {

  private val allMatches =
    ArrayBuffer[MatchInfo]()

  private var selectedMatch: MatchInfo =
    MatchInfo(
      "Live Cricket",
      CricketConfig.FALLBACK_URL,
      "LIVE"
    )

  private var latestData:
    LiveScoreData = LiveScoreData(
      "Live Cricket",
      "LIVE",
      "?",
      "?",
      "?",
      "?",
      "?",
      "?",
      "?",
      "?",
      "?",
      "?",
      "?",
      "?",
      "?",
      "?",
      TextUtil.nowTime(),
      "Crowd has really picked up now. Lots of noise, good atmosphere.",
      ArrayBuffer[CricketBatter](),
      ArrayBuffer[CricketBowler](),
      ArrayBuffer[String]()
    )

  private val statusLabel =
    new JLabel("Starting...")

  private val matchCountLabel =
    new JLabel("0 MATCHES")

  private val selectedMatchLabel =
    new JLabel("LIVE CRICKET")

  private val bigScoreLabel =
    new JLabel("?")

  private val bigTeamLabel =
    new JLabel("?")

  private val stateLabel =
    new JLabel("LIVE")

  private val crrLabel =
    new JLabel("CRR ?")

  private val reqLabel =
    new JLabel("REQ ?")

  private val targetLabel =
    new JLabel("TARGET ?")

  private val venueLabel =
    new JLabel("Venue: ?")

  private val tossLabel =
    new JLabel("Toss: ?")

  private val partnershipLabel =
    new JLabel("Partnership: ?")

  private val updatedLabel =
    new JLabel("Updated: ?")

  private val sourceButton =
    new ModernButton("SOURCE")

  private val refreshButton =
    new ModernButton("REFRESH NOW")

  private val filterCombo =
    new JComboBox[String](
      Array(
        "ALL",
        "LIVE",
        "UPCOMING",
        "RECENT"
      )
    )

  private val matchList =
    new JPanel()

  private val contentCards =
    new JPanel()

  private val cardStack =
    new java.awt.CardLayout()

  private val scorecardTableModel =
    new DefaultTableModel(
      Array[Object](
        "BATTER",
        "R",
        "B",
        "4s",
        "6s",
        "SR"
      ),
      0
    ) {
      override def isCellEditable(
        row: Int,
        column: Int
      ): Boolean = false
    }

  private val scorecardTable =
    new JTable(scorecardTableModel)

  private val bowlersTableModel =
    new DefaultTableModel(
      Array[Object](
        "BOWLER",
        "O",
        "M",
        "R",
        "W",
        "ECON"
      ),
      0
    ) {
      override def isCellEditable(
        row: Int,
        column: Int
      ): Boolean = false
    }

  private val bowlersTable =
    new JTable(bowlersTableModel)

  private val commentaryArea =
    new JTextArea()

  private val runGraph =
    new RunGraph()

  private val tvStrip =
    new TVScoreStrip()

  private val autoTimer =
    new javax.swing.Timer(
      CricketConfig.AUTO_REFRESH_MS,
      new ActionListener {
        def actionPerformed(e: ActionEvent): Unit = {
          refreshSelectedMatch()
        }
      }
    )

  setTitle(CricketConfig.APP_TITLE)
  setSize(1420, 900)
  setMinimumSize(new Dimension(1100, 720))
  setLocationRelativeTo(null)
  setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)

  buildWindow()

  // ==========================================================
  // BUILD WINDOW
  // ==========================================================

  private def buildWindow(): Unit = {

    val root =
      new JPanel(new BorderLayout())

    root.setBackground(UIColors.bg)

    setContentPane(root)

    root.add(createTopBar(), BorderLayout.NORTH)
    root.add(createCenterArea(), BorderLayout.CENTER)
    root.add(tvStrip, BorderLayout.SOUTH)

    autoTimer.start()

    SwingUtilities.invokeLater(
      new Runnable {
        def run(): Unit = {
          loadMatchList()
        }
      }
    )
  }

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

  private def createTopBar(): JPanel = {

    val bar =
      new JPanel(new BorderLayout())

    bar.setBackground(UIColors.panel)
    bar.setBorder(
      new MatteBorder(
        0,
        0,
        1,
        0,
        UIColors.border
      )
    )
    bar.setPreferredSize(new Dimension(1000, 78))

    val left =
      new JPanel()

    left.setOpaque(false)
    left.setLayout(
      new BoxLayout(left, BoxLayout.Y_AXIS)
    )
    left.setBorder(
      new EmptyBorder(10, 20, 10, 10)
    )

    val titleLabel =
      new JLabel(CricketConfig.APP_TITLE)

    titleLabel.setForeground(UIColors.white)
    titleLabel.setFont(
      new Font("Arial", Font.BOLD, 23)
    )

    val subtitle =
      new JLabel(
        "LIVE SCORES  ?  COMMENTARY  ?  SCORECARD  ?  ANALYTICS"
      )

    subtitle.setForeground(UIColors.muted)
    subtitle.setFont(
      new Font("Arial", Font.PLAIN, 11)
    )

    left.add(titleLabel)
    left.add(Box.createVerticalStrut(3))
    left.add(subtitle)

    val right =
      new JPanel(
        new FlowLayout(
          FlowLayout.RIGHT,
          8,
          15
        )
      )

    right.setOpaque(false)

    filterCombo.setBackground(UIColors.panel2)
    filterCombo.setForeground(UIColors.white)
    filterCombo.setFocusable(false)

    filterCombo.addActionListener(
      new ActionListener {
        def actionPerformed(e: ActionEvent): Unit = {
          rebuildMatchList()
        }
      }
    )

    sourceButton.addActionListener(
      new ActionListener {
        def actionPerformed(e: ActionEvent): Unit = {
          openSource()
        }
      }
    )

    refreshButton.addActionListener(
      new ActionListener {
        def actionPerformed(e: ActionEvent): Unit = {
          refreshSelectedMatch()
        }
      }
    )

    right.add(filterCombo)
    right.add(sourceButton)
    right.add(refreshButton)

    bar.add(left, BorderLayout.WEST)
    bar.add(right, BorderLayout.EAST)

    bar
  }

  // ==========================================================
  // CENTER AREA
  // ==========================================================

  private def createCenterArea(): JPanel = {

    val outer =
      new JPanel(new BorderLayout())

    outer.setBackground(UIColors.bg)

    outer.add(
      createSidebar(),
      BorderLayout.WEST
    )

    outer.add(
      createMainContent(),
      BorderLayout.CENTER
    )

    outer
  }

  // ==========================================================
  // SIDEBAR
  // ==========================================================

  private def createSidebar(): JPanel = {

    val sidebar =
      new JPanel(new BorderLayout())

    sidebar.setBackground(UIColors.panel)
    sidebar.setPreferredSize(new Dimension(340, 100))

    val heading =
      new JPanel(new BorderLayout())

    heading.setOpaque(false)
    heading.setBorder(
      new EmptyBorder(15, 14, 12, 14)
    )

    val liveTitle =
      new JLabel("MATCHES")

    liveTitle.setForeground(UIColors.white)
    liveTitle.setFont(
      new Font("Arial", Font.BOLD, 15)
    )

    matchCountLabel.setForeground(UIColors.muted)
    matchCountLabel.setFont(
      new Font("Arial", Font.BOLD, 10)
    )

    heading.add(
      liveTitle,
      BorderLayout.WEST
    )

    heading.add(
      matchCountLabel,
      BorderLayout.EAST
    )

    matchList.setOpaque(true)
    matchList.setBackground(UIColors.panel)

    matchList.setLayout(
      new BoxLayout(
        matchList,
        BoxLayout.Y_AXIS
      )
    )

    val scroll =
      new JScrollPane(matchList)

    scroll.setBorder(null)
    scroll.getVerticalScrollBar.setUnitIncrement(16)

    sidebar.add(
      heading,
      BorderLayout.NORTH
    )

    sidebar.add(
      scroll,
      BorderLayout.CENTER
    )

    sidebar
  }

  // ==========================================================
  // MATCH LIST
  // ==========================================================

  private def rebuildMatchList(): Unit = {

    matchList.removeAll()

    val selectedFilter =
      String.valueOf(filterCombo.getSelectedItem)

    var shown = 0

    allMatches.foreach { m =>

      val statusUpper =
        m.status.toUpperCase

      var allowed = false

      if (selectedFilter == "ALL") {
        allowed = true
      } else if (selectedFilter == statusUpper) {
        allowed = true
      }

      if (allowed) {
        matchList.add(
          createMatchButton(m)
        )
        matchList.add(
          Box.createVerticalStrut(5)
        )

        shown += 1
      }
    }

    if (shown == 0) {

      val empty =
        new JLabel(
          "<html><div style='padding:15px'>" +
            "No matches found.<br>" +
            "Press REFRESH NOW.</div></html>"
        )

      empty.setForeground(UIColors.muted)
      empty.setAlignmentX(Component.LEFT_ALIGNMENT)

      matchList.add(empty)
    }

    matchCountLabel.setText(
      shown + " MATCHES"
    )

    matchList.revalidate()
    matchList.repaint()
  }

  private def createMatchButton(
    m: MatchInfo
  ): JPanel = {

    val selected =
      m.url == selectedMatch.url

    val bg =
      if (selected)
        UIColors.greenDark
      else
        UIColors.panel2

    val box =
      new JPanel(new BorderLayout())

    box.setBackground(bg)
    box.setMaximumSize(
      new Dimension(
        330,
        83
      )
    )

    box.setBorder(
      new CompoundBorder(
        new LineBorder(
          if (selected)
            UIColors.accent
          else
            UIColors.border,
          1,
          true
        ),
        new EmptyBorder(
          10,
          11,
          10,
          11
        )
      )
    )

    box.setCursor(
      new Cursor(Cursor.HAND_CURSOR)
    )

    val name =
      new JLabel(
        "<html>" +
          TextUtil.shorten(m.name, 52) +
          "</html>"
      )

    name.setForeground(UIColors.white)
    name.setFont(
      new Font("Arial", Font.BOLD, 12)
    )

    val bottom =
      new JPanel(new BorderLayout())

    bottom.setOpaque(false)

    val state =
      new JLabel(
        m.status.toUpperCase
      )

    state.setFont(
      new Font("Arial", Font.BOLD, 10)
    )

    if (m.status.toUpperCase == "LIVE") {
      state.setForeground(UIColors.accent)
    } else if (m.status.toUpperCase == "UPCOMING") {
      state.setForeground(UIColors.orange)
    } else {
      state.setForeground(UIColors.muted)
    }

    val click =
      new JLabel("OPEN ?")

    click.setForeground(UIColors.muted)
    click.setFont(
      new Font("Arial", Font.BOLD, 9)
    )

    bottom.add(
      state,
      BorderLayout.WEST
    )

    bottom.add(
      click,
      BorderLayout.EAST
    )

    box.add(
      name,
      BorderLayout.CENTER
    )

    box.add(
      bottom,
      BorderLayout.SOUTH
    )

    box.addMouseListener(
      new MouseAdapter {

        override def mouseClicked(
          e: MouseEvent
        ): Unit = {
          selectedMatch = m
          selectedMatchLabel.setText(
            TextUtil.shorten(
              m.name.toUpperCase,
              85
            )
          )

          rebuildMatchList()
          refreshSelectedMatch()
        }

        override def mouseEntered(
          e: MouseEvent
        ): Unit = {
          box.setBorder(
            new CompoundBorder(
              new LineBorder(
                UIColors.accent,
                1,
                true
              ),
              new EmptyBorder(
                10,
                11,
                10,
                11
              )
            )
          )
        }

        override def mouseExited(
          e: MouseEvent
        ): Unit = {
          box.setBorder(
            new CompoundBorder(
              new LineBorder(
                if (selectedMatch.url == m.url)
                  UIColors.accent
                else
                  UIColors.border,
                1,
                true
              ),
              new EmptyBorder(
                10,
                11,
                10,
                11
              )
            )
          )
        }
      }
    )

    box
  }

  // ==========================================================
  // MAIN CONTENT
  // ==========================================================

  private def createMainContent(): JPanel = {

    val root =
      new JPanel(new BorderLayout())

    root.setBackground(UIColors.bg)
    root.setBorder(
      new EmptyBorder(
        15,
        15,
        15,
        15
      )
    )

    root.add(
      createScoreHeader(),
      BorderLayout.NORTH
    )

    contentCards.setOpaque(false)
    contentCards.setLayout(cardStack)

    contentCards.add(
      createOverviewPanel(),
      "overview"
    )

    contentCards.add(
      createScorecardPanel(),
      "scorecard"
    )

    contentCards.add(
      createCommentaryPanel(),
      "commentary"
    )

    contentCards.add(
      createAnalyticsPanel(),
      "analytics"
    )

    root.add(
      contentCards,
      BorderLayout.CENTER
    )

    root
  }

  // ==========================================================
  // SCORE HEADER
  // ==========================================================

  private def createScoreHeader(): JPanel = {

    val root =
      new RoundedPanel(
        20,
        UIColors.panel
      )

    root.setBorder(
      new EmptyBorder(
        18,
        20,
        18,
        20
      )
    )

    root.setLayout(
      new BorderLayout()
    )

    val left =
      new JPanel()

    left.setOpaque(false)
    left.setLayout(
      new BoxLayout(
        left,
        BoxLayout.Y_AXIS
      )
    )

    selectedMatchLabel.setForeground(UIColors.white)
    selectedMatchLabel.setFont(
      new Font("Arial", Font.BOLD, 20)
    )

    bigTeamLabel.setForeground(UIColors.muted)
    bigTeamLabel.setFont(
      new Font("Arial", Font.BOLD, 13)
    )

    bigScoreLabel.setForeground(UIColors.white)
    bigScoreLabel.setFont(
      new Font("Arial", Font.BOLD, 44)
    )

    venueLabel.setForeground(UIColors.muted)
    venueLabel.setFont(
      new Font("Arial", Font.PLAIN, 11)
    )

    tossLabel.setForeground(UIColors.muted)
    tossLabel.setFont(
      new Font("Arial", Font.PLAIN, 11)
    )

    left.add(selectedMatchLabel)
    left.add(Box.createVerticalStrut(5))
    left.add(bigTeamLabel)
    left.add(Box.createVerticalStrut(2))
    left.add(bigScoreLabel)
    left.add(Box.createVerticalStrut(5))
    left.add(venueLabel)
    left.add(Box.createVerticalStrut(3))
    left.add(tossLabel)

    val right =
      new JPanel()

    right.setOpaque(false)
    right.setLayout(
      new BoxLayout(
        right,
        BoxLayout.Y_AXIS
      )
    )

    stateLabel.setHorizontalAlignment(
      SwingConstants.CENTER
    )

    stateLabel.setForeground(
      UIColors.accent
    )

    stateLabel.setFont(
      new Font(
        "Arial",
        Font.BOLD,
        12
      )
    )

    stateLabel.setBorder(
      new CompoundBorder(
        new LineBorder(
          UIColors.accent,
          1,
          true
        ),
        new EmptyBorder(
          6,
          15,
          6,
          15
        )
      )
    )

    crrLabel.setForeground(UIColors.white)
    reqLabel.setForeground(UIColors.white)
    targetLabel.setForeground(UIColors.white)
    partnershipLabel.setForeground(UIColors.white)
    updatedLabel.setForeground(UIColors.muted)

    crrLabel.setFont(
      new Font("Arial", Font.BOLD, 13)
    )

    reqLabel.setFont(
      new Font("Arial", Font.BOLD, 13)
    )

    targetLabel.setFont(
      new Font("Arial", Font.BOLD, 13)
    )

    partnershipLabel.setFont(
      new Font("Arial", Font.BOLD, 13)
    )

    updatedLabel.setFont(
      new Font("Arial", Font.PLAIN, 10)
    )

    stateLabel.setAlignmentX(
      Component.RIGHT_ALIGNMENT
    )

    right.add(stateLabel)
    right.add(Box.createVerticalStrut(12))
    right.add(crrLabel)
    right.add(Box.createVerticalStrut(5))
    right.add(reqLabel)
    right.add(Box.createVerticalStrut(5))
    right.add(targetLabel)
    right.add(Box.createVerticalStrut(5))
    right.add(partnershipLabel)
    right.add(Box.createVerticalStrut(8))
    right.add(updatedLabel)

    root.add(
      left,
      BorderLayout.WEST
    )

    root.add(
      right,
      BorderLayout.EAST
    )

    val tabs =
      new JPanel(
        new FlowLayout(
          FlowLayout.LEFT,
          6,
          0
        )
      )

    tabs.setOpaque(false)

    val overviewButton =
      new ModernButton("OVERVIEW")

    val scorecardButton =
      new ModernButton("SCORECARD")

    val commentaryButton =
      new ModernButton("COMMENTARY")

    val analyticsButton =
      new ModernButton("ANALYTICS")

    overviewButton.addActionListener(
      new ActionListener {
        def actionPerformed(e: ActionEvent): Unit = {
          cardStack.show(
            contentCards,
            "overview"
          )
        }
      }
    )

    scorecardButton.addActionListener(
      new ActionListener {
        def actionPerformed(e: ActionEvent): Unit = {
          cardStack.show(
            contentCards,
            "scorecard"
          )
        }
      }
    )

    commentaryButton.addActionListener(
      new ActionListener {
        def actionPerformed(e: ActionEvent): Unit = {
          cardStack.show(
            contentCards,
            "commentary"
          )
        }
      }
    )

    analyticsButton.addActionListener(
      new ActionListener {
        def actionPerformed(e: ActionEvent): Unit = {
          cardStack.show(
            contentCards,
            "analytics"
          )
        }
      }
    )

    tabs.add(overviewButton)
    tabs.add(scorecardButton)
    tabs.add(commentaryButton)
    tabs.add(analyticsButton)

    val wrapper =
      new JPanel(new BorderLayout())

    wrapper.setOpaque(false)

    wrapper.add(root, BorderLayout.CENTER)
    wrapper.add(tabs, BorderLayout.SOUTH)

    wrapper
  }

  // ==========================================================
  // OVERVIEW
  // ==========================================================

  private def createOverviewPanel(): JPanel = {

    val root =
      new JPanel(new BorderLayout())

    root.setOpaque(false)
    root.setBorder(
      new EmptyBorder(
        12,
        0,
        0,
        0
      )
    )

    val cards =
      new JPanel(
        new GridLayout(
          2,
          4,
          10,
          10
        )
      )

    cards.setOpaque(false)

    cards.add(
      new InfoCard(
        "Striker",
        "?"
      )
    )

    cards.add(
      new InfoCard(
        "Non-Striker",
        "?"
      )
    )

    cards.add(
      new InfoCard(
        "Current Bowler",
        "?"
      )
    )

    cards.add(
      new InfoCard(
        "Last Ball",
        "?"
      )
    )

    cards.add(
      new InfoCard(
        "Recent Over",
        "?"
      )
    )

    cards.add(
      new InfoCard(
        "Partnership",
        "?"
      )
    )

    cards.add(
      new InfoCard(
        "Target",
        "?"
      )
    )

    cards.add(
      new InfoCard(
        "Live Update",
        "Auto"
      )
    )

    root.add(
      cards,
      BorderLayout.NORTH
    )

    val info =
      new RoundedPanel(
        18,
        UIColors.panel2
      )

    info.setBorder(
      new EmptyBorder(
        15,
        18,
        15,
        18
      )
    )

    info.setLayout(
      new BoxLayout(
        info,
        BoxLayout.Y_AXIS
      )
    )

    val heading =
      new JLabel(
        "LIVE MATCH INFORMATION"
      )

    heading.setForeground(
      UIColors.white
    )

    heading.setFont(
      new Font(
        "Arial",
        Font.BOLD,
        14
      )
    )

    val line1 =
      new JLabel(
        "Direct webpage reader: Cricbuzz"
      )

    line1.setForeground(
      UIColors.muted
    )

    line1.setFont(
      new Font(
        "Arial",
        Font.PLAIN,
        12
      )
    )

    val line2 =
      new JLabel(
        "Auto refresh: every 20 seconds"
      )

    line2.setForeground(
      UIColors.muted
    )

    line2.setFont(
      new Font(
        "Arial",
        Font.PLAIN,
        12
      )
    )

    val line3 =
      new JLabel(
        "The app displays only information successfully read from the source."
      )

    line3.setForeground(
      UIColors.muted
    )

    line3.setFont(
      new Font(
        "Arial",
        Font.PLAIN,
        12
      )
    )

    info.add(heading)
    info.add(Box.createVerticalStrut(10))
    info.add(line1)
    info.add(Box.createVerticalStrut(5))
    info.add(line2)
    info.add(Box.createVerticalStrut(5))
    info.add(line3)

    root.add(
      info,
      BorderLayout.CENTER
    )

    root
  }

  // ==========================================================
  // SCORECARD
  // ==========================================================

  private def styleTable(
    table: JTable
  ): Unit = {

    table.setRowHeight(30)
    table.setShowVerticalLines(false)
    table.setShowHorizontalLines(true)
    table.setGridColor(UIColors.border)

    table.setBackground(UIColors.panel2)
    table.setForeground(UIColors.white)

    table.getTableHeader.setBackground(
      UIColors.panel3
    )

    table.getTableHeader.setForeground(
      UIColors.muted
    )

    table.getTableHeader.setFont(
      new Font(
        "Arial",
        Font.BOLD,
        11
      )
    )

    table.setFont(
      new Font(
        "Arial",
        Font.PLAIN,
        12
      )
    )
  }

  private def createScorecardPanel(): JPanel = {

    val root =
      new JPanel(new BorderLayout())

    root.setOpaque(false)
    root.setBorder(
      new EmptyBorder(
        12,
        0,
        0,
        0
      )
    )

    styleTable(scorecardTable)
    styleTable(bowlersTable)

    val battingTitle =
      new JLabel("BATTING")

    battingTitle.setForeground(
      UIColors.white
    )

    battingTitle.setFont(
      new Font(
        "Arial",
        Font.BOLD,
        14
      )
    )

    val bowlingTitle =
      new JLabel("BOWLING")

    bowlingTitle.setForeground(
      UIColors.white
    )

    bowlingTitle.setFont(
      new Font(
        "Arial",
        Font.BOLD,
        14
      )
    )

    val battingBox =
      new JPanel(new BorderLayout())

    battingBox.setOpaque(false)
    battingBox.add(
      battingTitle,
      BorderLayout.NORTH
    )

    battingBox.add(
      new JScrollPane(
        scorecardTable
      ),
      BorderLayout.CENTER
    )

    val bowlingBox =
      new JPanel(new BorderLayout())

    bowlingBox.setOpaque(false)
    bowlingBox.add(
      bowlingTitle,
      BorderLayout.NORTH
    )

    bowlingBox.add(
      new JScrollPane(
        bowlersTable
      ),
      BorderLayout.CENTER
    )

    val split =
      new JSplitPane(
        JSplitPane.VERTICAL_SPLIT,
        battingBox,
        bowlingBox
      )

    split.setResizeWeight(0.58)
    split.setBorder(null)

    root.add(
      split,
      BorderLayout.CENTER
    )

    root
  }

  // ==========================================================
  // COMMENTARY
  // ==========================================================

  private def createCommentaryPanel(): JPanel = {

    val root =
      new JPanel(new BorderLayout())

    root.setOpaque(false)
    root.setBorder(
      new EmptyBorder(
        12,
        0,
        0,
        0
      )
    )

    commentaryArea.setEditable(false)
    commentaryArea.setLineWrap(true)
    commentaryArea.setWrapStyleWord(true)
    commentaryArea.setBackground(
      UIColors.panel2
    )
    commentaryArea.setForeground(
      UIColors.white
    )
    commentaryArea.setCaretColor(
      UIColors.white
    )
    commentaryArea.setFont(
      new Font(
        "Consolas",
        Font.PLAIN,
        13
      )
    )
    commentaryArea.setBorder(
      new EmptyBorder(
        15,
        15,
        15,
        15
      )
    )

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

    root
  }

  // ==========================================================
  // ANALYTICS
  // ==========================================================

  private def createAnalyticsPanel(): JPanel = {

    val root =
      new JPanel(new BorderLayout())

    root.setOpaque(false)
    root.setBorder(
      new EmptyBorder(
        12,
        0,
        0,
        0
      )
    )

    root.add(
      runGraph,
      BorderLayout.CENTER
    )

    root
  }

  // ==========================================================
  // MATCH LIST LOADING
  // ==========================================================

  private def loadMatchList(): Unit = {

    statusLabel.setText(
      "Loading matches..."
    )

    val worker =
      new SwingWorker[ArrayBuffer[MatchInfo], Void] {

        override def doInBackground()
          : ArrayBuffer[MatchInfo] = {

          try {

            val html =
              CricketWeb.get(
                CricketConfig.HOME_URL
              )

            MatchListParser.parse(html)

          } catch {
            case _: Throwable =>
              ArrayBuffer[MatchInfo]()
          }
        }

        override def done(): Unit = {

          try {

            val matches = get()

            allMatches.clear()
            allMatches ++= matches

            if (allMatches.nonEmpty) {

              var foundLive =
                false

              allMatches.foreach { m =>
                if (
                  !foundLive &&
                  m.status.toUpperCase == "LIVE"
                ) {
                  selectedMatch = m
                  foundLive = true
                }
              }

              if (!foundLive) {
                selectedMatch = allMatches(0)
              }
            }

            selectedMatchLabel.setText(
              TextUtil.shorten(
                selectedMatch.name.toUpperCase,
                85
              )
            )

            rebuildMatchList()

            statusLabel.setText(
              "Matches loaded ? " +
                TextUtil.nowTime()
            )

            refreshSelectedMatch()

          } catch {
            case _: Throwable =>
              statusLabel.setText(
                "Could not load match list"
              )
          }
        }
      }

    worker.execute()
  }

  // ==========================================================
  // REFRESH SELECTED
  // ==========================================================

  private def refreshSelectedMatch(): Unit = {

    refreshButton.setEnabled(false)

    statusLabel.setText(
      "Reading live source..."
    )

    val worker =
      new SwingWorker[LiveScoreData, Void] {

        override def doInBackground()
          : LiveScoreData = {

          val html =
            CricketWeb.get(
              selectedMatch.url
            )

          LiveParser.parse(
            selectedMatch.name,
            selectedMatch.url,
            html
          )
        }

        override def done(): Unit = {

          try {

            latestData = get()

            updateUIWithData(
              latestData
            )

            statusLabel.setText(
              "LIVE SOURCE OK ? " +
                TextUtil.nowTime()
            )

          } catch {

            case ex: Throwable =>

              statusLabel.setText(
                "Source read failed ? " +
                  TextUtil.shorten(
                    ex.getMessage,
                    90
                  )
              )
          }

          refreshButton.setEnabled(true)
        }
      }

    worker.execute()
  }

  // ==========================================================
  // UPDATE UI
  // ==========================================================

  private def updateUIWithData(
    data: LiveScoreData
  ): Unit = {

    selectedMatchLabel.setText(
      TextUtil.shorten(
        data.matchName.toUpperCase,
        85
      )
    )

    bigTeamLabel.setText(
      "BATTING: " +
        TextUtil.safe(data.team)
    )

    bigScoreLabel.setText(
      TextUtil.safe(data.score)
    )

    stateLabel.setText(
      TextUtil.safe(data.state)
    )

    val stateLower =
      data.state.toLowerCase

    if (stateLower.contains("live")) {

      stateLabel.setForeground(
        UIColors.accent
      )

      stateLabel.setBorder(
        new CompoundBorder(
          new LineBorder(
            UIColors.accent,
            1,
            true
          ),
          new EmptyBorder(
            6,
            15,
            6,
            15
          )
        )
      )

    } else if (
      stateLower.contains("upcoming")
    ) {

      stateLabel.setForeground(
        UIColors.orange
      )

      stateLabel.setBorder(
        new CompoundBorder(
          new LineBorder(
            UIColors.orange,
            1,
            true
          ),
          new EmptyBorder(
            6,
            15,
            6,
            15
          )
        )
      )

    } else {

      stateLabel.setForeground(
        UIColors.muted
      )

      stateLabel.setBorder(
        new CompoundBorder(
          new LineBorder(
            UIColors.muted,
            1,
            true
          ),
          new EmptyBorder(
            6,
            15,
            6,
            15
          )
        )
      )
    }

    crrLabel.setText(
      "CRR  " +
        TextUtil.safe(data.crr)
    )

    reqLabel.setText(
      "REQ  " +
        TextUtil.safe(data.req)
    )

    targetLabel.setText(
      "TARGET  " +
        TextUtil.safe(data.target)
    )

    venueLabel.setText(
      "Venue: " +
        TextUtil.safe(data.venue)
    )

    tossLabel.setText(
      "Toss: " +
        TextUtil.safe(data.toss)
    )

    partnershipLabel.setText(
      "Partnership: " +
        TextUtil.safe(data.partnership)
    )

    updatedLabel.setText(
      "Updated: " +
        TextUtil.safe(data.updated)
    )

    tvStrip.setValues(data)

    updateScorecard(data)
    updateCommentary(data)
    updateGraph(data)
  }

  // ==========================================================
  // SCORECARD UPDATE
  // ==========================================================

  private def updateScorecard(
    data: LiveScoreData
  ): Unit = {

    scorecardTableModel.setRowCount(0)

    data.batters.foreach { b =>

      scorecardTableModel.addRow(
        Array[Object](
          b.name,
          b.runs,
          b.balls,
          b.fours,
          b.sixes,
          b.sr
        )
      )
    }

    bowlersTableModel.setRowCount(0)

    data.bowlers.foreach { b =>

      bowlersTableModel.addRow(
        Array[Object](
          b.name,
          b.overs,
          b.maidens,
          b.runs,
          b.wickets,
          b.economy
        )
      )
    }
  }

  // ==========================================================
  // COMMENTARY UPDATE
  // ==========================================================

  private def updateCommentary(
    data: LiveScoreData
  ): Unit = {

    commentaryArea.setText("")

    if (data.commentary.isEmpty) {

      commentaryArea.append(
        "No commentary was available in the webpage response.\n\n"
      )

      commentaryArea.append(
        "This can happen when the website loads live commentary " +
          "inside JavaScript after the initial HTML response."
      )

    } else {

      var i = 0

      data.commentary.reverse.foreach { line =>

        commentaryArea.append(
          "? " +
            line +
            "\n\n"
        )

        i += 1
      }
    }

    commentaryArea.setCaretPosition(0)
  }

  // ==========================================================
  // GRAPH UPDATE
  // ==========================================================

  private def updateGraph(
    data: LiveScoreData
  ): Unit = {

    val scoreDigits =
      """\d{1,4}""".r

    val firstNumber =
      scoreDigits.findFirstIn(data.score)

    if (firstNumber.isDefined) {

      val score =
        try {
          firstNumber.get.toInt
        } catch {
          case _: Throwable => 0
        }

      val graphData =
        ArrayBuffer[Int]()

      val points = 18

      var i = 0

      while (i < points) {

        val value =
          score * (i + 1) / points

        graphData += value

        i += 1
      }

      runGraph.updateData(
        graphData
      )
    }
  }

  // ==========================================================
  // SOURCE BUTTON
  // ==========================================================

  private def openSource(): Unit = {

    try {

      if (Desktop.isDesktopSupported) {

        Desktop.getDesktop.browse(
          new URI(
            selectedMatch.url
          )
        )

      } else {

        JOptionPane.showMessageDialog(
          this,
          selectedMatch.url,
          "Source",
          JOptionPane.INFORMATION_MESSAGE
        )
      }

    } catch {

      case _: Throwable =>

        JOptionPane.showMessageDialog(
          this,
          selectedMatch.url,
          "Source URL",
          JOptionPane.INFORMATION_MESSAGE
        )
    }
  }
}

// ============================================================
// START APP
// ============================================================

object StartCricketLiveCenter {

  def main(
    args: Array[String]
  ): Unit = {

    SwingUtilities.invokeLater(
      new Runnable {

        def run(): Unit = {

          val login =
            new LoginWindow(
              () => {
                val app =
                  new CricketLiveCenter()

                app.setVisible(true)
              }
            )

          login.setVisible(true)
        }
      }
    )
  }
}

StartCricketLiveCenter.main(
  Array[String]()
)